mirror of
https://git.lecygnenoir.info/LecygneNoir/prismedia.git
synced 2025-10-03 17:39:16 +02:00
Compare commits
170 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 | ||
![]() |
5607c8ea06 | ||
![]() |
25435453bd | ||
![]() |
dab44244f3 | ||
![]() |
26476347d3 | ||
![]() |
5160e9e68d | ||
![]() |
e61a70460d | ||
![]() |
0aae4da68f | ||
![]() |
cbb7c745de | ||
![]() |
c2db597388 | ||
![]() |
e33a4c91a0 | ||
![]() |
bb451e108d | ||
![]() |
28a7541fa8 | ||
![]() |
320c3b1a0b | ||
![]() |
fbfe3356ec | ||
![]() |
e7a4d1656a | ||
![]() |
1e9b719e0c | ||
![]() |
ee578e8e82 | ||
![]() |
f8ca4b093a | ||
![]() |
379aef1dd8 | ||
![]() |
1c441bf67a | ||
![]() |
dc7dd5cb46 | ||
![]() |
25682af83a | ||
![]() |
542bb6f1f9 | ||
![]() |
5022aface4 | ||
![]() |
003830696f | ||
![]() |
2e4e876169 | ||
![]() |
e52e7f354d | ||
![]() |
701e61413c | ||
![]() |
4e20d9efc4 | ||
![]() |
802d70b8d5 | ||
![]() |
a1c472a5fa | ||
![]() |
57a4f3dfd0 | ||
![]() |
4e5c2e1245 | ||
![]() |
1b55340b34 | ||
![]() |
64c5378e18 | ||
![]() |
1169301f14 | ||
![]() |
03ae92d1af | ||
![]() |
11a91af534 | ||
![]() |
881a01f862 | ||
![]() |
ef5d6b843a | ||
![]() |
17017ae90c | ||
![]() |
e91ada951f | ||
![]() |
af65627fcf | ||
![]() |
cb39eef8e0 | ||
![]() |
8faae852ea | ||
![]() |
72b47b95ec | ||
![]() |
b1a5d244d4 | ||
![]() |
429ea2333e | ||
![]() |
159ab00cc9 | ||
![]() |
1dd41f0c46 | ||
![]() |
04e5c326ee | ||
![]() |
99eee2363b | ||
![]() |
76e379ab97 | ||
![]() |
1e72033846 | ||
![]() |
2a624e1d9b | ||
![]() |
2b00d65546 | ||
![]() |
77bcda7a79 | ||
![]() |
591ed0ab80 | ||
![]() |
e94b48278a | ||
![]() |
8c99747898 | ||
![]() |
aa7aeed688 | ||
![]() |
4f2a69e025 | ||
![]() |
aafa71ce6d | ||
![]() |
ee92ff3a6f | ||
![]() |
fa633ee5bb | ||
![]() |
8b26f0ee53 | ||
![]() |
7b0c543865 | ||
![]() |
e5c8c4c9b9 | ||
![]() |
5dc6c78211 | ||
![]() |
83a1d30c1c | ||
![]() |
1fc0577ce7 | ||
![]() |
ee2e11b788 | ||
![]() |
9c72a563bd | ||
![]() |
aa81f13973 | ||
![]() |
44875b3567 | ||
![]() |
322774a214 | ||
![]() |
b21317ec7e | ||
![]() |
4ea1daf966 | ||
![]() |
ef30541688 | ||
![]() |
42a20308f0 | ||
![]() |
070e05de0b | ||
![]() |
b29b9cedef | ||
![]() |
6f67de0a3d | ||
![]() |
6b1260c7bc | ||
![]() |
384a50ca63 | ||
![]() |
fff3b50074 | ||
![]() |
91bdfafb45 | ||
![]() |
2a8449afec | ||
![]() |
4707632f13 | ||
![]() |
82fd09c0e7 | ||
![]() |
9b3d793975 | ||
![]() |
3b38290040 | ||
![]() |
8f0fc4cfb5 | ||
![]() |
7db1ad2836 | ||
![]() |
2f40ef1826 | ||
![]() |
3797c9a9f0 | ||
![]() |
5907859066 | ||
![]() |
bddf2ee414 | ||
![]() |
f66ba6cc21 | ||
![]() |
8d8898aa55 | ||
![]() |
6c68c3363b | ||
![]() |
617e989154 | ||
![]() |
dbcd2ff010 | ||
![]() |
dffd3ffa84 | ||
![]() |
7e4f9d995c | ||
![]() |
8c0f1fd038 | ||
![]() |
08416d2796 | ||
![]() |
2338188325 | ||
![]() |
9426ca465c | ||
![]() |
70a933f48a | ||
![]() |
097ff965bb | ||
![]() |
ce671dab8e |
24 changed files with 2530 additions and 878 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -61,4 +61,8 @@ target/
|
|||
# Project
|
||||
youtube_secret.json
|
||||
peertube_secret
|
||||
.youtube_credentials.json
|
||||
.youtube_credentials.json
|
||||
nfo_example.txt
|
||||
peertube_secret.sample
|
||||
youtube_secret.json.sample
|
||||
*.mp4
|
160
CHANGELOG.md
160
CHANGELOG.md
|
@ -1,5 +1,163 @@
|
|||
# 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 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)
|
||||
|
||||
## v0.10.1
|
||||
|
||||
### Fix
|
||||
- Fix a bug introduced with v0.10.0 that broke thumbnail on youtube upload.
|
||||
|
||||
## v0.10.0
|
||||
|
||||
### Features
|
||||
- Add the possibility to specify strict checks option to never forgot parameters when uploading (see #36)
|
||||
- Improve logging system, add options for batch upload and print url-only in the stdout (see #29)
|
||||
- --debug option is now deprecated in favor of --log=debug
|
||||
|
||||
### Fixes
|
||||
- Workaround against the Youtube API breakdown while adding video in playlist. See #47 for details. Should be removed once Google fix their bugs.
|
||||
|
||||
|
||||
## v0.9.1
|
||||
|
||||
### Features
|
||||
- Possibility to bypass the MIME check for .mp4 when the user is sure of its video (#46 , thanks to @zykino)
|
||||
- Now **available with pip** for installation! (See the README for doc)
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
**Using poetry** (recommanded)
|
||||
|
||||
- [install poetry](https://python-poetry.org/docs/#installation)
|
||||
- git pull the repo
|
||||
- install prismedia:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
- use prismedia from the command line directly from your path:
|
||||
```bash
|
||||
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`.
|
||||
Once you have pulled the new v0.9.0, you may update by using:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
# Then use prismedia through python command line:
|
||||
python -m prismedia -h
|
||||
```
|
||||
|
||||
### Features
|
||||
- Prismedia now uses [poetry](https://python-poetry.org) to allow easier installation usage and build, see the README (fix #34)
|
||||
- Add two new options to schedule video by platform. You may now use youtubeAt and peertubeAt to prepare previews (fix #43)
|
||||
- Enhance the NFO system to allow a hierarchical loading of multiple NFO, with priorities. See README and [prismedia/samples](prismedia/samples) for details (fix #11)
|
||||
|
||||
## v0.8.0
|
||||
|
||||
### Breaking changes
|
||||
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)
|
||||
- Now uploading to Peertube before Youtube (thanks to @zykino)
|
||||
|
||||
## v0.7.1
|
||||
|
||||
### Fixes
|
||||
Fix bug #42 , crash on Peertube when video has only one tag
|
||||
|
||||
## v0.7.0
|
||||
|
||||
### Features
|
||||
Support Peertube channel additionally with playlist for Peertube!
|
||||
Peertube only as channel are Peertube's feature. See #40 for details.
|
||||
|
||||
### Fixes
|
||||
- Best uses of special chars in videoname, channel name and playlist name
|
||||
- Some fixes in logging message for better lisibility
|
||||
- Readme features list improved for better lisibility
|
||||
|
||||
## v0.6.4
|
||||
|
||||
### Fixes
|
||||
- Fix #33, no more trying to add a video into a playlist when the playlist does not exist on Youtube
|
||||
- fix #39, patch the playlist name check when playlist contains special chars
|
||||
|
||||
## v0.6.3
|
||||
|
||||
### Fixes
|
||||
Fix Critical bug #38 that prevent upload when creating playlists on Peertube, as public playlist need a non-null channel_id.
|
||||
|
||||
## v0.6.2
|
||||
|
||||
**Warning**: your Peertube instance should be at least in v1.3.0 to use this new functionality.
|
||||
|
||||
### Features
|
||||
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.
|
||||
**Warning** you need a new prerequisites: python-unidecode
|
||||
|
||||
- Remove mastodon tags (mt) options as it's deprecated. Compatibility between Peertube and Mastodon is complete.
|
||||
- Simplify python2 specific functions
|
||||
|
||||
## v0.6.1
|
||||
|
||||
### Fixes
|
||||
- fix an error when playlists on Peertube have same names but not same display names (issue #20)
|
||||
- fix an error where videos does not upload on Peertube when some characters are used in playlist(issue #19)
|
||||
|
||||
## v0.6
|
||||
|
||||
### Compatibility ###
|
||||
|
@ -24,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
|
||||
|
|
296
README.md
296
README.md
|
@ -1,166 +1,236 @@
|
|||
# Prismedia
|
||||
|
||||
A scripting way to upload videos to peertube and youtube written in python2
|
||||
Scripting your way to upload videos to peertube and youtube. Works with Python 3.5+.
|
||||
|
||||
[TOC]: #
|
||||
|
||||
## Table of Contents
|
||||
- [Installation](#installation-and-upgrade)
|
||||
- [From pip](#from-pip)
|
||||
- [From source](#from-source)
|
||||
- [Configuration](#configuration)
|
||||
- [Peertube](#peertube)
|
||||
- [Youtube](#youtube)
|
||||
- [Usage](#usage)
|
||||
- [Enhanced use of NFO](#enhanced-use-of-nfo)
|
||||
- [Strict check options](#strict-check-options)
|
||||
- [Features](#features)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Inspirations](#inspirations)
|
||||
- [Contributors](#contributors)
|
||||
|
||||
## Installation and upgrade
|
||||
|
||||
### From pip
|
||||
|
||||
Simply install with
|
||||
```sh
|
||||
pip install prismedia
|
||||
```
|
||||
|
||||
Upgrade with
|
||||
```sh
|
||||
pip install --upgrade prismedia
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
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 > requirements.txt`)
|
||||
|
||||
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
|
||||
(Or use the existing virtualenv if one is activated)
|
||||
|
||||
```sh
|
||||
poetry install
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
Search in your package manager, otherwise use ``pip install --upgrade``
|
||||
- google-auth
|
||||
- google-auth-oauthlib
|
||||
- google-auth-httplib2
|
||||
- google-api-python-client
|
||||
- docopt
|
||||
- schema
|
||||
- python-magic
|
||||
- python-magic-bin
|
||||
- requests-toolbelt
|
||||
- tzlocal
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit peertube_secret and youtube_secret.json with your credentials.
|
||||
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 as 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``.
|
||||
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`.
|
||||
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.
|
||||
If you plan an larger usage, please consider creating your own youtube_secret file:
|
||||
**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
|
||||
- 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.
|
||||
- Go to the [Google console](https://console.developers.google.com/).
|
||||
- Create project.
|
||||
- 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.
|
||||
|
||||
## How To
|
||||
Currently in heavy development
|
||||
## Usage
|
||||
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))
|
||||
|
||||
Support only mp4 for cross compatibility between Youtube and Peertube
|
||||
|
||||
Simply upload a video:
|
||||
Here are some demonstration of main usage:
|
||||
|
||||
Upload a video:
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4"
|
||||
```
|
||||
./prismedia_upload.py --file="yourvideo.mp4"
|
||||
```
|
||||
|
||||
|
||||
Specify description and tags:
|
||||
|
||||
```
|
||||
./prismedia_upload.py --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
||||
```
|
||||
|
||||
Provide a thumbnail:
|
||||
|
||||
```
|
||||
./prismedia_upload.py --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
|
||||
```sh
|
||||
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:
|
||||
|
||||
```
|
||||
./prismedia_upload.py --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
|
||||
(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
|
||||
--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 space and special characters (!, ', ", ?, ...)
|
||||
are not supported by Mastodon to be published from Peertube
|
||||
use mastodon compatibility below
|
||||
--mt Force Mastodon compatibility for tags (drop every incompatible characters inside tags)
|
||||
This option requires --tags
|
||||
-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 video name
|
||||
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
|
||||
For Peertube, requires the "atd" and "curl utilities installed on the system
|
||||
--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
|
||||
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
|
||||
If the playlist is not found, spawn an error except if --playlist-create 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.
|
||||
|
||||
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, **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 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:
|
||||
`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
|
||||
├── nfo.txt
|
||||
├── samples.txt
|
||||
├── yourvideo1.mp4
|
||||
├── yourvideo1.txt
|
||||
├── yourvideo1.jpg
|
||||
├── yourvideo2.mp4
|
||||
└── yourvideo2.txt
|
||||
```
|
||||
|
||||
By using
|
||||
```sh
|
||||
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
|
||||
```
|
||||
|
||||
Prismedia will:
|
||||
- look for options in `nfo.txt`
|
||||
- look for options in `samples.txt` (from directory name) and erase any previous conflicting options
|
||||
- look for options in `yourvideo1.txt` (from video name) and erase any previous conflicting options
|
||||
- look for options in `cli_nfo.txt` (from the `--nfo` in command line) and erase any previous conflicting options
|
||||
- erase any previous option regarding CCA as it's specified in cli with `--cca`
|
||||
- take `yourvideo1.jpg` as thumbnail if no other files has been specified in previous NFO
|
||||
|
||||
In other word, Prismedia will use option given in cli, then look for option in cli_nfo.txt, then complete with video_name.txt, then directory_name.txt, and finally complete with nfo.txt
|
||||
|
||||
It allows to specify more easily default options for an entire set of video, directory, playlist and so on.
|
||||
|
||||
## Strict check options
|
||||
Since prismedia v0.10.0, a bunch of special options have been added to force the presence of parameters before uploading.
|
||||
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
|
||||
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
|
||||
tags, thumbnail, ...
|
||||
All strict option are optionals and are provided only to avoid errors when uploading :-)
|
||||
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
|
||||
All strict options are off by default.
|
||||
|
||||
Available strict options:
|
||||
- --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory
|
||||
- --withThumbnail Prevent the upload without a thumbnail
|
||||
- --withName Prevent the upload if no name are found
|
||||
- --withDescription Prevent the upload without description
|
||||
- --withTags Prevent the upload without tags
|
||||
- --withPlaylist Prevent the upload if no playlist
|
||||
- --withPublishAt Prevent the upload if no schedule
|
||||
- --withPlatform Prevent the upload if at least one platform is not specified
|
||||
- --withCategory Prevent the upload if no category
|
||||
- --withLanguage Prevent upload if no language
|
||||
- --withChannel Prevent upload if no channel
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Youtube upload
|
||||
- [x] Peertube upload
|
||||
- Support of all videos arguments (description, tags, category, licence, ...)
|
||||
- Support of videos parameters (description, tags, category, licence, ...)
|
||||
- [x] description
|
||||
- [x] tags (no more than 30 characters per tag as Peertube does not support it)
|
||||
- [x] Option to force tags to be compatible with Mastodon publication
|
||||
- [x] categories
|
||||
- [x] license: cca or not (Youtube only as Peertube uses Attribution by design)
|
||||
- [x] privacy (between public, unlisted or private)
|
||||
- [x] enabling/disabling comment (Peertube only as Youtube API does not support it)
|
||||
- [x] nsfw (Peertube only as Youtube API does not support it)
|
||||
- [x] set default language
|
||||
- [x] thumbnail/preview
|
||||
- [x] thumbnail
|
||||
- [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
|
||||
- [x] add videos to playlist for Peertube
|
||||
- [x] add videos to playlist for Youtube
|
||||
- [x] add videos to playlist
|
||||
- [x] create playlist
|
||||
- [x] schedule your video with publishAt
|
||||
- [x] combine channel and playlist (Peertube only as channel is Peertube feature). See [issue 40](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/40) for detailed usage.
|
||||
- [x] Use a config file (NFO) file to retrieve videos arguments
|
||||
- [x] Allow to choose peertube or youtube upload (to resume failed upload for example)
|
||||
- [x] Add publishAt option to plan your videos
|
||||
- [ ] Record and forget: put the video in a directory, and the script uploads it for you
|
||||
- [ ] Usable on Desktop (Linux and/or Windows and/or MacOS)
|
||||
- [ ] Graphical User Interface
|
||||
- [x] Allow choosing peertube or youtube upload (to retry a failed upload for example)
|
||||
- [x] Usable on Desktop (Linux and/or Windows and/or MacOS)
|
||||
- [x] Different schedules on platforms to prepare preview
|
||||
- [x] Possibility to force the presence of upload options
|
||||
- [ ] Copy and forget, eg possibility to copy video in a directory, and prismedia uploads itself: [Work in progress](https://git.lecygnenoir.info/Zykino/prismedia-autoupload) thanks to @Zykino 🎉 (Discussions in [issue 27](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/27))
|
||||
- [ ] A usable graphical interface
|
||||
|
||||
## Compatibility
|
||||
|
||||
If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
|
||||
- If you still use python2, use the version 0.7.1 (no more updated)
|
||||
- If you use peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3
|
||||
|
||||
## Sources
|
||||
inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
|
||||
## Inspirations
|
||||
Inspired by peeror (First peertube mirror by Rigelk) and [youtube-upload](https://github.com/tokland/youtube-upload)
|
||||
|
||||
## Contributors
|
||||
Thanks to: @LecygneNoir, @Zykino, @meewan, @ysalmon, @rigelk 😘
|
||||
|
|
226
lib/pt_upload.py
226
lib/pt_upload.py
|
@ -1,226 +0,0 @@
|
|||
#!/usr/bin/env python2
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import mimetypes
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import pytz
|
||||
from os.path import splitext, basename, abspath
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from ConfigParser import RawConfigParser
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from oauthlib.oauth2 import LegacyApplicationClient
|
||||
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
||||
|
||||
import utils
|
||||
|
||||
PEERTUBE_SECRETS_FILE = 'peertube_secret'
|
||||
PEERTUBE_PRIVACY = {
|
||||
"public": 1,
|
||||
"unlisted": 2,
|
||||
"private": 3
|
||||
}
|
||||
|
||||
|
||||
def get_authenticated_service(secret):
|
||||
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
|
||||
|
||||
oauth_client = LegacyApplicationClient(
|
||||
client_id=str(secret.get('peertube', 'client_id'))
|
||||
)
|
||||
try:
|
||||
oauth = OAuth2Session(client=oauth_client)
|
||||
oauth.fetch_token(
|
||||
token_url=str(peertube_url + '/api/v1/users/token'),
|
||||
# lower as peertube does not store uppercase for pseudo
|
||||
username=str(secret.get('peertube', 'username').lower()),
|
||||
password=str(secret.get('peertube', 'password')),
|
||||
client_id=str(secret.get('peertube', 'client_id')),
|
||||
client_secret=str(secret.get('peertube', 'client_secret'))
|
||||
)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logging.error("Peertube: Error: " + str(e.message))
|
||||
exit(1)
|
||||
else:
|
||||
logging.error("Peertube: Error: " + str(e))
|
||||
exit(1)
|
||||
return oauth
|
||||
|
||||
|
||||
def get_default_playlist(user_info):
|
||||
return user_info['videoChannels'][0]['id']
|
||||
|
||||
|
||||
def get_playlist_by_name(user_info, options):
|
||||
for playlist in user_info["videoChannels"]:
|
||||
if playlist['displayName'].encode('utf8') == str(options.get('--playlist')):
|
||||
return playlist['id']
|
||||
|
||||
|
||||
def create_playlist(oauth, url, options):
|
||||
template = ('Peertube: Playlist %s does not exist, creating it.')
|
||||
logging.info(template % (str(options.get('--playlist'))))
|
||||
playlist_name = utils.cleanString(str(options.get('--playlist')))
|
||||
# Peertube allows 20 chars max for playlist name
|
||||
playlist_name = playlist_name[:19]
|
||||
data = '{"name":"' + playlist_name +'", \
|
||||
"displayName":"' + str(options.get('--playlist')) +'", \
|
||||
"description":null}'
|
||||
|
||||
headers = {
|
||||
'Content-Type': "application/json"
|
||||
}
|
||||
try:
|
||||
response = oauth.post(url + "/api/v1/video-channels/",
|
||||
data=data,
|
||||
headers=headers)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logging.error("Error: " + str(e.message))
|
||||
else:
|
||||
logging.error("Error: " + str(e))
|
||||
if response is not None:
|
||||
if response.status_code == 200:
|
||||
jresponse = response.json()
|
||||
jresponse = jresponse['videoChannel']
|
||||
return jresponse['id']
|
||||
else:
|
||||
logging.error(('Peertube: The upload failed with an unexpected response: '
|
||||
'%s') % response)
|
||||
exit(1)
|
||||
|
||||
|
||||
def upload_video(oauth, secret, options):
|
||||
|
||||
def get_userinfo():
|
||||
return json.loads(oauth.get(url+"/api/v1/users/me").content)
|
||||
|
||||
def get_file(path):
|
||||
mimetypes.init()
|
||||
return (basename(path), open(abspath(path), 'rb'),
|
||||
mimetypes.types_map[splitext(path)[1]])
|
||||
|
||||
path = options.get('--file')
|
||||
url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
|
||||
user_info = get_userinfo()
|
||||
|
||||
# We need to transform fields into tuple to deal with tags as
|
||||
# MultipartEncoder does not support list refer
|
||||
# https://github.com/requests/toolbelt/issues/190 and
|
||||
# https://github.com/requests/toolbelt/issues/205
|
||||
fields = [
|
||||
("name", options.get('--name') or splitext(basename(options.get('--file')))[0]),
|
||||
("licence", "1"),
|
||||
("description", options.get('--description') or "default description"),
|
||||
("nsfw", str(int(options.get('--nsfw')) or "0")),
|
||||
("videofile", get_file(path))
|
||||
]
|
||||
|
||||
if options.get('--tags'):
|
||||
tags = options.get('--tags').split(',')
|
||||
for strtag in tags:
|
||||
# Empty tag crashes Peertube, so skip them
|
||||
if strtag == "":
|
||||
continue
|
||||
# Tag more than 30 chars crashes Peertube, so exit and check tags
|
||||
if len(strtag) >= 30:
|
||||
logging.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce your tag size")
|
||||
exit(1)
|
||||
# If Mastodon compatibility is enabled, clean tags from special characters
|
||||
if options.get('--mt'):
|
||||
strtag = utils.cleanString(strtag)
|
||||
fields.append(("tags", strtag))
|
||||
|
||||
if options.get('--category'):
|
||||
fields.append(("category", str(utils.getCategory(options.get('--category'), 'peertube'))))
|
||||
else:
|
||||
# if no category, set default to 2 (Films)
|
||||
fields.append(("category", "2"))
|
||||
|
||||
if options.get('--language'):
|
||||
fields.append(("language", str(utils.getLanguage(options.get('--language'), "peertube"))))
|
||||
else:
|
||||
# if no language, set default to 1 (English)
|
||||
fields.append(("language", "en"))
|
||||
|
||||
if options.get('--disable-comments'):
|
||||
fields.append(("commentsEnabled", "0"))
|
||||
else:
|
||||
fields.append(("commentsEnabled", "1"))
|
||||
|
||||
privacy = None
|
||||
if options.get('--privacy'):
|
||||
privacy = options.get('--privacy').lower()
|
||||
|
||||
if options.get('--publishAt'):
|
||||
publishAt = options.get('--publishAt')
|
||||
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S')
|
||||
tz = get_localzone()
|
||||
tz = pytz.timezone(str(tz))
|
||||
publishAt = tz.localize(publishAt).isoformat()
|
||||
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"])))
|
||||
|
||||
if options.get('--thumbnail'):
|
||||
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
|
||||
fields.append(("previewfile", get_file(options.get('--thumbnail'))))
|
||||
|
||||
if options.get('--playlist'):
|
||||
playlist_id = get_playlist_by_name(user_info, options)
|
||||
if not playlist_id and options.get('--playlistCreate'):
|
||||
playlist_id = create_playlist(oauth, url, options)
|
||||
elif not playlist_id:
|
||||
logging.warning("Playlist `" + options.get('--playlist') + "` is unknown, using default playlist.")
|
||||
playlist_id = get_default_playlist(user_info)
|
||||
else:
|
||||
playlist_id = get_default_playlist(user_info)
|
||||
fields.append(("channelId", str(playlist_id)))
|
||||
|
||||
multipart_data = MultipartEncoder(fields)
|
||||
|
||||
headers = {
|
||||
'Content-Type': multipart_data.content_type
|
||||
}
|
||||
response = oauth.post(url + "/api/v1/videos/upload",
|
||||
data=multipart_data,
|
||||
headers=headers)
|
||||
if response is not None:
|
||||
if response.status_code == 200:
|
||||
jresponse = response.json()
|
||||
jresponse = jresponse['video']
|
||||
uuid = jresponse['uuid']
|
||||
idvideo = str(jresponse['id'])
|
||||
logging.info('Peertube : Video was successfully uploaded.')
|
||||
template = 'Peertube: Watch it at %s/videos/watch/%s.'
|
||||
logging.info(template % (url, uuid))
|
||||
else:
|
||||
logging.error(('Peertube: The upload failed with an unexpected response: '
|
||||
'%s') % response)
|
||||
exit(1)
|
||||
|
||||
|
||||
def run(options):
|
||||
secret = RawConfigParser()
|
||||
try:
|
||||
secret.read(PEERTUBE_SECRETS_FILE)
|
||||
except Exception as e:
|
||||
logging.error("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
|
||||
exit(1)
|
||||
insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT')
|
||||
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport
|
||||
oauth = get_authenticated_service(secret)
|
||||
try:
|
||||
logging.info('Peertube: Uploading video...')
|
||||
upload_video(oauth, secret, options)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logging.error("Peertube: Error: " + str(e.message))
|
||||
else:
|
||||
logging.error("Peertube: Error: " + str(e))
|
210
lib/utils.py
210
lib/utils.py
|
@ -1,210 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
|
||||
from os.path import dirname, splitext, basename, isfile
|
||||
from os import devnull
|
||||
from subprocess import check_call, CalledProcessError, STDOUT
|
||||
import unicodedata
|
||||
import logging
|
||||
|
||||
### CATEGORIES ###
|
||||
YOUTUBE_CATEGORY = {
|
||||
"music": 10,
|
||||
"films": 1,
|
||||
"vehicles": 2,
|
||||
"sport": 17,
|
||||
"travels": 19,
|
||||
"gaming": 20,
|
||||
"people": 22,
|
||||
"comedy": 23,
|
||||
"entertainment": 24,
|
||||
"news": 25,
|
||||
"how to": 26,
|
||||
"education": 27,
|
||||
"activism": 29,
|
||||
"science & technology": 28,
|
||||
"science": 28,
|
||||
"technology": 28,
|
||||
"animals": 15
|
||||
}
|
||||
|
||||
PEERTUBE_CATEGORY = {
|
||||
"music": 1,
|
||||
"films": 2,
|
||||
"vehicles": 3,
|
||||
"sport": 5,
|
||||
"travels": 6,
|
||||
"gaming": 7,
|
||||
"people": 8,
|
||||
"comedy": 9,
|
||||
"entertainment": 10,
|
||||
"news": 11,
|
||||
"how to": 12,
|
||||
"education": 13,
|
||||
"activism": 14,
|
||||
"science & technology": 15,
|
||||
"science": 15,
|
||||
"technology": 15,
|
||||
"animals": 16
|
||||
}
|
||||
|
||||
### LANGUAGES ###
|
||||
YOUTUBE_LANGUAGE = {
|
||||
"arabic": 'ar',
|
||||
"english": 'en',
|
||||
"french": 'fr',
|
||||
"german": 'de',
|
||||
"hindi": 'hi',
|
||||
"italian": 'it',
|
||||
"japanese": 'ja',
|
||||
"korean": 'ko',
|
||||
"mandarin": 'zh-CN',
|
||||
"portuguese": 'pt-PT',
|
||||
"punjabi": 'pa',
|
||||
"russian": 'ru',
|
||||
"spanish": 'es'
|
||||
}
|
||||
|
||||
PEERTUBE_LANGUAGE = {
|
||||
"arabic": "ar",
|
||||
"english": "en",
|
||||
"french": "fr",
|
||||
"german": "de",
|
||||
"hindi": "hi",
|
||||
"italian": "it",
|
||||
"japanese": "ja",
|
||||
"korean": "ko",
|
||||
"mandarin": "zh",
|
||||
"portuguese": "pt",
|
||||
"punjabi": "pa",
|
||||
"russian": "ru",
|
||||
"spanish": "es"
|
||||
}
|
||||
######################
|
||||
|
||||
|
||||
def getCategory(category, platform):
|
||||
if platform == "youtube":
|
||||
return YOUTUBE_CATEGORY[category.lower()]
|
||||
else:
|
||||
return PEERTUBE_CATEGORY[category.lower()]
|
||||
|
||||
|
||||
def getLanguage(language, platform):
|
||||
if platform == "youtube":
|
||||
return YOUTUBE_LANGUAGE[language.lower()]
|
||||
else:
|
||||
return PEERTUBE_LANGUAGE[language.lower()]
|
||||
|
||||
|
||||
def remove_empty_kwargs(**kwargs):
|
||||
good_kwargs = {}
|
||||
if kwargs is not None:
|
||||
for key, value in kwargs.iteritems():
|
||||
if value:
|
||||
good_kwargs[key] = value
|
||||
return good_kwargs
|
||||
|
||||
def searchThumbnail(options):
|
||||
video_directory = dirname(options.get('--file')) + "/"
|
||||
# First, check for thumbnail based on videoname
|
||||
if options.get('--name'):
|
||||
if isfile(video_directory + options.get('--name') + ".jpg"):
|
||||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
|
||||
elif isfile(video_directory + options.get('--name') + ".jpeg"):
|
||||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
|
||||
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
|
||||
if not options.get('--thumbnail'):
|
||||
video_file = splitext(basename(options.get('--file')))[0]
|
||||
if isfile(video_directory + video_file + ".jpg"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".jpg"
|
||||
elif isfile(video_directory + video_file + ".jpeg"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".jpeg"
|
||||
return options
|
||||
|
||||
|
||||
# return the nfo as a RawConfigParser object
|
||||
def loadNFO(options):
|
||||
video_directory = dirname(options.get('--file')) + "/"
|
||||
if options.get('--nfo'):
|
||||
try:
|
||||
logging.info("Using " + options.get('--nfo') + " as NFO, loading...")
|
||||
if isfile(options.get('--nfo')):
|
||||
nfo = RawConfigParser()
|
||||
nfo.read(options.get('--nfo'))
|
||||
return nfo
|
||||
else:
|
||||
logging.error("Given NFO file does not exist, please check your path.")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
logging.error("Problem with NFO file: " + str(e))
|
||||
exit(1)
|
||||
else:
|
||||
if options.get('--name'):
|
||||
nfo_file = video_directory + options.get('--name') + ".txt"
|
||||
if isfile(nfo_file):
|
||||
try:
|
||||
logging.info("Using " + nfo_file + " as NFO, loading...")
|
||||
nfo = RawConfigParser()
|
||||
nfo.read(nfo_file)
|
||||
return nfo
|
||||
except Exception as e:
|
||||
logging.error("Problem with NFO file: " + str(e))
|
||||
exit(1)
|
||||
|
||||
# if --nfo and --name does not exist, use --file as default
|
||||
video_file = splitext(basename(options.get('--file')))[0]
|
||||
nfo_file = video_directory + video_file + ".txt"
|
||||
if isfile(nfo_file):
|
||||
try:
|
||||
logging.info("Using " + nfo_file + " as NFO, loading...")
|
||||
nfo = RawConfigParser()
|
||||
nfo.read(nfo_file)
|
||||
return nfo
|
||||
except Exception as e:
|
||||
logging.error("Problem with nfo file: " + str(e))
|
||||
exit(1)
|
||||
logging.info("No suitable NFO found, skipping.")
|
||||
return False
|
||||
|
||||
|
||||
def parseNFO(options):
|
||||
nfo = loadNFO(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.iteritems():
|
||||
key = key.replace("-", "")
|
||||
try:
|
||||
# get string options
|
||||
if value is None and nfo.get('video', key):
|
||||
options['--' + key] = nfo.get('video', key)
|
||||
# get boolean options
|
||||
elif value is False and nfo.getboolean('video', key):
|
||||
options['--' + key] = nfo.getboolean('video', key)
|
||||
except NoOptionError:
|
||||
continue
|
||||
except NoSectionError:
|
||||
logging.error("Given NFO file miss section [video], please check syntax of your NFO.")
|
||||
exit(1)
|
||||
return options
|
||||
|
||||
|
||||
def upcaseFirstLetter(s):
|
||||
return s[0].upper() + s[1:]
|
||||
|
||||
|
||||
def cleanString(toclean):
|
||||
toclean = toclean.split(' ')
|
||||
cleaned = ''
|
||||
for s in toclean:
|
||||
if s == '':
|
||||
continue
|
||||
strtoclean = unicodedata.normalize('NFKD', unicode (s, 'utf-8')).encode('ASCII', 'ignore')
|
||||
strtoclean = ''.join(e for e in strtoclean if e.isalnum())
|
||||
if strtoclean == '':
|
||||
continue
|
||||
strtoclean = upcaseFirstLetter(strtoclean)
|
||||
cleaned = cleaned + strtoclean
|
||||
|
||||
return cleaned
|
|
@ -1,26 +0,0 @@
|
|||
### This NFO example show how to construct a NFO for your video ###
|
||||
### All fields are optional, but you need at least one fields (otherwise NFO is useless :-p) ###
|
||||
### See --help for options explanation
|
||||
### Prismedia will search and use NFO in this order: ###
|
||||
### 1. file passed in command line through --nfo ###
|
||||
### 2. file inside video directory named after --name command line option append with .txt ###
|
||||
### 3. file inside video directory named after --file command line option with .txt extension ###
|
||||
[video]
|
||||
name = videoname
|
||||
description = Your complete video description
|
||||
Multilines description
|
||||
should be wrote with a blank space
|
||||
at the beginning of the line :)
|
||||
tags = list of tags, comma separated
|
||||
mt = True
|
||||
category = Films
|
||||
cca = True
|
||||
privacy = private
|
||||
disable-comments = True
|
||||
thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
|
||||
playlist = My Test Playlist
|
||||
playlistCreate = True
|
||||
nsfw = True
|
||||
platform = youtube, peertube
|
||||
language = French
|
||||
publishAt=2034-05-07T19:00:00
|
638
poetry.lock
generated
Normal file
638
poetry.lock
generated
Normal file
|
@ -0,0 +1,638 @@
|
|||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "args"
|
||||
version = "0.1.0"
|
||||
description = "Command Arguments for Humans."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "args-0.1.0.tar.gz", hash = "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.5.2"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"},
|
||||
{file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.6.15"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"},
|
||||
{file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"},
|
||||
{file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"},
|
||||
{file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"},
|
||||
{file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"},
|
||||
{file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"},
|
||||
{file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"},
|
||||
{file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"},
|
||||
{file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"},
|
||||
{file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"},
|
||||
{file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clint"
|
||||
version = "0.5.1"
|
||||
description = "Python Command Line Interface Tools"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "clint-0.5.1.tar.gz", hash = "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
args = "*"
|
||||
|
||||
[[package]]
|
||||
name = "configparser"
|
||||
version = "7.2.0"
|
||||
description = "Updated configparser from stdlib for earlier Pythons."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "configparser-7.2.0-py3-none-any.whl", hash = "sha256:fee5e1f3db4156dcd0ed95bc4edfa3580475537711f67a819c966b389d09ce62"},
|
||||
{file = "configparser-7.2.0.tar.gz", hash = "sha256:b629cc8ae916e3afbd36d1b3d093f34193d851e11998920fdcfc4552218b7b70"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["pytest (>=6,!=8.1.*)", "types-backports"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "0.6.2"
|
||||
description = "Pythonic argument parser, that will make you smile"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "future"
|
||||
version = "1.0.0"
|
||||
description = "Clean single-source support for Python 3 and 2"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"},
|
||||
{file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
version = "2.25.1"
|
||||
description = "Google API client core library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"},
|
||||
{file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-auth = ">=2.14.1,<3.0.0"
|
||||
googleapis-common-protos = ">=1.56.2,<2.0.0"
|
||||
proto-plus = [
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
|
||||
]
|
||||
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
requests = ">=2.18.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"]
|
||||
grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\""]
|
||||
grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"]
|
||||
grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.174.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google_api_python_client-2.174.0-py3-none-any.whl", hash = "sha256:f695205ceec97bfaa1590a14282559c4109326c473b07352233a3584cdbf4b89"},
|
||||
{file = "google_api_python_client-2.174.0.tar.gz", hash = "sha256:9eb7616a820b38a9c12c5486f9b9055385c7feb18b20cbafc5c5a688b14f3515"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0"
|
||||
google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
|
||||
google-auth-httplib2 = ">=0.2.0,<1.0.0"
|
||||
httplib2 = ">=0.19.0,<1.0.0"
|
||||
uritemplate = ">=3.0.1,<5"
|
||||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.40.3"
|
||||
description = "Google Authentication Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"},
|
||||
{file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=2.0.0,<6.0"
|
||||
pyasn1-modules = ">=0.2.1"
|
||||
rsa = ">=3.1.4,<5"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"]
|
||||
enterprise-cert = ["cryptography", "pyopenssl"]
|
||||
pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"]
|
||||
pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"]
|
||||
reauth = ["pyu2f (>=0.1.5)"]
|
||||
requests = ["requests (>=2.20.0,<3.0.0)"]
|
||||
testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"]
|
||||
urllib3 = ["packaging", "urllib3"]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-httplib2"
|
||||
version = "0.2.0"
|
||||
description = "Google Authentication Library: httplib2 transport"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"},
|
||||
{file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-auth = "*"
|
||||
httplib2 = ">=0.19.0"
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-oauthlib"
|
||||
version = "1.2.2"
|
||||
description = "Google Authentication Library"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2"},
|
||||
{file = "google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-auth = ">=2.15.0"
|
||||
requests-oauthlib = ">=0.7.0"
|
||||
|
||||
[package.extras]
|
||||
tool = ["click (>=6.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "googleapis-common-protos"
|
||||
version = "1.70.0"
|
||||
description = "Common protobufs used in Google APIs"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"},
|
||||
{file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
|
||||
[package.extras]
|
||||
grpc = ["grpcio (>=1.44.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httplib2"
|
||||
version = "0.22.0"
|
||||
description = "A comprehensive HTTP client library."
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"},
|
||||
{file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "oauthlib"
|
||||
version = "2.1.0"
|
||||
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "oauthlib-2.1.0-py2.py3-none-any.whl", hash = "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"},
|
||||
{file = "oauthlib-2.1.0.tar.gz", hash = "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
rsa = ["cryptography"]
|
||||
signals = ["blinker"]
|
||||
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
|
||||
test = ["blinker", "cryptography", "mock", "nose", "pyjwt (>=1.0.0)", "unittest2"]
|
||||
|
||||
[[package]]
|
||||
name = "proto-plus"
|
||||
version = "1.26.1"
|
||||
description = "Beautiful, Pythonic protocol buffers"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"},
|
||||
{file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
protobuf = ">=3.19.0,<7.0.0"
|
||||
|
||||
[package.extras]
|
||||
testing = ["google-api-core (>=1.31.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.31.1"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9"},
|
||||
{file = "protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447"},
|
||||
{file = "protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402"},
|
||||
{file = "protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39"},
|
||||
{file = "protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6"},
|
||||
{file = "protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16"},
|
||||
{file = "protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9"},
|
||||
{file = "protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e"},
|
||||
{file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
|
||||
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.4.2"
|
||||
description = "A collection of ASN.1-based protocols modules"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"},
|
||||
{file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.6.1,<0.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.3"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"},
|
||||
{file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2022.1"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
|
||||
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
|
||||
{file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset_normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<3"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests-oauthlib"
|
||||
version = "1.1.0"
|
||||
description = "OAuthlib authentication support for Requests."
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-oauthlib-1.1.0.tar.gz", hash = "sha256:eabd8eb700ebed81ba080c6ead96d39d6bdc39996094bd23000204f6965786b0"},
|
||||
{file = "requests_oauthlib-1.1.0-py2.py3-none-any.whl", hash = "sha256:be76f2bb72ca5525998e81d47913e09b1ca8b7957ae89b46f787a79e68ad5e61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
oauthlib = ">=2.1.0,<3.0.0"
|
||||
requests = ">=2.0.0"
|
||||
|
||||
[package.extras]
|
||||
rsa = ["oauthlib[signedtoken] (>=2.1.0,<3.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests-toolbelt"
|
||||
version = "1.0.0"
|
||||
description = "A utility belt for advanced users of python-requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
requests = ">=2.0.1,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.2"
|
||||
description = "Pure-Python RSA implementation"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.13\""
|
||||
files = [
|
||||
{file = "rsa-4.2.tar.gz", hash = "sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.9.1"
|
||||
description = "Pure-Python RSA implementation"
|
||||
optional = false
|
||||
python-versions = "<4,>=3.6"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"3.13\""
|
||||
files = [
|
||||
{file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"},
|
||||
{file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "schema"
|
||||
version = "0.7.7"
|
||||
description = "Simple data validation library"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde"},
|
||||
{file = "schema-0.7.7.tar.gz", hash = "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
description = "Provider of IANA time zone data"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
|
||||
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
description = "tzinfo object for the local timezone"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"},
|
||||
{file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
|
||||
|
||||
[[package]]
|
||||
name = "unidecode"
|
||||
version = "1.4.0"
|
||||
description = "ASCII transliterations of Unicode text"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021"},
|
||||
{file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uritemplate"
|
||||
version = "4.2.0"
|
||||
description = "Implementation of RFC 6570 URI Templates"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"},
|
||||
{file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
|
||||
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9"
|
||||
content-hash = "c73f25255238f00f01c07afb952eae06d70fb2727b3839af7a7ec404d71a51d8"
|
12
prismedia/__init__.py
Normal file
12
prismedia/__init__.py
Normal file
|
@ -0,0 +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
|
2
prismedia/__main__.py
Normal file
2
prismedia/__main__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .upload import main
|
||||
main()
|
24
prismedia/genconfig.py
Normal file
24
prismedia/genconfig.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
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():
|
||||
path = join(dirname(__file__), 'config')
|
||||
files = [f for f in listdir(path) if isfile(join(path, f))]
|
||||
|
||||
for f in files:
|
||||
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__':
|
||||
genconfig()
|
401
prismedia/pt_upload.py
Normal file
401
prismedia/pt_upload.py
Normal file
|
@ -0,0 +1,401 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import mimetypes
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import datetime
|
||||
import pytz
|
||||
from os.path import splitext, basename, abspath
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from configparser import RawConfigParser
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from oauthlib.oauth2 import LegacyApplicationClient
|
||||
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
||||
from clint.textui.progress import Bar as ProgressBar
|
||||
|
||||
from . import utils
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
||||
PEERTUBE_SECRETS_FILE = 'peertube_secret'
|
||||
PEERTUBE_PRIVACY = {
|
||||
"public": 1,
|
||||
"unlisted": 2,
|
||||
"private": 3
|
||||
}
|
||||
|
||||
|
||||
def get_authenticated_service(secret):
|
||||
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
|
||||
|
||||
oauth_client = LegacyApplicationClient(
|
||||
client_id=str(secret.get('peertube', 'client_id'))
|
||||
)
|
||||
try:
|
||||
oauth = OAuth2Session(client=oauth_client)
|
||||
oauth.fetch_token(
|
||||
token_url=str(peertube_url + '/api/v1/users/token'),
|
||||
# lower as peertube does not store uppercase for pseudo
|
||||
username=str(secret.get('peertube', 'username').lower()),
|
||||
password=str(secret.get('peertube', 'password')),
|
||||
client_id=str(secret.get('peertube', 'client_id')),
|
||||
client_secret=str(secret.get('peertube', 'client_secret'))
|
||||
)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logger.critical("Peertube: " + str(e.message))
|
||||
exit(1)
|
||||
else:
|
||||
logger.critical("Peertube: " + str(e))
|
||||
exit(1)
|
||||
return oauth
|
||||
|
||||
|
||||
def get_default_channel(user_info):
|
||||
return user_info['videoChannels'][0]['id']
|
||||
|
||||
|
||||
def get_channel_by_name(user_info, options):
|
||||
for channel in user_info["videoChannels"]:
|
||||
if channel['displayName'] == options.get('--channel'):
|
||||
return channel['id']
|
||||
|
||||
|
||||
def convert_peertube_date(date):
|
||||
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S')
|
||||
tz = get_localzone()
|
||||
tz = pytz.timezone(str(tz))
|
||||
return tz.localize(date).isoformat()
|
||||
|
||||
|
||||
def create_channel(oauth, url, options):
|
||||
template = ('Peertube: Channel %s does not exist, creating it.')
|
||||
logger.info(template % (str(options.get('--channel'))))
|
||||
channel_name = utils.cleanString(str(options.get('--channel')))
|
||||
# Peertube allows 20 chars max for channel name
|
||||
channel_name = channel_name[:19]
|
||||
data = '{"name":"' + channel_name + '", \
|
||||
"displayName":"' + options.get('--channel') + '", \
|
||||
"description":null, \
|
||||
"support":null}'
|
||||
|
||||
headers = {
|
||||
'Content-Type': "application/json; charset=UTF-8"
|
||||
}
|
||||
try:
|
||||
response = oauth.post(url + "/api/v1/video-channels/",
|
||||
data=data.encode('utf-8'),
|
||||
headers=headers)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logger.error("Peertube: " + str(e.message))
|
||||
else:
|
||||
logger.error("Peertube: " + str(e))
|
||||
if response is not None:
|
||||
if response.status_code == 200:
|
||||
jresponse = response.json()
|
||||
jresponse = jresponse['videoChannel']
|
||||
return jresponse['id']
|
||||
if response.status_code == 409:
|
||||
logger.critical('Peertube: It seems there is a conflict with an existing channel named '
|
||||
+ channel_name + '.'
|
||||
' Please beware Peertube internal name is compiled from 20 firsts characters of channel name.'
|
||||
' Also note that channel name are not case sensitive (no uppercase nor accent)'
|
||||
' Please check your channel name and retry.')
|
||||
exit(1)
|
||||
else:
|
||||
logger.critical(('Peertube: Creating channel failed with an unexpected response: '
|
||||
'%s') % response)
|
||||
exit(1)
|
||||
|
||||
|
||||
def get_default_playlist(user_info):
|
||||
return user_info['videoChannels'][0]['id']
|
||||
|
||||
|
||||
def get_playlist_by_name(oauth, url, username, options):
|
||||
start = 0
|
||||
user_playlists = json.loads(oauth.get(
|
||||
url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content)
|
||||
total = user_playlists["total"]
|
||||
data = user_playlists["data"]
|
||||
# We need to iterate on pagination as peertube returns max 100 playlists (see #41)
|
||||
while start < total:
|
||||
for playlist in data:
|
||||
if playlist['displayName'] == options.get('--playlist'):
|
||||
return playlist['id']
|
||||
start = start + 100
|
||||
user_playlists = json.loads(oauth.get(
|
||||
url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content)
|
||||
data = user_playlists["data"]
|
||||
|
||||
|
||||
def create_playlist(oauth, url, options, channel):
|
||||
template = ('Peertube: Playlist %s does not exist, creating it.')
|
||||
logger.info(template % (str(options.get('--playlist'))))
|
||||
# We use files for form-data Content
|
||||
# see https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file
|
||||
# None is used to mute "filename" field
|
||||
files = {'displayName': (None, str(options.get('--playlist'))),
|
||||
'privacy': (None, "1"),
|
||||
'description': (None, "null"),
|
||||
'videoChannelId': (None, str(channel)),
|
||||
'thumbnailfile': (None, "null")}
|
||||
try:
|
||||
response = oauth.post(url + "/api/v1/video-playlists/",
|
||||
files=files)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logger.error("Peertube: " + str(e.message))
|
||||
else:
|
||||
logger.error("Peertube: " + str(e))
|
||||
if response is not None:
|
||||
if response.status_code == 200:
|
||||
jresponse = response.json()
|
||||
jresponse = jresponse['videoPlaylist']
|
||||
return jresponse['id']
|
||||
else:
|
||||
logger.critical(('Peertube: Creating the playlist failed with an unexpected response: '
|
||||
'%s') % response)
|
||||
exit(1)
|
||||
|
||||
|
||||
def set_playlist(oauth, url, video_id, playlist_id):
|
||||
logger.info('Peertube: add video to playlist.')
|
||||
data = '{"videoId":"' + str(video_id) + '"}'
|
||||
|
||||
headers = {
|
||||
'Content-Type': "application/json"
|
||||
}
|
||||
try:
|
||||
response = oauth.post(url + "/api/v1/video-playlists/"+str(playlist_id)+"/videos",
|
||||
data=data,
|
||||
headers=headers)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logger.error("Peertube: " + str(e.message))
|
||||
else:
|
||||
logger.error("Peertube: " + str(e))
|
||||
if response is not None:
|
||||
if response.status_code == 200:
|
||||
logger.info('Peertube: Video is successfully added to the playlist.')
|
||||
else:
|
||||
logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: '
|
||||
'%s') % response)
|
||||
exit(1)
|
||||
|
||||
|
||||
def upload_video(oauth, secret, options):
|
||||
|
||||
def get_userinfo():
|
||||
return json.loads(oauth.get(url+"/api/v1/users/me").content)
|
||||
|
||||
def get_file(path):
|
||||
mimetypes.init()
|
||||
return (basename(path), open(abspath(path), 'rb'),
|
||||
mimetypes.types_map[splitext(path)[1]])
|
||||
|
||||
path = options.get('--file')
|
||||
url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
|
||||
user_info = get_userinfo()
|
||||
username = str(secret.get('peertube', 'username').lower())
|
||||
|
||||
# We need to transform fields into tuple to deal with tags as
|
||||
# MultipartEncoder does not support list refer
|
||||
# https://github.com/requests/toolbelt/issues/190 and
|
||||
# https://github.com/requests/toolbelt/issues/205
|
||||
fields = [
|
||||
("name", options.get('--name') or splitext(basename(options.get('--file')))[0]),
|
||||
("licence", "1"),
|
||||
("description", options.get('--description') or "default description"),
|
||||
("nsfw", str(int(options.get('--nsfw')) or "0")),
|
||||
("videofile", get_file(path))
|
||||
]
|
||||
|
||||
if options.get('--tags'):
|
||||
tags = options.get('--tags').split(',')
|
||||
tag_number = 0
|
||||
for strtag in tags:
|
||||
tag_number = tag_number + 1
|
||||
# Empty tag crashes Peertube, so skip them
|
||||
if strtag == "":
|
||||
continue
|
||||
# Tag more than 30 chars crashes Peertube, so skip tags
|
||||
if len(strtag) >= 30:
|
||||
logger.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag)
|
||||
logger.warning("Peertube: Meanwhile, this tag will be skipped")
|
||||
continue
|
||||
# Peertube supports only 5 tags at the moment
|
||||
if tag_number > 5:
|
||||
logger.warning("Peertube: Sorry, Peertube support 5 tags max, additional tag will be skipped")
|
||||
logger.warning("Peertube: Skipping tag " + strtag)
|
||||
continue
|
||||
fields.append(("tags[]", strtag))
|
||||
|
||||
if options.get('--category'):
|
||||
fields.append(("category", str(utils.getCategory(options.get('--category'), 'peertube'))))
|
||||
else:
|
||||
# if no category, set default to 2 (Films)
|
||||
fields.append(("category", "2"))
|
||||
|
||||
if options.get('--language'):
|
||||
fields.append(("language", str(utils.getLanguage(options.get('--language'), "peertube"))))
|
||||
else:
|
||||
# if no language, set default to 1 (English)
|
||||
fields.append(("language", "en"))
|
||||
|
||||
if options.get('--disable-comments'):
|
||||
fields.append(("commentsEnabled", "0"))
|
||||
else:
|
||||
fields.append(("commentsEnabled", "1"))
|
||||
|
||||
privacy = None
|
||||
if options.get('--privacy'):
|
||||
privacy = options.get('--privacy').lower()
|
||||
|
||||
# If peertubeAt exists, use instead of publishAt
|
||||
if options.get('--peertubeAt'):
|
||||
publishAt = options.get('--peertubeAt')
|
||||
elif options.get('--publishAt'):
|
||||
publishAt = options.get('--publishAt')
|
||||
|
||||
if 'publishAt' in locals():
|
||||
publishAt = convert_peertube_date(publishAt)
|
||||
fields.append(("scheduleUpdate[updateAt]", publishAt))
|
||||
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
|
||||
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
|
||||
else:
|
||||
fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"])))
|
||||
|
||||
# Set originalDate except if the user force no originalDate
|
||||
if options.get('--originalDate'):
|
||||
originalDate = convert_peertube_date(options.get('--originalDate'))
|
||||
fields.append(("originallyPublishedAt", originalDate))
|
||||
|
||||
if options.get('--thumbnail'):
|
||||
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
|
||||
fields.append(("previewfile", get_file(options.get('--thumbnail'))))
|
||||
|
||||
if options.get('--channel'):
|
||||
channel_id = get_channel_by_name(user_info, options)
|
||||
if not channel_id and options.get('--channelCreate'):
|
||||
channel_id = create_channel(oauth, url, options)
|
||||
elif not channel_id:
|
||||
logger.warning("Peertube: Channel `" + options.get('--channel') + "` is unknown, using default channel.")
|
||||
channel_id = get_default_channel(user_info)
|
||||
else:
|
||||
channel_id = get_default_channel(user_info)
|
||||
|
||||
fields.append(("channelId", str(channel_id)))
|
||||
|
||||
if options.get('--playlist'):
|
||||
playlist_id = get_playlist_by_name(oauth, url, username, options)
|
||||
if not playlist_id and options.get('--playlistCreate'):
|
||||
playlist_id = create_playlist(oauth, url, options, channel_id)
|
||||
elif not playlist_id:
|
||||
logger.critical("Peertube: Playlist `" + options.get('--playlist') + "` does not exist, please set --playlistCreate"
|
||||
" if you want to create it")
|
||||
exit(1)
|
||||
|
||||
logger_stdout = None
|
||||
if options.get('--url-only') or options.get('--batch'):
|
||||
logger_stdout = logging.getLogger('stdoutlogs')
|
||||
|
||||
encoder = MultipartEncoder(fields)
|
||||
if options.get('--quiet'):
|
||||
multipart_data = encoder
|
||||
else:
|
||||
progress_callback = create_callback(encoder, options.get('--progress'))
|
||||
multipart_data = MultipartEncoderMonitor(encoder, progress_callback)
|
||||
|
||||
headers = {
|
||||
'Content-Type': multipart_data.content_type
|
||||
}
|
||||
response = oauth.post(url + "/api/v1/videos/upload",
|
||||
data=multipart_data,
|
||||
headers=headers)
|
||||
|
||||
if response is not None:
|
||||
if response.status_code == 200:
|
||||
jresponse = response.json()
|
||||
jresponse = jresponse['video']
|
||||
uuid = jresponse['uuid']
|
||||
video_id = str(jresponse['id'])
|
||||
|
||||
logger.info('Peertube: Video was successfully uploaded.')
|
||||
template = 'Peertube: Watch it at %s/videos/watch/%s.'
|
||||
logger.info(template % (url, uuid))
|
||||
template_stdout = '%s/videos/watch/%s'
|
||||
if options.get('--url-only'):
|
||||
logger_stdout.info(template_stdout % (url, uuid))
|
||||
elif options.get('--batch'):
|
||||
logger_stdout.info("Peertube: " + template_stdout % (url, uuid))
|
||||
# Upload is successful we may set playlist
|
||||
if options.get('--playlist'):
|
||||
set_playlist(oauth, url, video_id, playlist_id)
|
||||
else:
|
||||
logger.critical(('Peertube: The upload failed with an unexpected response: '
|
||||
'%s') % response)
|
||||
exit(1)
|
||||
|
||||
|
||||
upload_finished = False
|
||||
def create_callback(encoder, progress_type):
|
||||
upload_size_MB = encoder.len * (1 / (1024 * 1024))
|
||||
|
||||
if progress_type is None or "percentage" in progress_type.lower():
|
||||
progress_lambda = lambda x: int((x / encoder.len) * 100) # Default to percentage
|
||||
elif "bigfile" in progress_type.lower():
|
||||
progress_lambda = lambda x: x * (1 / (1024 * 1024)) # MB
|
||||
elif "accurate" in progress_type.lower():
|
||||
progress_lambda = lambda x: x * (1 / (1024)) # kB
|
||||
else:
|
||||
# Should not happen outside of development when adding partly a progress type
|
||||
logger.critical("Peertube: Unknown progress type `" + progress_type + "`")
|
||||
exit(1)
|
||||
|
||||
bar = ProgressBar(expected_size=progress_lambda(encoder.len), label=f"Peertube upload progress ({upload_size_MB:.2f}MB) ", filled_char='=')
|
||||
|
||||
def callback(monitor):
|
||||
# We want the condition to capture the varible from the parent scope, not a local variable that is created after
|
||||
global upload_finished
|
||||
progress = progress_lambda(monitor.bytes_read)
|
||||
|
||||
bar.show(progress)
|
||||
|
||||
if monitor.bytes_read == encoder.len:
|
||||
if not upload_finished:
|
||||
# We get two time in the callback with both bytes equals, skip the first
|
||||
upload_finished = True
|
||||
else:
|
||||
# Print a blank line to not (partly) override the progress bar
|
||||
print()
|
||||
logger.info("Peertube: Upload finish, Processing…")
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def run(options):
|
||||
secret = RawConfigParser()
|
||||
try:
|
||||
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)
|
||||
insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT')
|
||||
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport
|
||||
oauth = get_authenticated_service(secret)
|
||||
try:
|
||||
logger.info('Peertube: Uploading video...')
|
||||
upload_video(oauth, secret, options)
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logger.error("Peertube: " + str(e.message))
|
||||
else:
|
||||
logger.error("Peertube: " + str(e))
|
12
prismedia/samples/cli_nfo.txt
Normal file
12
prismedia/samples/cli_nfo.txt
Normal file
|
@ -0,0 +1,12 @@
|
|||
### This NFO is aimed to be passed to prismedia through the --nfo cli option ###
|
||||
### eg:
|
||||
### python -m prismedia --file=/path/to/yourvideo.mp4 --nfo=/path/to/cli_nfo.txt ###
|
||||
### It's the more priority NFO, only erased by direct cli options ###
|
||||
[video]
|
||||
disable-comments = False
|
||||
nsfw = True
|
||||
# Publish on Peertube at a specific date
|
||||
peertubeAt = 2034-05-14T19:00:00
|
||||
platform = peertube
|
||||
# debug to display all loaded options
|
||||
debug = True
|
26
prismedia/samples/full_nfo_examples.txt
Normal file
26
prismedia/samples/full_nfo_examples.txt
Normal file
|
@ -0,0 +1,26 @@
|
|||
### This NFO example show how to construct a NFO for your video ###
|
||||
### All fields are optionals, but you need at least one field (otherwise NFO is useless :-p) ###
|
||||
### See --help for options explanation
|
||||
[video]
|
||||
name = videoname
|
||||
description = Your complete video description
|
||||
Multilines description
|
||||
should be wrote with a blank space
|
||||
at the beginning of the line :-)
|
||||
tags = list of tags, comma separated
|
||||
category = Films
|
||||
cca = True
|
||||
privacy = private
|
||||
disable-comments = True
|
||||
#thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
|
||||
channel = CookingTest
|
||||
channelCreate = True
|
||||
playlist = Desserts Recipes playlist
|
||||
playlistCreate = True
|
||||
nsfw = False
|
||||
platform = youtube, peertube
|
||||
language = French
|
||||
publishAt = 2034-05-07T19:00:00
|
||||
# platformAt overrides the default publishAt for the corresponding platform
|
||||
#peertubeAt = 2034-05-14T19:00:00
|
||||
#youtubeAt = 2034-05-21T19:00:00
|
10
prismedia/samples/nfo.txt
Normal file
10
prismedia/samples/nfo.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
### This NFO is named nfo.txt and is stored in the directory of your videos ###
|
||||
### This is the less priority NFO, you may use it to set default generic options ###
|
||||
[video]
|
||||
# Some generic options for your videos
|
||||
cca = True
|
||||
privacy = private
|
||||
disable-comments = False
|
||||
channel = DefaultChannel
|
||||
channelCreate = True
|
||||
auto-originalDate = True
|
14
prismedia/samples/samples.txt
Normal file
14
prismedia/samples/samples.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
### This NFO is named from the directory where your video are. ###
|
||||
### While more specific than nfo.txt, it's less priority than other NFO ###
|
||||
### You may use it for options specific to videos in this directory, but still globals ###
|
||||
[video]
|
||||
channel = MyMoreSpecificChannel
|
||||
disable-comments = False
|
||||
channelCreate = True
|
||||
category = Films
|
||||
playlist = Desserts Recipes playlist
|
||||
playlistCreate = True
|
||||
nsfw = False
|
||||
platform = youtube, peertube
|
||||
language = French
|
||||
tags = list of tags, comma separated
|
14
prismedia/samples/yourvideo.txt
Normal file
14
prismedia/samples/yourvideo.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
### This NFO is named from your video name (here let's say your video is named "yourvideo.mp4") ###
|
||||
### It aims to give options specific to this videos ###
|
||||
[video]
|
||||
disable-comments = False
|
||||
#thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
|
||||
name = videoname
|
||||
description = Your complete video description
|
||||
Multilines description
|
||||
should be wrote with a blank space
|
||||
at the beginning of the line :-)
|
||||
publishAt = 2034-05-07T19:00:00
|
||||
# platformAt overrides the default publishAt for the corresponding platform
|
||||
#peertubeAt = 2034-05-14T19:00:00
|
||||
#youtubeAt = 2034-05-21T19:00:00
|
424
prismedia/upload.py
Executable file
424
prismedia/upload.py
Executable file
|
@ -0,0 +1,424 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
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. 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
|
||||
--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)
|
||||
--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
|
||||
--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.
|
||||
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)
|
||||
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.
|
||||
--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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
--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
|
||||
--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
|
||||
|
||||
"""
|
||||
import sys
|
||||
if sys.version_info[0] < 3:
|
||||
raise Exception("Python 3 or a more recent version is required.")
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import logging
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
||||
from docopt import docopt
|
||||
|
||||
from . import yt_upload
|
||||
from . import pt_upload
|
||||
from . import utils
|
||||
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from schema import Schema, And, Or, Optional, SchemaError, Hook, Use
|
||||
except ImportError:
|
||||
logger.critical('This program requires that the `schema` data-validation library'
|
||||
' is installed: \n'
|
||||
'see https://github.com/halst/schema\n')
|
||||
exit(1)
|
||||
|
||||
VERSION = "prismedia v0.12.2"
|
||||
|
||||
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
|
||||
VALID_CATEGORIES = (
|
||||
"music", "films", "vehicles",
|
||||
"sports", "travels", "gaming", "people",
|
||||
"comedy", "entertainment", "news",
|
||||
"how to", "education", "activism", "science & technology",
|
||||
"science", "technology", "animals"
|
||||
)
|
||||
VALID_PLATFORM = ('youtube', 'peertube', 'none')
|
||||
VALID_LANGUAGES = ('arabic', 'english', 'french',
|
||||
'german', 'hindi', 'italian',
|
||||
'japanese', 'korean', 'mandarin',
|
||||
'portuguese', 'punjabi', 'russian', 'spanish')
|
||||
VALID_PROGRESS = ('percentage', 'bigfile', 'accurate')
|
||||
|
||||
|
||||
def validateCategory(category):
|
||||
if category.lower() in VALID_CATEGORIES:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validatePrivacy(privacy):
|
||||
if privacy.lower() in VALID_PRIVACY_STATUSES:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validatePlatform(platform):
|
||||
for plfrm in platform.split(','):
|
||||
if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validateLanguage(language):
|
||||
if language.lower() in VALID_LANGUAGES:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validatePublishDate(publishDate):
|
||||
# Check date format and if date is future
|
||||
try:
|
||||
now = datetime.datetime.now()
|
||||
publishAt = datetime.datetime.strptime(publishDate, '%Y-%m-%dT%H:%M:%S')
|
||||
if now >= publishAt:
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validateOriginalDate(originalDate):
|
||||
# Check date format and if date is past
|
||||
try:
|
||||
now = datetime.datetime.now()
|
||||
originalDate = datetime.datetime.strptime(originalDate, '%Y-%m-%dT%H:%M:%S')
|
||||
if now <= originalDate:
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validateLogLevel(loglevel):
|
||||
numeric_level = getattr(logging, loglevel, None)
|
||||
if not isinstance(numeric_level, int):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validateProgress(progress):
|
||||
for prgs in progress.split(','):
|
||||
if prgs.lower().replace(" ", "") not in VALID_PROGRESS:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _optionnalOrStrict(key, scope, error):
|
||||
option = key.replace('-', '')
|
||||
option = option[0].upper() + option[1:]
|
||||
if scope["--with" + option] is True and scope[key] is None:
|
||||
logger.critical("Prismedia: you have required the strict presence of " + key + " but none is found")
|
||||
exit(1)
|
||||
return True
|
||||
|
||||
|
||||
def configureLogs(options):
|
||||
if options.get('--batch') and options.get('--url-only'):
|
||||
logger.critical("Prismedia: Please use either --batch OR --url-only, not both.")
|
||||
exit(1)
|
||||
# batch and url-only implies quiet
|
||||
if options.get('--batch') or options.get('--url-only'):
|
||||
options['--quiet'] = True
|
||||
|
||||
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():
|
||||
logger_stdout = logging.getLogger('stdoutlogs')
|
||||
logger_stdout.setLevel(logging.INFO)
|
||||
ch_stdout = logging.StreamHandler(stream=sys.stdout)
|
||||
ch_stdout.setLevel(logging.INFO)
|
||||
# Default stdout logs is url only
|
||||
formatter_stdout = logging.Formatter('%(message)s')
|
||||
ch_stdout.setFormatter(formatter_stdout)
|
||||
logger_stdout.addHandler(ch_stdout)
|
||||
|
||||
def main():
|
||||
options = docopt(__doc__, version=VERSION)
|
||||
|
||||
earlyoptionSchema = Schema({
|
||||
Optional('--log'): Or(None, And(
|
||||
str,
|
||||
Use(str.upper),
|
||||
validateLogLevel,
|
||||
error="Log level not recognized")
|
||||
),
|
||||
Optional('--quiet', default=False): bool,
|
||||
Optional('--debug'): bool,
|
||||
Optional('--url-only', default=False): bool,
|
||||
Optional('--batch', default=False): bool,
|
||||
Optional('--withNFO', default=False): bool,
|
||||
Optional('--withThumbnail', default=False): bool,
|
||||
Optional('--withName', default=False): bool,
|
||||
Optional('--withDescription', default=False): bool,
|
||||
Optional('--withTags', default=False): bool,
|
||||
Optional('--withPlaylist', default=False): bool,
|
||||
Optional('--withPublishAt', default=False): bool,
|
||||
Optional('--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, 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,
|
||||
Hook('--tags', handler=_optionnalOrStrict): object,
|
||||
Hook('--category', handler=_optionnalOrStrict): object,
|
||||
Hook('--language', handler=_optionnalOrStrict): object,
|
||||
Hook('--platform', handler=_optionnalOrStrict): object,
|
||||
Hook('--publishAt', handler=_optionnalOrStrict): object,
|
||||
Hook('--originalDate', handler=_optionnalOrStrict): object,
|
||||
Hook('--thumbnail', handler=_optionnalOrStrict): object,
|
||||
Hook('--channel', handler=_optionnalOrStrict): object,
|
||||
Hook('--playlist', handler=_optionnalOrStrict): object,
|
||||
# Validate checks #
|
||||
Optional('--name'): Or(None, And(
|
||||
str,
|
||||
lambda x: not x.isdigit(),
|
||||
error="The video name should be a string")
|
||||
),
|
||||
Optional('--description'): Or(None, And(
|
||||
str,
|
||||
lambda x: not x.isdigit(),
|
||||
error="The video description should be a string")
|
||||
),
|
||||
Optional('--tags'): Or(None, And(
|
||||
str,
|
||||
lambda x: not x.isdigit(),
|
||||
error="Tags should be a string")
|
||||
),
|
||||
Optional('--category'): Or(None, And(
|
||||
str,
|
||||
validateCategory,
|
||||
error="Category not recognized, please see --help")
|
||||
),
|
||||
Optional('--language'): Or(None, And(
|
||||
str,
|
||||
validateLanguage,
|
||||
error="Language not recognized, please see --help")
|
||||
),
|
||||
Optional('--privacy'): Or(None, And(
|
||||
str,
|
||||
validatePrivacy,
|
||||
error="Please use recognized privacy between public, unlisted or private")
|
||||
),
|
||||
Optional('--nfo'): Or(None, str),
|
||||
Optional('--platform'): Or(None, And(str, validatePlatform, error="Sorry, upload platform not supported")),
|
||||
Optional('--publishAt'): Or(None, And(
|
||||
str,
|
||||
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,
|
||||
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,
|
||||
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, 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)
|
||||
configureLogs(options)
|
||||
except SchemaError as e:
|
||||
logger.critical(e)
|
||||
exit(1)
|
||||
|
||||
if options.get('--url-only') or options.get('--batch'):
|
||||
configureStdoutLogs()
|
||||
|
||||
options = utils.parseNFO(options)
|
||||
|
||||
# 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)
|
||||
except SchemaError as e:
|
||||
logger.critical(e)
|
||||
exit(1)
|
||||
|
||||
if not options.get('--thumbnail'):
|
||||
options = utils.searchThumbnail(options)
|
||||
|
||||
try:
|
||||
options = schema.validate(options)
|
||||
except SchemaError as e:
|
||||
logger.critical(e)
|
||||
exit(1)
|
||||
|
||||
logger.debug("Python " + sys.version)
|
||||
logger.debug(options)
|
||||
|
||||
if options.get('--platform') is None or "peertube" in options.get('--platform'):
|
||||
pt_upload.run(options)
|
||||
if options.get('--platform') is None or "youtube" in options.get('--platform'):
|
||||
yt_upload.run(options)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.warning("DEPRECATION: use 'python -m prismedia', not 'python -m prismedia.upload'")
|
||||
main()
|
240
prismedia/utils.py
Normal file
240
prismedia/utils.py
Normal file
|
@ -0,0 +1,240 @@
|
|||
#!/usr/bin/python
|
||||
# coding: utf-8
|
||||
|
||||
from configparser import RawConfigParser, NoOptionError, NoSectionError
|
||||
from os.path import dirname, splitext, basename, isfile, getmtime
|
||||
import re
|
||||
import unidecode
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
||||
### CATEGORIES ###
|
||||
YOUTUBE_CATEGORY = {
|
||||
"music": 10,
|
||||
"films": 1,
|
||||
"vehicles": 2,
|
||||
"sport": 17,
|
||||
"travels": 19,
|
||||
"gaming": 20,
|
||||
"people": 22,
|
||||
"comedy": 23,
|
||||
"entertainment": 24,
|
||||
"news": 25,
|
||||
"how to": 26,
|
||||
"education": 27,
|
||||
"activism": 29,
|
||||
"science & technology": 28,
|
||||
"science": 28,
|
||||
"technology": 28,
|
||||
"animals": 15
|
||||
}
|
||||
|
||||
PEERTUBE_CATEGORY = {
|
||||
"music": 1,
|
||||
"films": 2,
|
||||
"vehicles": 3,
|
||||
"sport": 5,
|
||||
"travels": 6,
|
||||
"gaming": 7,
|
||||
"people": 8,
|
||||
"comedy": 9,
|
||||
"entertainment": 10,
|
||||
"news": 11,
|
||||
"how to": 12,
|
||||
"education": 13,
|
||||
"activism": 14,
|
||||
"science & technology": 15,
|
||||
"science": 15,
|
||||
"technology": 15,
|
||||
"animals": 16
|
||||
}
|
||||
|
||||
### LANGUAGES ###
|
||||
YOUTUBE_LANGUAGE = {
|
||||
"arabic": 'ar',
|
||||
"english": 'en',
|
||||
"french": 'fr',
|
||||
"german": 'de',
|
||||
"hindi": 'hi',
|
||||
"italian": 'it',
|
||||
"japanese": 'ja',
|
||||
"korean": 'ko',
|
||||
"mandarin": 'zh-CN',
|
||||
"portuguese": 'pt-PT',
|
||||
"punjabi": 'pa',
|
||||
"russian": 'ru',
|
||||
"spanish": 'es'
|
||||
}
|
||||
|
||||
PEERTUBE_LANGUAGE = {
|
||||
"arabic": "ar",
|
||||
"english": "en",
|
||||
"french": "fr",
|
||||
"german": "de",
|
||||
"hindi": "hi",
|
||||
"italian": "it",
|
||||
"japanese": "ja",
|
||||
"korean": "ko",
|
||||
"mandarin": "zh",
|
||||
"portuguese": "pt",
|
||||
"punjabi": "pa",
|
||||
"russian": "ru",
|
||||
"spanish": "es"
|
||||
}
|
||||
######################
|
||||
|
||||
|
||||
def getCategory(category, platform):
|
||||
if platform == "youtube":
|
||||
return YOUTUBE_CATEGORY[category.lower()]
|
||||
else:
|
||||
return PEERTUBE_CATEGORY[category.lower()]
|
||||
|
||||
|
||||
def getLanguage(language, platform):
|
||||
if platform == "youtube":
|
||||
return YOUTUBE_LANGUAGE[language.lower()]
|
||||
else:
|
||||
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:
|
||||
for key, value in kwargs.items():
|
||||
if value:
|
||||
good_kwargs[key] = value
|
||||
return good_kwargs
|
||||
|
||||
|
||||
def searchThumbnail(options):
|
||||
video_directory = dirname(options.get('--file')) + "/"
|
||||
# First, check for thumbnail based on videoname
|
||||
if options.get('--name'):
|
||||
if isfile(video_directory + options.get('--name') + ".jpg"):
|
||||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
|
||||
elif isfile(video_directory + options.get('--name') + ".jpeg"):
|
||||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
|
||||
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]
|
||||
if isfile(video_directory + video_file + ".jpg"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".jpg"
|
||||
elif isfile(video_directory + video_file + ".jpeg"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".jpeg"
|
||||
elif isfile(video_directory + video_file + ".png"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".png"
|
||||
|
||||
# Display some info after research
|
||||
if not options.get('--thumbnail'):
|
||||
logger.debug("No thumbnail has been found, continuing")
|
||||
else:
|
||||
logger.info("Using " + options.get('--thumbnail') + " as thumbnail")
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def searchOriginalDate(options):
|
||||
fileModificationDate = str(getmtime(options.get('--file'))).split('.')
|
||||
return datetime.datetime.fromtimestamp(int(fileModificationDate[0])).isoformat()
|
||||
|
||||
|
||||
# return the nfo as a RawConfigParser object
|
||||
def loadNFO(filename):
|
||||
try:
|
||||
logger.info("Loading " + filename + " as NFO")
|
||||
nfo = RawConfigParser()
|
||||
nfo.read(filename, encoding='utf-8')
|
||||
return nfo
|
||||
except Exception as e:
|
||||
logger.critical("Problem loading NFO file " + filename + ": " + str(e))
|
||||
exit(1)
|
||||
return False
|
||||
|
||||
|
||||
def parseNFO(options):
|
||||
video_directory = dirname(options.get('--file'))
|
||||
directory_name = basename(video_directory)
|
||||
nfo_txt = False
|
||||
nfo_directory = False
|
||||
nfo_videoname = False
|
||||
nfo_file = False
|
||||
nfo_cli = False
|
||||
|
||||
if isfile(video_directory + "/" + "nfo.txt"):
|
||||
nfo_txt = loadNFO(video_directory + "/" + "nfo.txt")
|
||||
elif isfile(video_directory + "/" + "NFO.txt"):
|
||||
nfo_txt = loadNFO(video_directory + "/" + "NFO.txt")
|
||||
|
||||
if isfile(video_directory + "/" + directory_name + ".txt"):
|
||||
nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt")
|
||||
|
||||
if options.get('--name'):
|
||||
if isfile(video_directory + "/" + options.get('--name')):
|
||||
nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt")
|
||||
|
||||
video_file = splitext(basename(options.get('--file')))[0]
|
||||
if isfile(video_directory + "/" + video_file + ".txt"):
|
||||
nfo_file = loadNFO(video_directory + "/" + video_file + ".txt")
|
||||
|
||||
if options.get('--nfo'):
|
||||
if isfile(options.get('--nfo')):
|
||||
nfo_cli = loadNFO(options.get('--nfo'))
|
||||
else:
|
||||
logger.critical("Given NFO file does not exist, please check your path.")
|
||||
exit(1)
|
||||
|
||||
# If there is no NFO and strict option is enabled, then stop there
|
||||
if options.get('--withNFO'):
|
||||
if not isinstance(nfo_cli, RawConfigParser) and \
|
||||
not isinstance(nfo_file, RawConfigParser) and \
|
||||
not isinstance(nfo_videoname, RawConfigParser) and \
|
||||
not isinstance(nfo_directory, RawConfigParser) and \
|
||||
not isinstance(nfo_txt, RawConfigParser):
|
||||
logger.critical("You have required the strict presence of NFO but none is found, please use a NFO.")
|
||||
exit(1)
|
||||
|
||||
# We need to load NFO in this exact order to keep the priorities
|
||||
# options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt
|
||||
for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]:
|
||||
if nfo:
|
||||
# We need to check all options and replace it with the nfo value if not defined (None or False)
|
||||
for key, value in options.items():
|
||||
key = key.replace("--", "")
|
||||
try:
|
||||
# get string options
|
||||
if value is None and nfo.get('video', key):
|
||||
options['--' + key] = nfo.get('video', key)
|
||||
# get boolean options
|
||||
elif value is False and nfo.getboolean('video', key):
|
||||
options['--' + key] = nfo.getboolean('video', key)
|
||||
except NoOptionError:
|
||||
continue
|
||||
except NoSectionError:
|
||||
logger.critical(nfo + " misses section [video], please check syntax of your NFO.")
|
||||
exit(1)
|
||||
return options
|
||||
|
||||
|
||||
def upcaseFirstLetter(s):
|
||||
return s[0].upper() + s[1:]
|
||||
|
||||
|
||||
def cleanString(toclean):
|
||||
toclean = unidecode.unidecode(toclean)
|
||||
cleaned = re.sub('[^A-Za-z0-9]+', '', toclean)
|
||||
|
||||
return cleaned
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python2
|
||||
#!/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 httplib
|
||||
import http.client
|
||||
import httplib2
|
||||
import random
|
||||
import time
|
||||
|
@ -22,10 +22,8 @@ from googleapiclient.http import MediaFileUpload
|
|||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
|
||||
|
||||
import utils
|
||||
|
||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
|
||||
|
||||
from . import utils
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
||||
# Explicitly tell the underlying HTTP transport library not to retry, since
|
||||
# we are handling retry logic ourselves.
|
||||
|
@ -38,20 +36,20 @@ MAX_RETRIES = 10
|
|||
RETRIABLE_EXCEPTIONS = (
|
||||
IOError,
|
||||
httplib2.HttpLib2Error,
|
||||
httplib.NotConnected,
|
||||
httplib.IncompleteRead,
|
||||
httplib.ImproperConnectionState,
|
||||
httplib.CannotSendRequest,
|
||||
httplib.CannotSendHeader,
|
||||
httplib.ResponseNotReady,
|
||||
httplib.BadStatusLine,
|
||||
http.client.NotConnected,
|
||||
http.client.IncompleteRead,
|
||||
http.client.ImproperConnectionState,
|
||||
http.client.CannotSendRequest,
|
||||
http.client.CannotSendHeader,
|
||||
http.client.ResponseNotReady,
|
||||
http.client.BadStatusLine,
|
||||
)
|
||||
|
||||
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'
|
||||
|
@ -62,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)
|
||||
|
@ -78,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():
|
||||
|
@ -87,10 +86,19 @@ def check_authenticated_scopes():
|
|||
credential_params = json.load(f)
|
||||
# Check if all scopes are present
|
||||
if credential_params["_scopes"] != SCOPES:
|
||||
logging.warning("Youtube: Credentials are obsolete, need to re-authenticate.")
|
||||
logger.warning("Youtube: Credentials are obsolete, need to re-authenticate.")
|
||||
os.remove(CREDENTIALS_PATH)
|
||||
|
||||
|
||||
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
|
||||
|
@ -109,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],
|
||||
|
@ -121,60 +131,80 @@ def initialize_upload(youtube, options):
|
|||
"status": {
|
||||
"privacyStatus": str(options.get('--privacy') or "private"),
|
||||
"license": str(license or "youtube"),
|
||||
},
|
||||
"recordingDetails": {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if options.get('--publishAt'):
|
||||
# Youtube needs microsecond and the local timezone from ISO 8601
|
||||
publishAt = options.get('--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()
|
||||
# If peertubeAt exists, use instead of publishAt
|
||||
if options.get('--youtubeAt'):
|
||||
publishAt = options.get('--youtubeAt')
|
||||
elif options.get('--publishAt'):
|
||||
publishAt = options.get('--publishAt')
|
||||
|
||||
# Check if publishAt variable exists in local variables
|
||||
if 'publishAt' in locals():
|
||||
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'):
|
||||
playlist_id = create_playlist(youtube, options.get('--playlist'))
|
||||
elif not playlist_id:
|
||||
logging.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.")
|
||||
logging.warning("If you want to create it, set the --playlistCreate option.")
|
||||
logger.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.")
|
||||
logger.warning("Youtube: If you want to create it, set the --playlistCreate option.")
|
||||
playlist_id = ""
|
||||
else:
|
||||
playlist_id = ""
|
||||
|
||||
# Call the API's videos.insert method to create and upload the video.
|
||||
insert_request = youtube.videos().insert(
|
||||
part=','.join(body.keys()),
|
||||
part=','.join(list(body.keys())),
|
||||
body=body,
|
||||
media_body=MediaFileUpload(path, chunksize=-1, resumable=True)
|
||||
)
|
||||
video_id = resumable_upload(insert_request, 'video', 'insert')
|
||||
video_id = resumable_upload(insert_request, 'video', 'insert', options)
|
||||
|
||||
# If we get a video_id, upload is successful and we are able to set thumbnail
|
||||
if video_id and options.get('--thumbnail'):
|
||||
set_thumbnail(youtube, options.get('--thumbnail'), videoId=video_id)
|
||||
set_thumbnail(options, youtube, options.get('--thumbnail'), videoId=video_id)
|
||||
|
||||
# If we get a video_id, upload is successful and we are able to set playlist
|
||||
if video_id and options.get('--playlist'):
|
||||
# If we get a video_id and a playlist_id, upload is successful and we are able to set playlist
|
||||
if video_id and playlist_id != "":
|
||||
set_playlist(youtube, playlist_id, video_id)
|
||||
|
||||
|
||||
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):
|
||||
template = ('Youtube: Playlist %s does not exist, creating it.')
|
||||
logging.info(template % (str(playlist_name)))
|
||||
template = 'Youtube: Playlist %s does not exist, creating it.'
|
||||
logger.info(template % (str(playlist_name)))
|
||||
resources = build_resource({'snippet.title': playlist_name,
|
||||
'snippet.description': '',
|
||||
'status.privacyStatus': 'public'})
|
||||
|
@ -225,7 +255,7 @@ def build_resource(properties):
|
|||
return resource
|
||||
|
||||
|
||||
def set_thumbnail(youtube, media_file, **kwargs):
|
||||
def set_thumbnail(options, youtube, media_file, **kwargs):
|
||||
kwargs = utils.remove_empty_kwargs(**kwargs)
|
||||
request = youtube.thumbnails().set(
|
||||
media_body=MediaFileUpload(media_file, chunksize=-1,
|
||||
|
@ -233,12 +263,11 @@ def set_thumbnail(youtube, media_file, **kwargs):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
# See full sample for function
|
||||
return resumable_upload(request, 'thumbnail', 'set')
|
||||
return resumable_upload(request, 'thumbnail', 'set', options)
|
||||
|
||||
|
||||
def set_playlist(youtube, playlist_id, video_id):
|
||||
logging.info('Youtube: Configuring playlist...')
|
||||
logger.info('Youtube: Configuring playlist...')
|
||||
resource = build_resource({'snippet.playlistId': playlist_id,
|
||||
'snippet.resourceId.kind': 'youtube#video',
|
||||
'snippet.resourceId.videoId': video_id,
|
||||
|
@ -251,63 +280,94 @@ def set_playlist(youtube, playlist_id, video_id):
|
|||
).execute()
|
||||
except Exception as e:
|
||||
if hasattr(e, 'message'):
|
||||
logging.error("Youtube: Error: " + str(e.message))
|
||||
logger.critical("Youtube: " + str(e.message))
|
||||
exit(1)
|
||||
else:
|
||||
logging.error("Youtube: Error: " + str(e))
|
||||
logging.info('Youtube: Video is correclty added to the playlist.')
|
||||
logger.critical("Youtube: " + str(e))
|
||||
exit(1)
|
||||
logger.info('Youtube: Video is correctly added to the playlist.')
|
||||
|
||||
|
||||
# This method implements an exponential backoff strategy to resume a
|
||||
# failed upload.
|
||||
def resumable_upload(request, resource, method):
|
||||
def resumable_upload(request, resource, method, options):
|
||||
response = None
|
||||
error = None
|
||||
retry = 0
|
||||
logger_stdout = None
|
||||
if options.get('--url-only') or options.get('--batch'):
|
||||
logger_stdout = logging.getLogger('stdoutlogs')
|
||||
while response is None:
|
||||
try:
|
||||
template = 'Youtube: Uploading %s...'
|
||||
logging.info(template % resource)
|
||||
logger.info(template % resource)
|
||||
status, response = request.next_chunk()
|
||||
if response is not None:
|
||||
if method == 'insert' and 'id' in response:
|
||||
logging.info('Youtube : Video was successfully uploaded.')
|
||||
logger.info('Youtube: Video was successfully uploaded.')
|
||||
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
|
||||
logging.info(template % response['id'])
|
||||
logger.info(template % response['id'])
|
||||
template_stdout = 'https://youtu.be/%s'
|
||||
if options.get('--url-only'):
|
||||
logger_stdout.info(template_stdout % response['id'])
|
||||
elif options.get('--batch'):
|
||||
logger_stdout.info("Youtube: " + template_stdout % response['id'])
|
||||
return response['id']
|
||||
elif method != 'insert' or "id" not in response:
|
||||
logging.info('Youtube: Thumbnail was successfully set.')
|
||||
logger.info('Youtube: Thumbnail was successfully set.')
|
||||
else:
|
||||
template = ('Youtube : The upload failed with an '
|
||||
template = ('Youtube: The upload failed with an '
|
||||
'unexpected response: %s')
|
||||
logging.error(template % response)
|
||||
logger.critical(template % response)
|
||||
exit(1)
|
||||
except HttpError as e:
|
||||
if e.resp.status in RETRIABLE_STATUS_CODES:
|
||||
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:
|
||||
logging.warning(error)
|
||||
logger.warning(error)
|
||||
retry += 1
|
||||
if retry > MAX_RETRIES:
|
||||
logging.error('Youtube : No longer attempting to retry.')
|
||||
exit(1)
|
||||
logger.error('Youtube: No longer attempting to retry.')
|
||||
|
||||
max_sleep = 2 ** retry
|
||||
sleep_seconds = random.random() * max_sleep
|
||||
logging.warning('Youtube : Sleeping %f seconds and then retrying...'
|
||||
logger.warning('Youtube: Sleeping %f seconds and then retrying...'
|
||||
% sleep_seconds)
|
||||
time.sleep(sleep_seconds)
|
||||
|
||||
|
||||
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:
|
||||
logging.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status,
|
||||
logger.error('Youtube: An HTTP error %d occurred:\n%s' % (e.resp.status,
|
||||
e.content))
|
|
@ -1,238 +0,0 @@
|
|||
#!/usr/bin/env python2
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
prismedia_upload - tool to upload videos to Peertube and Youtube
|
||||
|
||||
Usage:
|
||||
prismedia_upload.py --file=<FILE> [options]
|
||||
prismedia_upload.py -f <FILE> --tags=STRING [--mt options]
|
||||
prismedia_upload.py -h | --help
|
||||
prismedia_upload.py --version
|
||||
|
||||
Options:
|
||||
-f, --file=STRING Path to the video file to upload in mp4
|
||||
--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 space and special characters (!, ', ", ?, ...)
|
||||
are not supported by Mastodon to be published from Peertube
|
||||
use mastodon compatibility below
|
||||
--mt Force Mastodon compatibility for tags (drop every incompatible characters inside tags)
|
||||
This option requires --tags
|
||||
-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 video name
|
||||
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
|
||||
For Peertube, requires the "atd" and "curl utilities installed on the system
|
||||
--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
|
||||
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
|
||||
If the playlist is not found, spawn an error except if --playlist-create 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.
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
from os.path import dirname, realpath
|
||||
import sys
|
||||
import datetime
|
||||
import logging
|
||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
|
||||
|
||||
from docopt import docopt
|
||||
|
||||
|
||||
# Allows a relative import from the parent folder
|
||||
sys.path.insert(0, dirname(realpath(__file__)) + "/lib")
|
||||
|
||||
import yt_upload
|
||||
import pt_upload
|
||||
import utils
|
||||
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from schema import Schema, And, Or, Optional, SchemaError
|
||||
except ImportError:
|
||||
logging.error('This program requires that the `schema` data-validation library'
|
||||
' is installed: \n'
|
||||
'see https://github.com/halst/schema\n')
|
||||
exit(1)
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
import magic
|
||||
except ImportError:
|
||||
logging.error('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.6"
|
||||
|
||||
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
|
||||
VALID_CATEGORIES = (
|
||||
"music", "films", "vehicles",
|
||||
"sports", "travels", "gaming", "people",
|
||||
"comedy", "entertainment", "news",
|
||||
"how to", "education", "activism", "science & technology",
|
||||
"science", "technology", "animals"
|
||||
)
|
||||
VALID_PLATFORM = ('youtube', 'peertube')
|
||||
VALID_LANGUAGES = ('arabic', 'english', 'french',
|
||||
'german', 'hindi', 'italian',
|
||||
'japanese', 'korean', 'mandarin',
|
||||
'portuguese', 'punjabi', 'russian', 'spanish')
|
||||
|
||||
|
||||
def validateVideo(path):
|
||||
supported_types = ['video/mp4']
|
||||
if magic.from_file(path, mime=True) in supported_types:
|
||||
return path
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validateCategory(category):
|
||||
if category.lower() in VALID_CATEGORIES:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validatePrivacy(privacy):
|
||||
if privacy.lower() in VALID_PRIVACY_STATUSES:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validatePlatform(platform):
|
||||
for plfrm in platform.split(','):
|
||||
if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validateLanguage(language):
|
||||
if language.lower() in VALID_LANGUAGES:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def validatePublish(publish):
|
||||
# Check date format and if date is future
|
||||
try:
|
||||
now = datetime.datetime.now()
|
||||
publishAt = datetime.datetime.strptime(publish, '%Y-%m-%dT%H:%M:%S')
|
||||
if now >= publishAt:
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def validateThumbnail(thumbnail):
|
||||
supported_types = ['image/jpg', 'image/jpeg']
|
||||
if magic.from_file(thumbnail, mime=True) in supported_types:
|
||||
return thumbnail
|
||||
else:
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
options = docopt(__doc__, version=VERSION)
|
||||
|
||||
schema = Schema({
|
||||
'--file': And(str, validateVideo, error='file is not supported, please use mp4'),
|
||||
Optional('--name'): Or(None, And(
|
||||
str,
|
||||
lambda x: not x.isdigit(),
|
||||
error="The video name should be a string")
|
||||
),
|
||||
Optional('--description'): Or(None, And(
|
||||
str,
|
||||
lambda x: not x.isdigit(),
|
||||
error="The video name should be a string")
|
||||
),
|
||||
Optional('--tags'): Or(None, And(
|
||||
str,
|
||||
lambda x: not x.isdigit(),
|
||||
error="Tags should be a string")
|
||||
),
|
||||
Optional('--mt'): bool,
|
||||
Optional('--category'): Or(None, And(
|
||||
str,
|
||||
validateCategory,
|
||||
error="Category not recognized, please see --help")
|
||||
),
|
||||
Optional('--language'): Or(None, And(
|
||||
str,
|
||||
validateLanguage,
|
||||
error="Language not recognized, please see --help")
|
||||
),
|
||||
Optional('--privacy'): Or(None, And(
|
||||
str,
|
||||
validatePrivacy,
|
||||
error="Please use recognized privacy between public, unlisted or private")
|
||||
),
|
||||
Optional('--nfo'): Or(None, str),
|
||||
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")
|
||||
),
|
||||
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'),
|
||||
),
|
||||
Optional('--playlist'): Or(None, str),
|
||||
Optional('--playlistCreate'): bool,
|
||||
'--help': bool,
|
||||
'--version': bool
|
||||
})
|
||||
|
||||
options = utils.parseNFO(options)
|
||||
|
||||
if not options.get('--thumbnail'):
|
||||
options = utils.searchThumbnail(options)
|
||||
|
||||
try:
|
||||
options = schema.validate(options)
|
||||
except SchemaError as e:
|
||||
exit(e)
|
||||
|
||||
if options.get('--platform') is None or "youtube" in options.get('--platform'):
|
||||
yt_upload.run(options)
|
||||
if options.get('--platform') is None or "peertube" in options.get('--platform'):
|
||||
pt_upload.run(options)
|
48
pyproject.toml
Normal file
48
pyproject.toml
Normal file
|
@ -0,0 +1,48 @@
|
|||
[tool.poetry]
|
||||
name = "prismedia"
|
||||
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",
|
||||
"YSalmon"
|
||||
]
|
||||
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
readme = 'README.md'
|
||||
repository = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
|
||||
homepage = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
|
||||
|
||||
keywords = ['peertube', 'youtube', 'prismedia']
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
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"
|
||||
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"
|
||||
|
||||
|
||||
[tool.poetry.scripts]
|
||||
prismedia = 'prismedia.upload:main'
|
||||
prismedia-init = 'prismedia.genconfig:genconfig'
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
195
requirements.txt
Normal file
195
requirements.txt
Normal file
|
@ -0,0 +1,195 @@
|
|||
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==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