mirror of
https://git.lecygnenoir.info/LecygneNoir/prismedia.git
synced 2025-10-03 09:29:16 +02:00
Merge pull request 'hearthbeat (keepalive ?)' (#54) from Zykino/prismedia:hearthbeat into develop
Reviewed-on: https://git.lecygnenoir.info/LecygneNoir/prismedia/pulls/54 Thanks a lot for the idea and the feature!
This commit is contained in:
commit
ca733e0dc3
4 changed files with 87 additions and 48 deletions
63
README.md
63
README.md
|
@ -23,23 +23,20 @@ Scripting your way to upload videos to peertube and youtube. Works with Python 3
|
||||||
|
|
||||||
### From pip
|
### From pip
|
||||||
|
|
||||||
Simply install with
|
Simply install with
|
||||||
|
```sh
|
||||||
```bash
|
|
||||||
pip install prismedia
|
pip install prismedia
|
||||||
```
|
```
|
||||||
|
|
||||||
Upgrade with
|
Upgrade with
|
||||||
|
```sh
|
||||||
```bash
|
|
||||||
pip install --upgrade prismedia
|
pip install --upgrade prismedia
|
||||||
```
|
```
|
||||||
|
|
||||||
### From source
|
### From source
|
||||||
|
|
||||||
Get the source:
|
Get the source:
|
||||||
|
```sh
|
||||||
```bash
|
|
||||||
git clone https://git.lecygnenoir.info/LecygneNoir/prismedia.git prismedia
|
git clone https://git.lecygnenoir.info/LecygneNoir/prismedia.git prismedia
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -49,11 +46,10 @@ You may use pip to install requirements: `pip install -r requirements.txt` if yo
|
||||||
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
|
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
|
||||||
(Or use the existing virtualenv if one is activated)
|
(Or use the existing virtualenv if one is activated)
|
||||||
|
|
||||||
```
|
```sh
|
||||||
poetry install
|
poetry install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Generate sample files with `python -m prismedia.genconfig`.
|
Generate sample files with `python -m prismedia.genconfig`.
|
||||||
|
@ -72,7 +68,7 @@ Youtube uses combination of oauth and API access to identify.
|
||||||
The first time you connect, prismedia will open your browser to ask you to authenticate to
|
The first time you connect, prismedia will open your browser to ask you to authenticate to
|
||||||
Youtube and allow the app to use your Youtube channel.
|
Youtube and allow the app to use your Youtube channel.
|
||||||
**It is here you choose which channel you will upload to**.
|
**It is here you choose which channel you will upload to**.
|
||||||
Once authenticated, the token is stored inside the file ``.youtube_credentials.json``.
|
Once authenticated, the token is stored inside the file `.youtube_credentials.json`.
|
||||||
Prismedia will try to use this file at each launch, and re-ask for authentication if it does not exist.
|
Prismedia will try to use this file at each launch, and re-ask for authentication if it does not exist.
|
||||||
|
|
||||||
**Oauth**:
|
**Oauth**:
|
||||||
|
@ -95,35 +91,42 @@ Support only mp4 for cross compatibility between Youtube and Peertube.
|
||||||
Here are some demonstration of main usage you would like!
|
Here are some demonstration of main usage you would like!
|
||||||
|
|
||||||
Upload a video:
|
Upload a video:
|
||||||
```
|
```sh
|
||||||
prismedia --file="yourvideo.mp4"
|
python -m prismedia --file="yourvideo.mp4"
|
||||||
```
|
```
|
||||||
|
|
||||||
Specify description and tags:
|
Specify description and tags:
|
||||||
```
|
```sh
|
||||||
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
python -m prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
||||||
```
|
```
|
||||||
|
|
||||||
Provide a thumbnail:
|
Provide a thumbnail:
|
||||||
```
|
```sh
|
||||||
prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
|
python -m 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 does not exist.:
|
Publish on Peertube only, while using a channel and a playlist, creating them if they does not exist.:
|
||||||
```
|
```sh
|
||||||
prismedia --file="yourvideo.mp4" --platform=peertube --channel="Cooking recipes" --playlist="Cake recipes" --channelCreate --playlistCreate
|
python -m prismedia --file="yourvideo.mp4" --platform=peertube --channel="Cooking recipes" --playlist="Cake recipes" --channelCreate --playlistCreate
|
||||||
```
|
```
|
||||||
|
|
||||||
Use a NFO file to specify your video options:
|
Use a NFO file to specify your video options:
|
||||||
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
|
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
|
||||||
```
|
```sh
|
||||||
prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
|
python -m prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
To prevent Youtube from inactivating your apikey after 90days of inactivity it is recommended to launch this command automatically from a script around once a month. It will mwke 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
|
||||||
|
python -m prismedia --hearthbeat
|
||||||
|
```
|
||||||
|
|
||||||
Take a look at all available options with `--help`!
|
Take a look at all available options with `--help`!
|
||||||
```
|
```sh
|
||||||
prismedia --help
|
python -m prismedia --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Enhanced use of NFO
|
## Enhanced use of NFO
|
||||||
|
@ -136,7 +139,7 @@ Basically, Prismedia will now load options in this order, using the last value f
|
||||||
`nfo.txt < directory_name.txt < video_name.txt < command line NFO < command line argument`
|
`nfo.txt < directory_name.txt < video_name.txt < command line NFO < command line argument`
|
||||||
|
|
||||||
You'll find a complete set of samples in the [prismedia/samples](prismedia/samples) directory so let's take it as an example:
|
You'll find a complete set of samples in the [prismedia/samples](prismedia/samples) directory so let's take it as an example:
|
||||||
```
|
```sh
|
||||||
$ tree Recipes/
|
$ tree Recipes/
|
||||||
Recipes/
|
Recipes/
|
||||||
├── cli_nfo.txt
|
├── cli_nfo.txt
|
||||||
|
@ -149,9 +152,9 @@ Recipes/
|
||||||
└── yourvideo2.txt
|
└── yourvideo2.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
By using
|
By using
|
||||||
```
|
```sh
|
||||||
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
|
python -m prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
|
||||||
```
|
```
|
||||||
|
|
||||||
Prismedia will:
|
Prismedia will:
|
||||||
|
@ -186,7 +189,7 @@ Available strict options:
|
||||||
- --withPlatform Prevent the upload if at least one platform is not specified
|
- --withPlatform Prevent the upload if at least one platform is not specified
|
||||||
- --withCategory Prevent the upload if no category
|
- --withCategory Prevent the upload if no category
|
||||||
- --withLanguage Prevent upload if no language
|
- --withLanguage Prevent upload if no language
|
||||||
- --withChannel Prevent upload if no channel
|
- --withChannel Prevent upload if no channel
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -224,4 +227,4 @@ Available strict options:
|
||||||
Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
|
Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
Thanks to: @Zykino, @meewan, @rigelk 😘
|
Thanks to: @Zykino, @meewan, @rigelk 😘
|
||||||
|
|
|
@ -373,7 +373,7 @@ def create_callback(encoder, progress_type):
|
||||||
else:
|
else:
|
||||||
# Print a blank line to not (partly) override the progress bar
|
# Print a blank line to not (partly) override the progress bar
|
||||||
print()
|
print()
|
||||||
logger.info("Peertube : Upload finish, Processing…")
|
logger.info("Peertube: Upload finish, Processing…")
|
||||||
|
|
||||||
return callback
|
return callback
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ prismedia - tool to upload videos to Peertube and Youtube
|
||||||
Usage:
|
Usage:
|
||||||
prismedia --file=<FILE> [options]
|
prismedia --file=<FILE> [options]
|
||||||
prismedia -f <FILE> --tags=STRING [options]
|
prismedia -f <FILE> --tags=STRING [options]
|
||||||
|
prismedia --hearthbeat
|
||||||
prismedia -h | --help
|
prismedia -h | --help
|
||||||
prismedia --version
|
prismedia --version
|
||||||
|
|
||||||
|
@ -50,6 +51,9 @@ Options:
|
||||||
--playlistCreate Create the playlist if not exists. (default do not create)
|
--playlistCreate Create the playlist if not exists. (default do not create)
|
||||||
Only relevant if --playlist is set.
|
Only relevant if --playlist is set.
|
||||||
--progress=STRING Set the progress bar view, one of percentage, bigFile (MB), accurate (KB).
|
--progress=STRING Set the progress bar view, one of percentage, bigFile (MB), accurate (KB).
|
||||||
|
|
||||||
|
--hearthbeat Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently)
|
||||||
|
|
||||||
-h --help Show this help.
|
-h --help Show this help.
|
||||||
--version Show version.
|
--version Show version.
|
||||||
|
|
||||||
|
@ -396,11 +400,17 @@ def main():
|
||||||
Optional('--playlist'): Or(None, str),
|
Optional('--playlist'): Or(None, str),
|
||||||
Optional('--playlistCreate'): bool,
|
Optional('--playlistCreate'): bool,
|
||||||
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
|
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
|
||||||
|
'--hearthbeat': bool,
|
||||||
'--help': bool,
|
'--help': bool,
|
||||||
'--version': bool,
|
'--version': bool,
|
||||||
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
|
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
|
||||||
object: object
|
object: object
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if options.get('--hearthbeat'):
|
||||||
|
yt_upload.hearthbeat()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
# We need to validate early options first as withNFO and logs options should be prioritized
|
# We need to validate early options first as withNFO and logs options should be prioritized
|
||||||
try:
|
try:
|
||||||
options = earlyoptionSchema.validate(options)
|
options = earlyoptionSchema.validate(options)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
|
# From Youtube samples: https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
|
||||||
|
|
||||||
import http.client
|
import http.client
|
||||||
import httplib2
|
import httplib2
|
||||||
|
@ -60,6 +60,7 @@ def get_authenticated_service():
|
||||||
check_authenticated_scopes()
|
check_authenticated_scopes()
|
||||||
flow = InstalledAppFlow.from_client_secrets_file(
|
flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
CLIENT_SECRETS_FILE, SCOPES)
|
CLIENT_SECRETS_FILE, SCOPES)
|
||||||
|
|
||||||
if exists(CREDENTIALS_PATH):
|
if exists(CREDENTIALS_PATH):
|
||||||
with open(CREDENTIALS_PATH, 'r') as f:
|
with open(CREDENTIALS_PATH, 'r') as f:
|
||||||
credential_params = json.load(f)
|
credential_params = json.load(f)
|
||||||
|
@ -76,7 +77,7 @@ def get_authenticated_service():
|
||||||
p = copy.deepcopy(vars(credentials))
|
p = copy.deepcopy(vars(credentials))
|
||||||
del p["expiry"]
|
del p["expiry"]
|
||||||
json.dump(p, f)
|
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():
|
def check_authenticated_scopes():
|
||||||
|
@ -181,14 +182,24 @@ def initialize_upload(youtube, options):
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_by_name(youtube, playlist_name):
|
def get_playlist_by_name(youtube, playlist_name):
|
||||||
response = youtube.playlists().list(
|
pageToken = ""
|
||||||
part='snippet,id',
|
while pageToken != None:
|
||||||
mine=True,
|
response = youtube.playlists().list(
|
||||||
maxResults=50
|
part='snippet,id',
|
||||||
).execute()
|
mine=True,
|
||||||
for playlist in response["items"]:
|
maxResults=50,
|
||||||
if playlist["snippet"]['title'] == playlist_name:
|
pageToken=pageToken
|
||||||
return playlist['id']
|
).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):
|
def create_playlist(youtube, playlist_name):
|
||||||
|
@ -293,7 +304,7 @@ def resumable_upload(request, resource, method, options):
|
||||||
status, response = request.next_chunk()
|
status, response = request.next_chunk()
|
||||||
if response is not None:
|
if response is not None:
|
||||||
if method == 'insert' and 'id' in response:
|
if method == 'insert' and 'id' in response:
|
||||||
logger.info('Youtube : Video was successfully uploaded.')
|
logger.info('Youtube: Video was successfully uploaded.')
|
||||||
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
|
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
|
||||||
logger.info(template % response['id'])
|
logger.info(template % response['id'])
|
||||||
template_stdout = 'https://youtu.be/%s'
|
template_stdout = 'https://youtu.be/%s'
|
||||||
|
@ -305,36 +316,51 @@ def resumable_upload(request, resource, method, options):
|
||||||
elif method != 'insert' or "id" not in response:
|
elif method != 'insert' or "id" not in response:
|
||||||
logger.info('Youtube: Thumbnail was successfully set.')
|
logger.info('Youtube: Thumbnail was successfully set.')
|
||||||
else:
|
else:
|
||||||
template = ('Youtube : The upload failed with an '
|
template = ('Youtube: The upload failed with an '
|
||||||
'unexpected response: %s')
|
'unexpected response: %s')
|
||||||
logger.critical(template % response)
|
logger.critical(template % response)
|
||||||
exit(1)
|
exit(1)
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
if e.resp.status in RETRIABLE_STATUS_CODES:
|
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)
|
error = template % (e.resp.status, e.content)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except RETRIABLE_EXCEPTIONS as e:
|
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:
|
if error is not None:
|
||||||
logger.warning(error)
|
logger.warning(error)
|
||||||
retry += 1
|
retry += 1
|
||||||
if retry > MAX_RETRIES:
|
if retry > MAX_RETRIES:
|
||||||
logger.error('Youtube : No longer attempting to retry.')
|
logger.error('Youtube: No longer attempting to retry.')
|
||||||
|
|
||||||
max_sleep = 2 ** retry
|
max_sleep = 2 ** retry
|
||||||
sleep_seconds = random.random() * max_sleep
|
sleep_seconds = random.random() * max_sleep
|
||||||
logger.warning('Youtube : Sleeping %f seconds and then retrying...'
|
logger.warning('Youtube: Sleeping %f seconds and then retrying...'
|
||||||
% sleep_seconds)
|
% sleep_seconds)
|
||||||
time.sleep(sleep_seconds)
|
time.sleep(sleep_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def hearthbeat():
|
||||||
|
"""Use the minimums credits possibles of the API so google does not readuce to 0 the allowed credits.
|
||||||
|
This apparently happens after 90 days without any usage of credits.
|
||||||
|
For more info see the official documentations:
|
||||||
|
- General informations about quotas: https://developers.google.com/youtube/v3/getting-started#quota
|
||||||
|
- Quota costs for API requests: https://developers.google.com/youtube/v3/determine_quota_cost
|
||||||
|
- ToS (Americas) #Usage and Quotas: https://developers.google.com/youtube/terms/api-services-terms-of-service#usage-and-quotas"""
|
||||||
|
youtube = get_authenticated_service()
|
||||||
|
try:
|
||||||
|
get_playlist_by_name(youtube, "Foo")
|
||||||
|
except HttpError as e:
|
||||||
|
logger.error('Youtube: An HTTP error %d occurred on hearthbeat:\n%s' %
|
||||||
|
(e.resp.status, e.content))
|
||||||
|
|
||||||
|
|
||||||
def run(options):
|
def run(options):
|
||||||
youtube = get_authenticated_service()
|
youtube = get_authenticated_service()
|
||||||
try:
|
try:
|
||||||
initialize_upload(youtube, options)
|
initialize_upload(youtube, options)
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
logger.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status,
|
logger.error('Youtube: An HTTP error %d occurred:\n%s' % (e.resp.status,
|
||||||
e.content))
|
e.content))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue