@ -1,161 +0,0 @@ | |||||
#!/usr/bin/python | |||||
# coding: utf-8 | |||||
import os | |||||
import mimetypes | |||||
import json | |||||
import logging | |||||
from os.path import splitext, basename, abspath | |||||
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 upload_video(oauth, secret, options): | |||||
def get_userinfo(): | |||||
user_info = json.loads(oauth.get(url + "/api/v1/users/me").content) | |||||
return str(user_info["id"]) | |||||
def get_file(path): | |||||
mimetypes.init() | |||||
return (basename(path), open(abspath(path), 'rb'), | |||||
mimetypes.types_map[splitext(path)[1]]) | |||||
url = str(secret.get('peertube', 'peertube_url')).rstrip('/') | |||||
# 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")), | |||||
("channelId", get_userinfo()), | |||||
("videofile", get_file(options.get('--file'))) | |||||
] | |||||
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.mastodonTag(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('--privacy'): | |||||
fields.append(("privacy", str(PEERTUBE_PRIVACY[options.get('--privacy').lower()]))) | |||||
else: | |||||
fields.append(("privacy", "3")) | |||||
if options.get('--disable-comments'): | |||||
fields.append(("commentsEnabled", "0")) | |||||
else: | |||||
fields.append(("commentsEnabled", "1")) | |||||
if options.get('--thumbnail'): | |||||
fields.append(("thumbnailfile", get_file(options.get('--thumbnail')))) | |||||
fields.append(("previewfile", get_file(options.get('--thumbnail')))) | |||||
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)) | |||||
if options.get('--publishAt'): | |||||
utils.publishAt(str(options.get('--publishAt')), oauth, url, idvideo, secret) | |||||
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)) |
@ -1,263 +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 publishAt(publishAt, oauth, url, idvideo, secret): | |||||
try: | |||||
FNULL = open(devnull, 'w') | |||||
check_call(["at", "-V"], stdout=FNULL, stderr=STDOUT) | |||||
except CalledProcessError: | |||||
logging.error("You need to install the atd daemon to use the publishAt option.") | |||||
exit(1) | |||||
try: | |||||
FNULL = open(devnull, 'w') | |||||
check_call(["curl", "-V"], stdout=FNULL, stderr=STDOUT) | |||||
except CalledProcessError: | |||||
logging.error("You need to install the curl command line to use the publishAt option.") | |||||
exit(1) | |||||
try: | |||||
FNULL = open(devnull, 'w') | |||||
check_call(["jq", "-V"], stdout=FNULL, stderr=STDOUT) | |||||
except CalledProcessError: | |||||
logging.error("You need to install the jq command line to use the publishAt option.") | |||||
exit(1) | |||||
time = publishAt.split("T") | |||||
# Remove leading seconds that atd does not manage | |||||
if time[1].count(":") == 2: | |||||
time[1] = time[1][:-3] | |||||
atTime = time[1] + " " + time[0] | |||||
refresh_token=str(oauth.__dict__['_client'].__dict__['refresh_token']) | |||||
atFile = "/tmp/peertube_" + idvideo + "_" + publishAt + ".at" | |||||
try: | |||||
openfile = open(atFile,"w") | |||||
openfile.write('token=$(curl -X POST -d "client_id=' + str(secret.get('peertube', 'client_id')) + | |||||
'&client_secret=' + str(secret.get('peertube', 'client_secret')) + | |||||
'&grant_type=refresh_token&refresh_token=' + str(refresh_token) + | |||||
'" "' + url + '/api/v1/users/token" | jq -r .access_token)') | |||||
openfile.write("\n") | |||||
openfile.write('curl "' + url + '/api/v1/videos/' + idvideo + | |||||
'" -X PUT -H "Authorization: Bearer ${token}"' + | |||||
' -H "Content-Type: multipart/form-data" -F "privacy=1"') | |||||
openfile.write("\n ") # atd needs an empty line at the end of the file to load... | |||||
openfile.close() | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logging.error("Error: " + str(e.message)) | |||||
else: | |||||
logging.error("Error: " + str(e)) | |||||
try: | |||||
FNULL = open(devnull, 'w') | |||||
check_call(["at", "-M", "-f", atFile, atTime], stdout=FNULL, stderr=STDOUT) | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logging.error("Error: " + str(e.message)) | |||||
else: | |||||
logging.error("Error: " + str(e)) | |||||
def mastodonTag(tag): | |||||
tags = tag.split(' ') | |||||
mtag = '' | |||||
for s in tags: | |||||
if s == '': | |||||
continue | |||||
strtag = unicodedata.normalize('NFKD', unicode (s, 'utf-8')).encode('ASCII', 'ignore') | |||||
strtag = ''.join(e for e in strtag if e.isalnum()) | |||||
strtag = upcaseFirstLetter(strtag) | |||||
mtag = mtag + strtag | |||||
return mtag |
@ -1,202 +0,0 @@ | |||||
#!/usr/bin/python | |||||
# coding: utf-8 | |||||
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa | |||||
import httplib | |||||
import httplib2 | |||||
import random | |||||
import time | |||||
import copy | |||||
import json | |||||
from os.path import splitext, basename, exists | |||||
import google.oauth2.credentials | |||||
import datetime | |||||
import pytz | |||||
import logging | |||||
from tzlocal import get_localzone | |||||
from googleapiclient.discovery import build | |||||
from googleapiclient.errors import HttpError | |||||
from googleapiclient.http import MediaFileUpload | |||||
from google_auth_oauthlib.flow import InstalledAppFlow | |||||
import utils | |||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) | |||||
# Explicitly tell the underlying HTTP transport library not to retry, since | |||||
# we are handling retry logic ourselves. | |||||
httplib2.RETRIES = 1 | |||||
# Maximum number of times to retry before giving up. | |||||
MAX_RETRIES = 10 | |||||
# Youtube retriables cases | |||||
RETRIABLE_EXCEPTIONS = ( | |||||
IOError, | |||||
httplib2.HttpLib2Error, | |||||
httplib.NotConnected, | |||||
httplib.IncompleteRead, | |||||
httplib.ImproperConnectionState, | |||||
httplib.CannotSendRequest, | |||||
httplib.CannotSendHeader, | |||||
httplib.ResponseNotReady, | |||||
httplib.BadStatusLine, | |||||
) | |||||
RETRIABLE_STATUS_CODES = [500, 502, 503, 504] | |||||
CLIENT_SECRETS_FILE = 'youtube_secret.json' | |||||
CREDENTIALS_PATH = ".youtube_credentials.json" | |||||
SCOPES = ['https://www.googleapis.com/auth/youtube.upload'] | |||||
API_SERVICE_NAME = 'youtube' | |||||
API_VERSION = 'v3' | |||||
# Authorize the request and store authorization credentials. | |||||
def get_authenticated_service(): | |||||
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) | |||||
credentials = google.oauth2.credentials.Credentials( | |||||
credential_params["token"], | |||||
refresh_token=credential_params["_refresh_token"], | |||||
token_uri=credential_params["_token_uri"], | |||||
client_id=credential_params["_client_id"], | |||||
client_secret=credential_params["_client_secret"] | |||||
) | |||||
else: | |||||
credentials = flow.run_local_server() | |||||
with open(CREDENTIALS_PATH, 'w') as f: | |||||
p = copy.deepcopy(vars(credentials)) | |||||
del p["expiry"] | |||||
json.dump(p, f) | |||||
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False) | |||||
def initialize_upload(youtube, options): | |||||
path = options.get('--file') | |||||
tags = None | |||||
if options.get('--tags'): | |||||
tags = options.get('--tags').split(',') | |||||
category = None | |||||
if options.get('--category'): | |||||
category = utils.getCategory(options.get('--category'), 'youtube') | |||||
language = None | |||||
if options.get('--language'): | |||||
language = utils.getLanguage(options.get('--language'), "youtube") | |||||
license = None | |||||
if options.get('--cca'): | |||||
license = "creativeCommon" | |||||
body = { | |||||
"snippet": { | |||||
"title": options.get('--name') or splitext(basename(path))[0], | |||||
"description": options.get('--description') or "default description", | |||||
"tags": tags, | |||||
# if no category, set default to 1 (Films) | |||||
"categoryId": str(category or 1), | |||||
"defaultAudioLanguage": str(language or 'en') | |||||
}, | |||||
"status": { | |||||
"privacyStatus": str(options.get('--privacy') or "private"), | |||||
"license": str(license or "youtube"), | |||||
} | |||||
} | |||||
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() | |||||
body['status']['publishAt'] = str(publishAt) | |||||
# Call the API's videos.insert method to create and upload the video. | |||||
insert_request = youtube.videos().insert( | |||||
part=','.join(body.keys()), | |||||
body=body, | |||||
media_body=MediaFileUpload(path, chunksize=-1, resumable=True) | |||||
) | |||||
video_id = resumable_upload(insert_request, 'video', 'insert') | |||||
# 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) | |||||
def set_thumbnail(youtube, media_file, **kwargs): | |||||
kwargs = utils.remove_empty_kwargs(**kwargs) # See full sample for function | |||||
request = youtube.thumbnails().set( | |||||
media_body=MediaFileUpload(media_file, chunksize=-1, | |||||
resumable=True), | |||||
**kwargs | |||||
) | |||||
# See full sample for function | |||||
return resumable_upload(request, 'thumbnail', 'set') | |||||
# This method implements an exponential backoff strategy to resume a | |||||
# failed upload. | |||||
def resumable_upload(request, resource, method): | |||||
response = None | |||||
error = None | |||||
retry = 0 | |||||
while response is None: | |||||
try: | |||||
template = 'Youtube: Uploading %s...' | |||||
logging.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.') | |||||
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)' | |||||
logging.info(template % response['id']) | |||||
return response['id'] | |||||
elif method != 'insert' or "id" not in response: | |||||
logging.info('Youtube: Thumbnail was successfully set.') | |||||
else: | |||||
template = ('Youtube : The upload failed with an ' | |||||
'unexpected response: %s') | |||||
logging.error(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' | |||||
error = template % (e.resp.status, e.content) | |||||
else: | |||||
raise | |||||
except RETRIABLE_EXCEPTIONS as e: | |||||
error = 'Youtube : A retriable error occurred: %s' % e | |||||
if error is not None: | |||||
logging.warning(error) | |||||
retry += 1 | |||||
if retry > MAX_RETRIES: | |||||
logging.error('Youtube : No longer attempting to retry.') | |||||
exit(1) | |||||
max_sleep = 2 ** retry | |||||
sleep_seconds = random.random() * max_sleep | |||||
logging.warning('Youtube : Sleeping %f seconds and then retrying...' | |||||
% sleep_seconds) | |||||
time.sleep(sleep_seconds) | |||||
def run(options): | |||||
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, | |||||
e.content)) |
@ -1,24 +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 | |||||
nsfw = True | |||||
platform = youtube, peertube | |||||
language = French | |||||
publishAt=2034-05-07T19:00:00 |
@ -0,0 +1,657 @@ | |||||
[[package]] | |||||
category = "main" | |||||
description = "Command Arguments for Humans." | |||||
name = "args" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.1.0" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Extensible memoizing collections and decorators" | |||||
name = "cachetools" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "3.1.1" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Extensible memoizing collections and decorators" | |||||
name = "cachetools" | |||||
optional = false | |||||
python-versions = "~=3.5" | |||||
version = "4.2.0" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Python package for providing Mozilla's CA Bundle." | |||||
name = "certifi" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "2020.12.5" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Universal encoding detector for Python 2 and 3" | |||||
name = "chardet" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | |||||
version = "4.0.0" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Python Command Line Interface Tools" | |||||
name = "clint" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.5.1" | |||||
[package.dependencies] | |||||
args = "*" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Updated configparser from Python 3.7 for Python 2.6+." | |||||
name = "configparser" | |||||
optional = false | |||||
python-versions = ">=2.6" | |||||
version = "3.8.1" | |||||
[package.extras] | |||||
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] | |||||
testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Backports and enhancements for the contextlib module" | |||||
name = "contextlib2" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||||
version = "0.6.0.post1" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Pythonic argument parser, that will make you smile" | |||||
name = "docopt" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.6.2" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Clean single-source support for Python 3 and 2" | |||||
name = "future" | |||||
optional = false | |||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" | |||||
version = "0.17.1" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google API client core library" | |||||
name = "google-api-core" | |||||
optional = false | |||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" | |||||
version = "1.23.0" | |||||
[package.dependencies] | |||||
google-auth = ">=1.21.1,<2.0dev" | |||||
googleapis-common-protos = ">=1.6.0,<2.0dev" | |||||
protobuf = ">=3.12.0" | |||||
pytz = "*" | |||||
requests = ">=2.18.0,<3.0.0dev" | |||||
setuptools = ">=34.0.0" | |||||
six = ">=1.13.0" | |||||
[package.extras] | |||||
grpc = ["grpcio (>=1.29.0,<2.0dev)"] | |||||
grpcgcp = ["grpcio-gcp (>=0.2.2)"] | |||||
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google API client core library" | |||||
name = "google-api-core" | |||||
optional = false | |||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" | |||||
version = "1.25.0" | |||||
[package.dependencies] | |||||
google-auth = ">=1.21.1,<2.0dev" | |||||
googleapis-common-protos = ">=1.6.0,<2.0dev" | |||||
protobuf = ">=3.12.0" | |||||
pytz = "*" | |||||
requests = ">=2.18.0,<3.0.0dev" | |||||
setuptools = ">=40.3.0" | |||||
six = ">=1.13.0" | |||||
[package.extras] | |||||
grpc = ["grpcio (>=1.29.0,<2.0dev)"] | |||||
grpcgcp = ["grpcio-gcp (>=0.2.2)"] | |||||
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google API Client Library for Python" | |||||
name = "google-api-python-client" | |||||
optional = false | |||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" | |||||
version = "1.12.2" | |||||
[package.dependencies] | |||||
google-api-core = ">=1.21.0,<2dev" | |||||
google-auth = ">=1.16.0" | |||||
google-auth-httplib2 = ">=0.0.3" | |||||
httplib2 = ">=0.9.2,<1dev" | |||||
six = ">=1.13.0,<2dev" | |||||
uritemplate = ">=3.0.0,<4dev" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google Authentication Library" | |||||
name = "google-auth" | |||||
optional = false | |||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" | |||||
version = "1.23.0" | |||||
[package.dependencies] | |||||
cachetools = ">=2.0.0,<5.0" | |||||
pyasn1-modules = ">=0.2.1" | |||||
setuptools = ">=40.3.0" | |||||
six = ">=1.9.0" | |||||
[package.dependencies.rsa] | |||||
python = ">=3.5" | |||||
version = ">=3.1.4,<5" | |||||
[package.extras] | |||||
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google Authentication Library" | |||||
name = "google-auth" | |||||
optional = false | |||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" | |||||
version = "1.24.0" | |||||
[package.dependencies] | |||||
cachetools = ">=2.0.0,<5.0" | |||||
pyasn1-modules = ">=0.2.1" | |||||
setuptools = ">=40.3.0" | |||||
six = ">=1.9.0" | |||||
[package.dependencies.rsa] | |||||
python = ">=3.6" | |||||
version = ">=3.1.4,<5" | |||||
[package.extras] | |||||
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google Authentication Library: httplib2 transport" | |||||
name = "google-auth-httplib2" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.0.4" | |||||
[package.dependencies] | |||||
google-auth = "*" | |||||
httplib2 = ">=0.9.1" | |||||
six = "*" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google Authentication Library" | |||||
name = "google-auth-oauthlib" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.4.1" | |||||
[package.dependencies] | |||||
google-auth = "*" | |||||
requests-oauthlib = ">=0.7.0" | |||||
[package.extras] | |||||
tool = ["click"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Google Authentication Library" | |||||
name = "google-auth-oauthlib" | |||||
optional = false | |||||
python-versions = ">=3.6" | |||||
version = "0.4.2" | |||||
[package.dependencies] | |||||
google-auth = "*" | |||||
requests-oauthlib = ">=0.7.0" | |||||
[package.extras] | |||||
tool = ["click"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Common protobufs used in Google APIs" | |||||
name = "googleapis-common-protos" | |||||
optional = false | |||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" | |||||
version = "1.52.0" | |||||
[package.dependencies] | |||||
protobuf = ">=3.6.0" | |||||
[package.extras] | |||||
grpc = ["grpcio (>=1.0.0)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "A comprehensive HTTP client library." | |||||
name = "httplib2" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.12.3" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Internationalized Domain Names in Applications (IDNA)" | |||||
name = "idna" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||||
version = "2.10" | |||||
[[package]] | |||||
category = "main" | |||||
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" | |||||
name = "oauthlib" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "2.1.0" | |||||
[package.extras] | |||||
rsa = ["cryptography"] | |||||
signals = ["blinker"] | |||||
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] | |||||
test = ["nose", "unittest2", "cryptography", "mock", "pyjwt (>=1.0.0)", "blinker"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "Protocol Buffers" | |||||
name = "protobuf" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "3.14.0" | |||||
[package.dependencies] | |||||
six = ">=1.9" | |||||
[[package]] | |||||
category = "main" | |||||
description = "ASN.1 types and codecs" | |||||
name = "pyasn1" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.4.8" | |||||
[[package]] | |||||
category = "main" | |||||
description = "A collection of ASN.1-based protocols modules." | |||||
name = "pyasn1-modules" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.2.8" | |||||
[package.dependencies] | |||||
pyasn1 = ">=0.4.6,<0.5.0" | |||||
[[package]] | |||||
category = "main" | |||||
description = "File type identification using libmagic" | |||||
name = "python-magic" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | |||||
version = "0.4.20" | |||||
[[package]] | |||||
category = "main" | |||||
description = "File type identification using libmagic binary package" | |||||
marker = "platform_system == \"Windows\"" | |||||
name = "python-magic-bin" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.4.14" | |||||
[[package]] | |||||
category = "main" | |||||
description = "World timezone definitions, modern and historical" | |||||
name = "pytz" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "2020.5" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Python HTTP for Humans." | |||||
name = "requests" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | |||||
version = "2.25.1" | |||||
[package.dependencies] | |||||
certifi = ">=2017.4.17" | |||||
chardet = ">=3.0.2,<5" | |||||
idna = ">=2.5,<3" | |||||
urllib3 = ">=1.21.1,<1.27" | |||||
[package.extras] | |||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] | |||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "OAuthlib authentication support for Requests." | |||||
name = "requests-oauthlib" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.8.0" | |||||
[package.dependencies] | |||||
oauthlib = ">=0.6.2" | |||||
requests = ">=2.0.0" | |||||
[package.extras] | |||||
rsa = ["oauthlib (>=0.6.2)", "requests (>=2.0.0)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "A utility belt for advanced users of python-requests" | |||||
name = "requests-toolbelt" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.9.1" | |||||
[package.dependencies] | |||||
requests = ">=2.0.1,<3.0.0" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Pure-Python RSA implementation" | |||||
marker = "python_version >= \"3.6\"" | |||||
name = "rsa" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "4.4" | |||||
[package.dependencies] | |||||
pyasn1 = ">=0.1.3" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Pure-Python RSA implementation" | |||||
marker = "python_version >= \"3.5\"" | |||||
name = "rsa" | |||||
optional = false | |||||
python-versions = ">=3.5, <4" | |||||
version = "4.7" | |||||
[package.dependencies] | |||||
pyasn1 = ">=0.1.3" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Simple data validation library" | |||||
name = "schema" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "0.7.3" | |||||
[package.dependencies] | |||||
contextlib2 = ">=0.5.5" | |||||
[[package]] | |||||
category = "main" | |||||
description = "Python 2 and 3 compatibility utilities" | |||||
name = "six" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" | |||||
version = "1.15.0" | |||||
[[package]] | |||||
category = "main" | |||||
description = "tzinfo object for the local timezone" | |||||
name = "tzlocal" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "1.5.1" | |||||
[package.dependencies] | |||||
pytz = "*" | |||||
[[package]] | |||||
category = "main" | |||||
description = "ASCII transliterations of Unicode text" | |||||
name = "unidecode" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||||
version = "1.1.2" | |||||
[[package]] | |||||
category = "main" | |||||
description = "URI templates" | |||||
name = "uritemplate" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||||
version = "3.0.1" | |||||
[[package]] | |||||
category = "main" | |||||
description = "HTTP library with thread-safe connection pooling, file post, and more." | |||||
name = "urllib3" | |||||
optional = false | |||||
python-versions = "*" | |||||
version = "1.22" | |||||
[package.extras] | |||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] | |||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] | |||||
[[package]] | |||||
category = "main" | |||||
description = "HTTP library with thread-safe connection pooling, file post, and more." | |||||
name = "urllib3" | |||||
optional = false | |||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" | |||||
version = "1.26.2" | |||||
[package.extras] | |||||
brotli = ["brotlipy (>=0.6.0)"] | |||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] | |||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] | |||||
[metadata] | |||||
content-hash = "41a9471d93da0f5e3d684cdf9e8f981659030d67f29ef6bf55c07a0d49a3ee93" | |||||
python-versions = ">=3.5" | |||||
[metadata.files] | |||||
args = [ | |||||
{file = "args-0.1.0.tar.gz", hash = "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814"}, | |||||
] | |||||
cachetools = [ | |||||
{file = "cachetools-3.1.1-py2.py3-none-any.whl", hash = "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae"}, | |||||
{file = "cachetools-3.1.1.tar.gz", hash = "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a"}, | |||||
{file = "cachetools-4.2.0-py3-none-any.whl", hash = "sha256:c6b07a6ded8c78bf36730b3dc452dfff7d95f2a12a2fed856b1a0cb13ca78c61"}, | |||||
{file = "cachetools-4.2.0.tar.gz", hash = "sha256:3796e1de094f0eaca982441c92ce96c68c89cced4cd97721ab297ea4b16db90e"}, | |||||
] | |||||
certifi = [ | |||||
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, | |||||
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, | |||||
] | |||||
chardet = [ | |||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, | |||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, | |||||
] | |||||
clint = [ | |||||
{file = "clint-0.5.1.tar.gz", hash = "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa"}, | |||||
] | |||||
configparser = [ | |||||
{file = "configparser-3.8.1-py2.py3-none-any.whl", hash = "sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18"}, | |||||
{file = "configparser-3.8.1.tar.gz", hash = "sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17"}, | |||||
] | |||||
contextlib2 = [ | |||||
{file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, | |||||
{file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, | |||||
] | |||||
docopt = [ | |||||
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, | |||||
] | |||||
future = [ | |||||
{file = "future-0.17.1.tar.gz", hash = "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"}, | |||||
] | |||||
google-api-core = [ | |||||
{file = "google-api-core-1.23.0.tar.gz", hash = "sha256:1bb3c485c38eacded8d685b1759968f6cf47dd9432922d34edb90359eaa391e2"}, | |||||
{file = "google_api_core-1.23.0-py2.py3-none-any.whl", hash = "sha256:94d8c707d358d8d9e8b0045c42be20efb58433d308bd92cf748511c7825569c8"}, | |||||
{file = "google-api-core-1.25.0.tar.gz", hash = "sha256:d967beae8d8acdb88fb2f6f769e2ee0ee813042576a08891bded3b8e234150ae"}, | |||||
{file = "google_api_core-1.25.0-py2.py3-none-any.whl", hash = "sha256:4656345cba9627ab1290eab51300a6397cc50370d99366133df1ae64b744e1eb"}, | |||||
] | |||||
google-api-python-client = [ | |||||
{file = "google-api-python-client-1.12.2.tar.gz", hash = "sha256:54a7d330833a2e7b0587446d7e4ae6d0244925a9a8e1dfe878f3f7e06cdedb62"}, | |||||
{file = "google_api_python_client-1.12.2-py2.py3-none-any.whl", hash = "sha256:05cb331ed1aa15746f606c7e36ea51dbe7c29b1a5df9bbf58140901fe23d7142"}, | |||||
] | |||||
google-auth = [ | |||||
{file = "google-auth-1.23.0.tar.gz", hash = "sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440"}, | |||||
{file = "google_auth-1.23.0-py2.py3-none-any.whl", hash = "sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"}, | |||||
{file = "google-auth-1.24.0.tar.gz", hash = "sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e"}, | |||||
{file = "google_auth-1.24.0-py2.py3-none-any.whl", hash = "sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"}, | |||||
] | |||||
google-auth-httplib2 = [ | |||||
{file = "google-auth-httplib2-0.0.4.tar.gz", hash = "sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39"}, | |||||
{file = "google_auth_httplib2-0.0.4-py2.py3-none-any.whl", hash = "sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee"}, | |||||
] | |||||
google-auth-oauthlib = [ | |||||
{file = "google-auth-oauthlib-0.4.1.tar.gz", hash = "sha256:88d2cd115e3391eb85e1243ac6902e76e77c5fe438b7276b297fbe68015458dd"}, | |||||
{file = "google_auth_oauthlib-0.4.1-py2.py3-none-any.whl", hash = "sha256:a92a0f6f41a0fb6138454fbc02674e64f89d82a244ea32f98471733c8ef0e0e1"}, | |||||
{file = "google-auth-oauthlib-0.4.2.tar.gz", hash = "sha256:65b65bc39ad8cab15039b35e5898455d3d66296d0584d96fe0e79d67d04c51d9"}, | |||||
{file = "google_auth_oauthlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:d4d98c831ea21d574699978827490a41b94f05d565c617fe1b420e88f1fc8d8d"}, | |||||
] | |||||
googleapis-common-protos = [ | |||||
{file = "googleapis-common-protos-1.52.0.tar.gz", hash = "sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351"}, | |||||
{file = "googleapis_common_protos-1.52.0-py2.py3-none-any.whl", hash = "sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24"}, | |||||
] | |||||
httplib2 = [ | |||||
{file = "httplib2-0.12.3-py3-none-any.whl", hash = "sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8"}, | |||||
{file = "httplib2-0.12.3.tar.gz", hash = "sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600"}, | |||||
] | |||||
idna = [ | |||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, | |||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, | |||||
] | |||||
oauthlib = [ | |||||
{file = "oauthlib-2.1.0-py2.py3-none-any.whl", hash = "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"}, | |||||
{file = "oauthlib-2.1.0.tar.gz", hash = "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162"}, | |||||
] | |||||
protobuf = [ | |||||
{file = "protobuf-3.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a"}, | |||||
{file = "protobuf-3.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5"}, | |||||
{file = "protobuf-3.14.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472"}, | |||||
{file = "protobuf-3.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142"}, | |||||
{file = "protobuf-3.14.0-cp35-cp35m-win32.whl", hash = "sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2"}, | |||||
{file = "protobuf-3.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980"}, | |||||
{file = "protobuf-3.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2"}, | |||||
{file = "protobuf-3.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1"}, | |||||
{file = "protobuf-3.14.0-cp36-cp36m-win32.whl", hash = "sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e"}, | |||||
{file = "protobuf-3.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836"}, | |||||
{file = "protobuf-3.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd"}, | |||||
{file = "protobuf-3.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac"}, | |||||
{file = "protobuf-3.14.0-cp37-cp37m-win32.whl", hash = "sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d"}, | |||||
{file = "protobuf-3.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5"}, | |||||
{file = "protobuf-3.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043"}, | |||||
{file = "protobuf-3.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00"}, | |||||
{file = "protobuf-3.14.0-py2.py3-none-any.whl", hash = "sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c"}, | |||||
{file = "protobuf-3.14.0.tar.gz", hash = "sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce"}, | |||||
] | |||||
pyasn1 = [ | |||||
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, | |||||
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, | |||||
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, | |||||
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, | |||||
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, | |||||
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, | |||||
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, | |||||
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, | |||||
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, | |||||
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, | |||||
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, | |||||
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, | |||||
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, | |||||
] | |||||
pyasn1-modules = [ | |||||
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, | |||||
{file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, | |||||
{file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, | |||||
{file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, | |||||
{file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, | |||||
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, | |||||
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, | |||||
] | |||||
python-magic = [ | |||||
{file = "python-magic-0.4.20.tar.gz", hash = "sha256:0cc52ccad086c377b9194014e3dbf98d94b194344630172510a6a3e716b47801"}, | |||||
{file = "python_magic-0.4.20-py2.py3-none-any.whl", hash = "sha256:33ce94d9395aa269a9c5fac10ae124a5fb328ebe248f36efc5a43922edee662e"}, | |||||
] | |||||
python-magic-bin = [ | |||||
{file = "python_magic_bin-0.4.14-py2.py3-none-macosx_10_6_intel.whl", hash = "sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4"}, | |||||
{file = "python_magic_bin-0.4.14-py2.py3-none-win32.whl", hash = "sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892"}, | |||||
{file = "python_magic_bin-0.4.14-py2.py3-none-win_amd64.whl", hash = "sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69"}, | |||||
] | |||||
pytz = [ | |||||
{file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, | |||||
{file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, | |||||
] | |||||
requests = [ | |||||
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, | |||||
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, | |||||
] | |||||
requests-oauthlib = [ | |||||
{file = "requests-oauthlib-0.8.0.tar.gz", hash = "sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468"}, | |||||
{file = "requests_oauthlib-0.8.0-py2.py3-none-any.whl", hash = "sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca"}, | |||||
] | |||||
requests-toolbelt = [ | |||||
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, | |||||
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, | |||||
] | |||||
rsa = [ | |||||
{file = "rsa-4.4-py2.py3-none-any.whl", hash = "sha256:4afbaaecc3e9550c7351fdf0ab3fea1857ff616b85bab59215f00fb42e0e9582"}, | |||||
{file = "rsa-4.4.tar.gz", hash = "sha256:5d95293bbd0fbee1dd9cb4b72d27b723942eb50584abc8c4f5f00e4bcfa55307"}, | |||||
{file = "rsa-4.7-py3-none-any.whl", hash = "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"}, | |||||
{file = "rsa-4.7.tar.gz", hash = "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4"}, | |||||
] | |||||
schema = [ | |||||
{file = "schema-0.7.3-py2.py3-none-any.whl", hash = "sha256:c331438b60f634cab5664ab720d3083cc444f924d55269530c36b33e3354276f"}, | |||||
{file = "schema-0.7.3.tar.gz", hash = "sha256:4cf529318cfd1e844ecbe02f41f7e5aa027463e7403666a52746f31f04f47a5e"}, | |||||
] | |||||
six = [ | |||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, | |||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, | |||||
] | |||||
tzlocal = [ | |||||
{file = "tzlocal-1.5.1.tar.gz", hash = "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"}, | |||||
] | |||||
unidecode = [ | |||||
{file = "Unidecode-1.1.2-py2.py3-none-any.whl", hash = "sha256:4c9d15d2f73eb0d2649a151c566901f80a030da1ccb0a2043352e1dbf647586b"}, | |||||
{file = "Unidecode-1.1.2.tar.gz", hash = "sha256:a039f89014245e0cad8858976293e23501accc9ff5a7bdbc739a14a2b7b85cdc"}, | |||||
] | |||||
uritemplate = [ | |||||
{file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, | |||||
{file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, | |||||
] | |||||
urllib3 = [ | |||||
{file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"}, | |||||
{file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"}, | |||||
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, | |||||
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, | |||||
] |
@ -0,0 +1,5 @@ | |||||
from future import standard_library | |||||
standard_library.install_aliases() | |||||
from . import upload | |||||
from . import genconfig |
@ -0,0 +1,2 @@ | |||||
from .upload import main | |||||
main() |
@ -0,0 +1,15 @@ | |||||
from os.path import join, abspath, isfile, dirname | |||||
from os import listdir | |||||
from shutil import copyfile | |||||
def genconfig(): | |||||
path = join(dirname(__file__), 'config') | |||||
files = [f for f in listdir(path) if isfile(join(path, f))] | |||||
for f in files: | |||||
copyfile(join(path, f), f) | |||||
if __name__ == '__main__': | |||||
genconfig() |
@ -0,0 +1,398 @@ | |||||
#!/usr/bin/env python | |||||
# coding: utf-8 | |||||
import os | |||||
import mimetypes | |||||
import json | |||||
import logging | |||||
import sys | |||||
import datetime | |||||
import pytz | |||||
from os.path import splitext, basename, abspath | |||||
from tzlocal import get_localzone | |||||
from configparser import RawConfigParser | |||||
from requests_oauthlib import OAuth2Session | |||||
from oauthlib.oauth2 import LegacyApplicationClient | |||||
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor | |||||
from clint.textui.progress import Bar as ProgressBar | |||||
from . import utils | |||||
logger = logging.getLogger('Prismedia') | |||||
PEERTUBE_SECRETS_FILE = 'peertube_secret' | |||||
PEERTUBE_PRIVACY = { | |||||
"public": 1, | |||||
"unlisted": 2, | |||||
"private": 3 | |||||
} | |||||
def get_authenticated_service(secret): | |||||
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/") | |||||
oauth_client = LegacyApplicationClient( | |||||
client_id=str(secret.get('peertube', 'client_id')) | |||||
) | |||||
try: | |||||
oauth = OAuth2Session(client=oauth_client) | |||||
oauth.fetch_token( | |||||
token_url=str(peertube_url + '/api/v1/users/token'), | |||||
# lower as peertube does not store uppercase for pseudo | |||||
username=str(secret.get('peertube', 'username').lower()), | |||||
password=str(secret.get('peertube', 'password')), | |||||
client_id=str(secret.get('peertube', 'client_id')), | |||||
client_secret=str(secret.get('peertube', 'client_secret')) | |||||
) | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logger.critical("Peertube: " + str(e.message)) | |||||
exit(1) | |||||
else: | |||||
logger.critical("Peertube: " + str(e)) | |||||
exit(1) | |||||
return oauth | |||||
def get_default_channel(user_info): | |||||
return user_info['videoChannels'][0]['id'] | |||||
def get_channel_by_name(user_info, options): | |||||
for channel in user_info["videoChannels"]: | |||||
if channel['displayName'] == options.get('--channel'): | |||||
return channel['id'] | |||||
def convert_peertube_date(date): | |||||
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S') | |||||
tz = get_localzone() | |||||
tz = pytz.timezone(str(tz)) | |||||
return tz.localize(date).isoformat() | |||||
def create_channel(oauth, url, options): | |||||
template = ('Peertube: Channel %s does not exist, creating it.') | |||||
logger.info(template % (str(options.get('--channel')))) | |||||
channel_name = utils.cleanString(str(options.get('--channel'))) | |||||
# Peertube allows 20 chars max for channel name | |||||
channel_name = channel_name[:19] | |||||
data = '{"name":"' + channel_name + '", \ | |||||
"displayName":"' + options.get('--channel') + '", \ | |||||
"description":null, \ | |||||
"support":null}' | |||||
headers = { | |||||
'Content-Type': "application/json; charset=UTF-8" | |||||
} | |||||
try: | |||||
response = oauth.post(url + "/api/v1/video-channels/", | |||||
data=data.encode('utf-8'), | |||||
headers=headers) | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logger.error("Peertube: " + str(e.message)) | |||||
else: | |||||
logger.error("Peertube: " + str(e)) | |||||
if response is not None: | |||||
if response.status_code == 200: | |||||
jresponse = response.json() | |||||
jresponse = jresponse['videoChannel'] | |||||
return jresponse['id'] | |||||
if response.status_code == 409: | |||||
logger.critical('Peertube: It seems there is a conflict with an existing channel named ' | |||||
+ channel_name + '.' | |||||
' Please beware Peertube internal name is compiled from 20 firsts characters of channel name.' | |||||
' Also note that channel name are not case sensitive (no uppercase nor accent)' | |||||
' Please check your channel name and retry.') | |||||
exit(1) | |||||
else: | |||||
logger.critical(('Peertube: Creating channel failed with an unexpected response: ' | |||||
'%s') % response) | |||||
exit(1) | |||||
def get_default_playlist(user_info): | |||||
return user_info['videoChannels'][0]['id'] | |||||
def get_playlist_by_name(oauth, url, username, options): | |||||
start = 0 | |||||
user_playlists = json.loads(oauth.get( | |||||
url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content) | |||||
total = user_playlists["total"] | |||||
data = user_playlists["data"] | |||||
# We need to iterate on pagination as peertube returns max 100 playlists (see #41) | |||||
while start < total: | |||||
for playlist in data: | |||||
if playlist['displayName'] == options.get('--playlist'): | |||||
return playlist['id'] | |||||
start = start + 100 | |||||
user_playlists = json.loads(oauth.get( | |||||
url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content) | |||||
data = user_playlists["data"] | |||||
def create_playlist(oauth, url, options, channel): | |||||
template = ('Peertube: Playlist %s does not exist, creating it.') | |||||
logger.info(template % (str(options.get('--playlist')))) | |||||
# We use files for form-data Content | |||||
# see https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file | |||||
# None is used to mute "filename" field | |||||
files = {'displayName': (None, str(options.get('--playlist'))), | |||||
'privacy': (None, "1"), | |||||
'description': (None, "null"), | |||||
'videoChannelId': (None, str(channel)), | |||||
'thumbnailfile': (None, "null")} | |||||
try: | |||||
response = oauth.post(url + "/api/v1/video-playlists/", | |||||
files=files) | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logger.error("Peertube: " + str(e.message)) | |||||
else: | |||||
logger.error("Peertube: " + str(e)) | |||||
if response is not None: | |||||
if response.status_code == 200: | |||||
jresponse = response.json() | |||||
jresponse = jresponse['videoPlaylist'] | |||||
return jresponse['id'] | |||||
else: | |||||
logger.critical(('Peertube: Creating the playlist failed with an unexpected response: ' | |||||
'%s') % response) | |||||
exit(1) | |||||
def set_playlist(oauth, url, video_id, playlist_id): | |||||
logger.info('Peertube: add video to playlist.') | |||||
data = '{"videoId":"' + str(video_id) + '"}' | |||||
headers = { | |||||
'Content-Type': "application/json" | |||||
} | |||||
try: | |||||
response = oauth.post(url + "/api/v1/video-playlists/"+str(playlist_id)+"/videos", | |||||
data=data, | |||||
headers=headers) | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logger.error("Peertube: " + str(e.message)) | |||||
else: | |||||
logger.error("Peertube: " + str(e)) | |||||
if response is not None: | |||||
if response.status_code == 200: | |||||
logger.info('Peertube: Video is successfully added to the playlist.') | |||||
else: | |||||
logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: ' | |||||
'%s') % response) | |||||
exit(1) | |||||
def upload_video(oauth, secret, options): | |||||
def get_userinfo(): | |||||
return json.loads(oauth.get(url+"/api/v1/users/me").content) | |||||
def get_file(path): | |||||
mimetypes.init() | |||||
return (basename(path), open(abspath(path), 'rb'), | |||||
mimetypes.types_map[splitext(path)[1]]) | |||||
path = options.get('--file') | |||||
url = str(secret.get('peertube', 'peertube_url')).rstrip('/') | |||||
user_info = get_userinfo() | |||||
username = str(secret.get('peertube', 'username').lower()) | |||||
# We need to transform fields into tuple to deal with tags as | |||||
# MultipartEncoder does not support list refer | |||||
# https://github.com/requests/toolbelt/issues/190 and | |||||
# https://github.com/requests/toolbelt/issues/205 | |||||
fields = [ | |||||
("name", options.get('--name') or splitext(basename(options.get('--file')))[0]), | |||||
("licence", "1"), | |||||
("description", options.get('--description') or "default description"), | |||||
("nsfw", str(int(options.get('--nsfw')) or "0")), | |||||
("videofile", get_file(path)) | |||||
] | |||||
if options.get('--tags'): | |||||
tags = options.get('--tags').split(',') | |||||
tag_number = 0 | |||||
for strtag in tags: | |||||
tag_number = tag_number + 1 | |||||
# Empty tag crashes Peertube, so skip them | |||||
if strtag == "": | |||||
continue | |||||
# Tag more than 30 chars crashes Peertube, so skip tags | |||||
if len(strtag) >= 30: | |||||
logger.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag) | |||||
logger.warning("Peertube: Meanwhile, this tag will be skipped") | |||||
continue | |||||
# Peertube supports only 5 tags at the moment | |||||
if tag_number > 5: | |||||
logger.warning("Peertube: Sorry, Peertube support 5 tags max, additional tag will be skipped") | |||||
logger.warning("Peertube: Skipping tag " + strtag) | |||||
continue | |||||
fields.append(("tags[]", strtag)) | |||||
if options.get('--category'): | |||||
fields.append(("category", str(utils.getCategory(options.get('--category'), 'peertube')))) | |||||
else: | |||||
# if no category, set default to 2 (Films) | |||||
fields.append(("category", "2")) | |||||
if options.get('--language'): | |||||
fields.append(("language", str(utils.getLanguage(options.get('--language'), "peertube")))) | |||||
else: | |||||
# if no language, set default to 1 (English) | |||||
fields.append(("language", "en")) | |||||
if options.get('--disable-comments'): | |||||
fields.append(("commentsEnabled", "0")) | |||||
else: | |||||
fields.append(("commentsEnabled", "1")) | |||||
privacy = None | |||||
if options.get('--privacy'): | |||||
privacy = options.get('--privacy').lower() | |||||
# If peertubeAt exists, use instead of publishAt | |||||
if options.get('--peertubeAt'): | |||||
publishAt = options.get('--peertubeAt') | |||||
elif options.get('--publishAt'): | |||||
publishAt = options.get('--publishAt') | |||||
if 'publishAt' in locals(): | |||||
publishAt = convert_peertube_date(publishAt) | |||||
fields.append(("scheduleUpdate[updateAt]", publishAt)) | |||||
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"]))) | |||||
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"]))) | |||||
else: | |||||
fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"]))) | |||||
# Set originalDate except if the user force no originalDate | |||||
if options.get('--originalDate'): | |||||
originalDate = convert_peertube_date(options.get('--originalDate')) | |||||
fields.append(("originallyPublishedAt", originalDate)) | |||||
if options.get('--thumbnail'): | |||||
fields.append(("thumbnailfile", get_file(options.get('--thumbnail')))) | |||||
fields.append(("previewfile", get_file(options.get('--thumbnail')))) | |||||
if options.get('--channel'): | |||||
channel_id = get_channel_by_name(user_info, options) | |||||
if not channel_id and options.get('--channelCreate'): | |||||
channel_id = create_channel(oauth, url, options) | |||||
elif not channel_id: | |||||
logger.warning("Peertube: Channel `" + options.get('--channel') + "` is unknown, using default channel.") | |||||
channel_id = get_default_channel(user_info) | |||||
else: | |||||
channel_id = get_default_channel(user_info) | |||||
fields.append(("channelId", str(channel_id))) | |||||
if options.get('--playlist'): | |||||
playlist_id = get_playlist_by_name(oauth, url, username, options) | |||||
if not playlist_id and options.get('--playlistCreate'): | |||||
playlist_id = create_playlist(oauth, url, options, channel_id) | |||||
elif not playlist_id: | |||||
logger.critical("Peertube: Playlist `" + options.get('--playlist') + "` does not exist, please set --playlistCreate" | |||||
" if you want to create it") | |||||
exit(1) | |||||
logger_stdout = None | |||||
if options.get('--url-only') or options.get('--batch'): | |||||
logger_stdout = logging.getLogger('stdoutlogs') | |||||
encoder = MultipartEncoder(fields) | |||||
if options.get('--quiet'): | |||||
multipart_data = encoder | |||||
else: | |||||
progress_callback = create_callback(encoder, options.get('--progress')) | |||||
multipart_data = MultipartEncoderMonitor(encoder, progress_callback) | |||||
headers = { | |||||
'Content-Type': multipart_data.content_type | |||||
} | |||||
response = oauth.post(url + "/api/v1/videos/upload", | |||||
data=multipart_data, | |||||
headers=headers) | |||||
if response is not None: | |||||
if response.status_code == 200: | |||||
jresponse = response.json() | |||||
jresponse = jresponse['video'] | |||||
uuid = jresponse['uuid'] | |||||
video_id = str(jresponse['id']) | |||||
logger.info('Peertube: Video was successfully uploaded.') | |||||
template = 'Peertube: Watch it at %s/videos/watch/%s.' | |||||
logger.info(template % (url, uuid)) | |||||
template_stdout = '%s/videos/watch/%s' | |||||
if options.get('--url-only'): | |||||
logger_stdout.info(template_stdout % (url, uuid)) | |||||
elif options.get('--batch'): | |||||
logger_stdout.info("Peertube: " + template_stdout % (url, uuid)) | |||||
# Upload is successful we may set playlist | |||||
if options.get('--playlist'): | |||||
set_playlist(oauth, url, video_id, playlist_id) | |||||
else: | |||||
logger.critical(('Peertube: The upload failed with an unexpected response: ' | |||||
'%s') % response) | |||||
exit(1) | |||||
upload_finished = False | |||||
def create_callback(encoder, progress_type): | |||||
upload_size_MB = encoder.len * (1 / (1024 * 1024)) | |||||
if progress_type is None or "percentage" in progress_type.lower(): | |||||
progress_lambda = lambda x: int((x / encoder.len) * 100) # Default to percentage | |||||
elif "bigfile" in progress_type.lower(): | |||||
progress_lambda = lambda x: x * (1 / (1024 * 1024)) # MB | |||||
elif "accurate" in progress_type.lower(): | |||||
progress_lambda = lambda x: x * (1 / (1024)) # kB | |||||
else: | |||||
# Should not happen outside of development when adding partly a progress type | |||||
logger.critical("Peertube: Unknown progress type `" + progress_type + "`") | |||||
exit(1) | |||||
bar = ProgressBar(expected_size=progress_lambda(encoder.len), label=f"Peertube upload progress ({upload_size_MB:.2f}MB) ", filled_char='=') | |||||
def callback(monitor): | |||||
# We want the condition to capture the varible from the parent scope, not a local variable that is created after | |||||
global upload_finished | |||||
progress = progress_lambda(monitor.bytes_read) | |||||
bar.show(progress) | |||||
if monitor.bytes_read == encoder.len: | |||||
if not upload_finished: | |||||
# We get two time in the callback with both bytes equals, skip the first | |||||
upload_finished = True | |||||
else: | |||||
# Print a blank line to not (partly) override the progress bar | |||||
print() | |||||
logger.info("Peertube : Upload finish, Processing…") | |||||
return callback | |||||
def run(options): | |||||
secret = RawConfigParser() | |||||
try: | |||||
secret.read(PEERTUBE_SECRETS_FILE) | |||||
except Exception as e: | |||||
logger.critical("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e)) | |||||
exit(1) | |||||
insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT') | |||||
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport | |||||
oauth = get_authenticated_service(secret) | |||||
try: | |||||
logger.info('Peertube: Uploading video...') | |||||
upload_video(oauth, secret, options) | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logger.error("Peertube: " + str(e.message)) | |||||
else: | |||||
logger.error("Peertube: " + str(e)) |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1,450 @@ | |||||
#!/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 -h | --help | |||||
prismedia --version | |||||
Options: | |||||
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option. | |||||
--name=NAME Name of the video to upload. (default to video filename) | |||||
-d, --description=STRING Description of the video. (default: default description) | |||||
-t, --tags=STRING Tags for the video. comma separated. | |||||
WARN: tags with punctuation (!, ', ", ?, ...) | |||||
are not supported by Mastodon to be published from Peertube | |||||
-c, --category=STRING Category for the videos, see below. (default: Films) | |||||
--cca License should be CreativeCommon Attribution (affects Youtube upload only) | |||||
-p, --privacy=STRING Choose between public, unlisted or private. (default: private) | |||||
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled) | |||||
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe) | |||||
--nfo=STRING Configure a specific nfo file to set options for the video. | |||||
By default Prismedia search a .txt based on the video name and will | |||||
decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded) | |||||
See nfo_example.txt for more details | |||||
--platform=STRING List of platform(s) to upload to, comma separated. | |||||
Supported platforms are youtube and peertube (default is both) | |||||
--language=STRING Specify the default language for video. See below for supported language. (default is English) | |||||
--publishAt=DATE Publish the video at the given DATE using local server timezone. | |||||
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00 | |||||
DATE should be in the future | |||||
--peertubeAt=DATE | |||||
--youtubeAt=DATE Override publishAt for the corresponding platform. Allow to create preview on specific platform | |||||
--originalDate=DATE Configure the video as initially recorded at DATE | |||||
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00 | |||||
DATE should be in the past | |||||
--auto-originalDate Automatically use the file modification time as original date | |||||
--thumbnail=STRING Path to a file to use as a thumbnail for the video. | |||||
Supported types are jpg and jpeg. | |||||
By default, prismedia search for an image based on video name followed by .jpg or .jpeg | |||||
--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). | |||||
-h --help Show this help. | |||||
--version Show version. | |||||
Logging options | |||||
-q --quiet Suppress any log except Critical (alias for --log=critical). | |||||
--log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info) | |||||
-u --url-only Display generated URL after upload directly on stdout, implies --quiet | |||||
--batch Display generated URL after upload with platform information for easier parsing. Implies --quiet | |||||
Be careful --batch and --url-only are mutually exclusives. | |||||
--debug (Deprecated) Alias for --log=debug. Ignored if --log is set | |||||
Strict options: | |||||
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not | |||||
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description, | |||||
tags, thumbnail, ... | |||||
All strict option are optionals and are provided only to avoid errors when uploading :-) | |||||
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO | |||||
All strict options are off by default | |||||
--withNFO Prevent the upload without a NFO, either specified via cli or found in the directory | |||||
--withThumbnail Prevent the upload without a thumbnail | |||||
--withName Prevent the upload if no name are found | |||||
--withDescription Prevent the upload without description | |||||
--withTags Prevent the upload without tags | |||||
--withPlaylist Prevent the upload if no playlist | |||||
--withPublishAt Prevent the upload if no schedule | |||||
--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') | |||||
logger.setLevel(logging.INFO) | |||||
ch = logging.StreamHandler() | |||||
ch.setLevel(logging.INFO) | |||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s') | |||||
ch.setFormatter(formatter) | |||||
logger.addHandler(ch) | |||||
from docopt import docopt | |||||
from . 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) | |||||
try: | |||||
# noinspection PyUnresolvedReferences | |||||
import magic | |||||
except ImportError: | |||||
logger.critical('This program requires that the `python-magic` library' | |||||
' is installed, NOT the Python bindings to libmagic API \n' | |||||
'see https://github.com/ahupp/python-magic\n') | |||||
exit(1) | |||||
VERSION = "prismedia v0.11.0" | |||||
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted') | |||||
VALID_CATEGORIES = ( | |||||
"music", "films", "vehicles", | |||||
"sports", "travels", "gaming", "people", | |||||
"comedy", "entertainment", "news", | |||||
"how to", "education", "activism", "science & technology", | |||||
"science", "technology", "animals" | |||||
) | |||||
VALID_PLATFORM = ('youtube', 'peertube', 'none') | |||||
VALID_LANGUAGES = ('arabic', 'english', 'french', | |||||
'german', 'hindi', 'italian', | |||||
'japanese', 'korean', 'mandarin', | |||||
'portuguese', 'punjabi', 'russian', 'spanish') | |||||
VALID_PROGRESS = ('percentage', 'bigfile', 'accurate') | |||||
def validateVideo(path): | |||||
supported_types = ['video/mp4'] | |||||
detected_type = magic.from_file(path, mime=True) | |||||
if detected_type not in supported_types: | |||||
print("File", path, "detected type is", detected_type, "which is not one of", supported_types) | |||||
force_file = ['y', 'yes'] | |||||
is_forcing = input("Are you sure you selected the correct file? (y/N)") | |||||
if is_forcing.lower() not in force_file: | |||||
return False | |||||
return path | |||||
def validateCategory(category): | |||||
if category.lower() in VALID_CATEGORIES: | |||||
return True | |||||
else: | |||||
return False | |||||
def validatePrivacy(privacy): | |||||
if privacy.lower() in VALID_PRIVACY_STATUSES: | |||||
return True | |||||
else: | |||||
return False | |||||
def validatePlatform(platform): | |||||
for plfrm in platform.split(','): | |||||
if plfrm.lower().replace(" ", "") not in VALID_PLATFORM: | |||||
return False | |||||
return True | |||||
def validateLanguage(language): | |||||
if language.lower() in VALID_LANGUAGES: | |||||
return True | |||||
else: | |||||
return False | |||||
def validatePublishDate(publishDate): | |||||
# Check date format and if date is future | |||||
try: | |||||
now = datetime.datetime.now() | |||||
publishAt = datetime.datetime.strptime(publishDate, '%Y-%m-%dT%H:%M:%S') | |||||
if now >= publishAt: | |||||
return False | |||||
except ValueError: | |||||
return False | |||||
return True | |||||
def validateOriginalDate(originalDate): | |||||
# Check date format and if date is past | |||||
try: | |||||
now = datetime.datetime.now() | |||||
originalDate = datetime.datetime.strptime(originalDate, '%Y-%m-%dT%H:%M:%S') | |||||
if now <= originalDate: | |||||
return False | |||||
except ValueError: | |||||
return False | |||||
return True | |||||
def validateThumbnail(thumbnail): | |||||
supported_types = ['image/jpg', 'image/jpeg'] | |||||
if os.path.exists(thumbnail) and \ | |||||
magic.from_file(thumbnail, mime=True) in supported_types: | |||||
return thumbnail | |||||
else: | |||||
return False | |||||
def validateLogLevel(loglevel): | |||||
numeric_level = getattr(logging, loglevel, None) | |||||
if not isinstance(numeric_level, int): | |||||
return False | |||||
return True | |||||
def validateProgress(progress): | |||||
for prgs in progress.split(','): | |||||
if prgs.lower().replace(" ", "") not in VALID_PROGRESS: | |||||
return False | |||||
return True | |||||
def _optionnalOrStrict(key, scope, error): | |||||
option = key.replace('-', '') | |||||
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 | |||||
if options.get('--quiet'): | |||||
# We need to set both log level in the same time | |||||
logger.setLevel(50) | |||||
ch.setLevel(50) | |||||
elif options.get('--log'): | |||||
numeric_level = getattr(logging, options["--log"], None) | |||||
# We need to set both log level in the same time | |||||
logger.setLevel(numeric_level) | |||||
ch.setLevel(numeric_level) | |||||
elif options.get('--debug'): | |||||
logger.warning("DEPRECATION: --debug is deprecated, please use --log=debug instead") | |||||
logger.setLevel(10) | |||||
ch.setLevel(10) | |||||
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, | |||||
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys | |||||
object: object | |||||
}) | |||||
schema = Schema({ | |||||
'--file': And(str, os.path.exists, validateVideo, error='file is not supported, please use mp4'), | |||||
# 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, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'), | |||||
), | |||||
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")), | |||||
'--help': bool, | |||||
'--version': bool, | |||||
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys | |||||
object: object | |||||
}) | |||||
# 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() |
@ -0,0 +1,227 @@ | |||||
#!/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 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" | |||||
# Then, if we still not have thumbnail, check for thumbnail based on videofile name | |||||
if not options.get('--thumbnail'): | |||||
video_file = splitext(basename(options.get('--file')))[0] | |||||
if isfile(video_directory + video_file + ".jpg"): | |||||
options['--thumbnail'] = video_directory + video_file + ".jpg" | |||||
elif isfile(video_directory + video_file + ".jpeg"): | |||||
options['--thumbnail'] = video_directory + video_file + ".jpeg" | |||||
# Display some info after research | |||||
if not options.get('--thumbnail'): | |||||
logger.debug("No thumbnail has been found, continuing") | |||||
else: | |||||
logger.info("Using " + options.get('--thumbnail') + " as thumbnail") | |||||
return options | |||||
def searchOriginalDate(options): | |||||
fileModificationDate = str(getmtime(options.get('--file'))).split('.') | |||||
return datetime.datetime.fromtimestamp(int(fileModificationDate[0])).isoformat() | |||||
# return the nfo as a RawConfigParser object | |||||
def loadNFO(filename): | |||||
try: | |||||
logger.info("Loading " + filename + " as NFO") | |||||
nfo = RawConfigParser() | |||||
nfo.read(filename, encoding='utf-8') | |||||
return nfo | |||||
except Exception as e: | |||||
logger.critical("Problem loading NFO file " + filename + ": " + str(e)) | |||||
exit(1) | |||||
return False | |||||
def parseNFO(options): | |||||
video_directory = dirname(options.get('--file')) | |||||
directory_name = basename(video_directory) | |||||
nfo_txt = False | |||||
nfo_directory = False | |||||
nfo_videoname = False | |||||
nfo_file = False | |||||
nfo_cli = False | |||||
if isfile(video_directory + "/" + "nfo.txt"): | |||||
nfo_txt = loadNFO(video_directory + "/" + "nfo.txt") | |||||
elif isfile(video_directory + "/" + "NFO.txt"): | |||||
nfo_txt = loadNFO(video_directory + "/" + "NFO.txt") | |||||
if isfile(video_directory + "/" + directory_name + ".txt"): | |||||
nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt") | |||||
if options.get('--name'): | |||||
if isfile(video_directory + "/" + options.get('--name')): | |||||
nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt") | |||||
video_file = splitext(basename(options.get('--file')))[0] | |||||
if isfile(video_directory + "/" + video_file + ".txt"): | |||||
nfo_file = loadNFO(video_directory + "/" + video_file + ".txt") | |||||
if options.get('--nfo'): | |||||
if isfile(options.get('--nfo')): | |||||
nfo_cli = loadNFO(options.get('--nfo')) | |||||
else: | |||||
logger.critical("Given NFO file does not exist, please check your path.") | |||||
exit(1) | |||||
# If there is no NFO and strict option is enabled, then stop there | |||||
if options.get('--withNFO'): | |||||
if not isinstance(nfo_cli, RawConfigParser) and \ | |||||
not isinstance(nfo_file, RawConfigParser) and \ | |||||
not isinstance(nfo_videoname, RawConfigParser) and \ | |||||
not isinstance(nfo_directory, RawConfigParser) and \ | |||||
not isinstance(nfo_txt, RawConfigParser): | |||||
logger.critical("You have required the strict presence of NFO but none is found, please use a NFO.") | |||||
exit(1) | |||||
# We need to load NFO in this exact order to keep the priorities | |||||
# options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt | |||||
for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]: | |||||
if nfo: | |||||
# We need to check all options and replace it with the nfo value if not defined (None or False) | |||||
for key, value in options.items(): | |||||
key = key.replace("--", "") | |||||
try: | |||||
# get string options | |||||
if value is None and nfo.get('video', key): | |||||
options['--' + key] = nfo.get('video', key) | |||||
# get boolean options | |||||
elif value is False and nfo.getboolean('video', key): | |||||
options['--' + key] = nfo.getboolean('video', key) | |||||
except NoOptionError: | |||||
continue | |||||
except NoSectionError: | |||||
logger.critical(nfo + " misses section [video], please check syntax of your NFO.") | |||||
exit(1) | |||||
return options | |||||
def upcaseFirstLetter(s): | |||||
return s[0].upper() + s[1:] | |||||
def cleanString(toclean): | |||||
toclean = unidecode.unidecode(toclean) | |||||
cleaned = re.sub('[^A-Za-z0-9]+', '', toclean) | |||||
return cleaned |
@ -0,0 +1,340 @@ | |||||
#!/usr/bin/env python | |||||
# coding: utf-8 | |||||
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa | |||||
import http.client | |||||
import httplib2 | |||||
import random | |||||
import time | |||||
import copy | |||||
import json | |||||
from os.path import splitext, basename, exists | |||||
import os | |||||
import google.oauth2.credentials | |||||
import datetime | |||||
import pytz | |||||
import logging | |||||
from tzlocal import get_localzone | |||||
from googleapiclient.discovery import build | |||||
from googleapiclient.errors import HttpError | |||||
from googleapiclient.http import MediaFileUpload | |||||
from google_auth_oauthlib.flow import InstalledAppFlow | |||||
from . import utils | |||||
logger = logging.getLogger('Prismedia') | |||||
# Explicitly tell the underlying HTTP transport library not to retry, since | |||||
# we are handling retry logic ourselves. | |||||
httplib2.RETRIES = 1 | |||||
# Maximum number of times to retry before giving up. | |||||
MAX_RETRIES = 10 | |||||
# Youtube retriables cases | |||||
RETRIABLE_EXCEPTIONS = ( | |||||
IOError, | |||||
httplib2.HttpLib2Error, | |||||
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" | |||||
SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl'] | |||||
API_SERVICE_NAME = 'youtube' | |||||
API_VERSION = 'v3' | |||||
# Authorize the request and store authorization credentials. | |||||
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) | |||||
credentials = google.oauth2.credentials.Credentials( | |||||
credential_params["token"], | |||||
refresh_token=credential_params["_refresh_token"], | |||||
token_uri=credential_params["_token_uri"], | |||||
client_id=credential_params["_client_id"], | |||||
client_secret=credential_params["_client_secret"] | |||||
) | |||||
else: | |||||
credentials = flow.run_console() | |||||
with open(CREDENTIALS_PATH, 'w') as f: | |||||
p = copy.deepcopy(vars(credentials)) | |||||
del p["expiry"] | |||||
json.dump(p, f) | |||||
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False) | |||||
def check_authenticated_scopes(): | |||||
if exists(CREDENTIALS_PATH): | |||||
with open(CREDENTIALS_PATH, 'r') as f: | |||||
credential_params = json.load(f) | |||||
# Check if all scopes are present | |||||
if credential_params["_scopes"] != SCOPES: | |||||
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 | |||||
if options.get('--tags'): | |||||
tags = options.get('--tags').split(',') | |||||
category = None | |||||
if options.get('--category'): | |||||
category = utils.getCategory(options.get('--category'), 'youtube') | |||||
language = None | |||||
if options.get('--language'): | |||||
language = utils.getLanguage(options.get('--language'), "youtube") | |||||
license = None | |||||
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], | |||||
"description": options.get('--description') or "default description", | |||||
"tags": tags, | |||||
# if no category, set default to 1 (Films) | |||||
"categoryId": str(category or 1), | |||||
"defaultAudioLanguage": str(language or 'en') | |||||
}, | |||||
"status": { | |||||
"privacyStatus": str(options.get('--privacy') or "private"), | |||||
"license": str(license or "youtube"), | |||||
}, | |||||
"recordingDetails": { | |||||
} | |||||
} | |||||
# 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: | |||||
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(list(body.keys())), | |||||
body=body, | |||||
media_body=MediaFileUpload(path, chunksize=-1, resumable=True) | |||||
) | |||||
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(options, youtube, options.get('--thumbnail'), videoId=video_id) | |||||
# 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'] | |||||
def create_playlist(youtube, 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'}) | |||||
response = youtube.playlists().insert( | |||||
body=resources, | |||||
part='status,snippet,id' | |||||
).execute() | |||||
return response["id"] | |||||
def build_resource(properties): | |||||
resource = {} | |||||
for p in properties: | |||||
# Given a key like "snippet.title", split into "snippet" and "title", where | |||||
# "snippet" will be an object and "title" will be a property in that object. | |||||
prop_array = p.split('.') | |||||
ref = resource | |||||
for pa in range(0, len(prop_array)): | |||||
is_array = False | |||||
key = prop_array[pa] | |||||
# For properties that have array values, convert a name like | |||||
# "snippet.tags[]" to snippet.tags, and set a flag to handle | |||||
# the value as an array. | |||||
if key[-2:] == '[]': | |||||
key = key[0:len(key)-2:] | |||||
is_array = True | |||||
if pa == (len(prop_array) - 1): | |||||
# Leave properties without values out of inserted resource. | |||||
if properties[p]: | |||||
if is_array: | |||||
ref[key] = properties[p].split(',') | |||||
else: | |||||
ref[key] = properties[p] | |||||
elif key not in ref: | |||||
# For example, the property is "snippet.title", but the resource does | |||||
# not yet have a "snippet" object. Create the snippet object here. | |||||
# Setting "ref = ref[key]" means that in the next time through the | |||||
# "for pa in range ..." loop, we will be setting a property in the | |||||
# resource's "snippet" object. | |||||
ref[key] = {} | |||||
ref = ref[key] | |||||
else: | |||||
# For example, the property is "snippet.description", and the resource | |||||
# already has a "snippet" object. | |||||
ref = ref[key] | |||||
return resource | |||||
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, | |||||
resumable=True), | |||||
**kwargs | |||||
) | |||||
return resumable_upload(request, 'thumbnail', 'set', options) | |||||
def set_playlist(youtube, playlist_id, video_id): | |||||
logger.info('Youtube: Configuring playlist...') | |||||
resource = build_resource({'snippet.playlistId': playlist_id, | |||||
'snippet.resourceId.kind': 'youtube#video', | |||||
'snippet.resourceId.videoId': video_id, | |||||
'snippet.position': ''} | |||||
) | |||||
try: | |||||
youtube.playlistItems().insert( | |||||
body=resource, | |||||
part='snippet' | |||||
).execute() | |||||
except Exception as e: | |||||
if hasattr(e, 'message'): | |||||
logger.critical("Youtube: " + str(e.message)) | |||||
exit(1) | |||||
else: | |||||
logger.critical("Youtube: " + str(e)) | |||||
exit(1) | |||||
logger.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, 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...' | |||||
logger.info(template % resource) | |||||
status, response = request.next_chunk() | |||||
if response is not None: | |||||
if method == 'insert' and 'id' in response: | |||||
logger.info('Youtube : Video was successfully uploaded.') | |||||
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)' | |||||
logger.info(template % response['id']) | |||||
template_stdout = 'https://youtu.be/%s' | |||||
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: | |||||
logger.info('Youtube: Thumbnail was successfully set.') | |||||
else: | |||||
template = ('Youtube : The upload failed with an ' | |||||
'unexpected response: %s') | |||||
logger.critical(template % response) | |||||
exit(1) | |||||
except HttpError as e: | |||||
if e.resp.status in RETRIABLE_STATUS_CODES: | |||||
template = 'Youtube : A retriable HTTP error %d occurred:\n%s' | |||||
error = template % (e.resp.status, e.content) | |||||
else: | |||||
raise | |||||
except RETRIABLE_EXCEPTIONS as e: | |||||
error = 'Youtube : A retriable error occurred: %s' % e | |||||
if error is not None: | |||||
logger.warning(error) | |||||
retry += 1 | |||||
if retry > MAX_RETRIES: | |||||
logger.error('Youtube : No longer attempting to retry.') | |||||
max_sleep = 2 ** retry | |||||
sleep_seconds = random.random() * max_sleep | |||||
logger.warning('Youtube : Sleeping %f seconds and then retrying...' | |||||
% sleep_seconds) | |||||
time.sleep(sleep_seconds) | |||||
def run(options): | |||||
youtube = get_authenticated_service() | |||||
try: | |||||
initialize_upload(youtube, options) | |||||
except HttpError as e: | |||||
logger.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status, | |||||
e.content)) |
@ -1,231 +0,0 @@ | |||||
#!/usr/bin/python | |||||
# coding: utf-8 | |||||
""" | |||||
prismedia_upload - tool to upload videos to Peertube and Youtube | |||||
Usage: | |||||
prismedia_upload.py --file=<FILE> [options] | |||||
prismedia_upload.py --file=<FILE> --tags=STRING [--mt options] | |||||
prismedia_upload.py -h | --help | |||||
prismedia_upload.py --version | |||||
Options: | |||||
--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 | |||||
-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.5" | |||||
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'), | |||||
), | |||||
'--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) |
@ -0,0 +1,49 @@ | |||||
[tool.poetry] | |||||
name = "prismedia" | |||||
version = "0.11.0" | |||||
description = "scripting your way to upload videos on peertube and youtube" | |||||
authors = [ | |||||
"LecygneNoir <git@lecygnenoir.info>", | |||||
"Rigel Kent <sendmemail@rigelk.eu>", | |||||
"Zykino" | |||||
] | |||||
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.5" | |||||
clint = "^0.5.1" | |||||
configparser = "^3.7.1" | |||||
docopt = "^0.6.2" | |||||
future = "^0.17.1" | |||||
google-api-python-client = ">=1.7.6" | |||||
google-auth = ">=1.6.1" | |||||
google-auth-httplib2 = ">=0.0.3" | |||||
google-auth-oauthlib = ">=0.2.0" | |||||
httplib2 = "^0.12.1" | |||||
oauthlib = "^2.1.0" | |||||
python-magic = "^0.4.15" | |||||
python-magic-bin = { version = "^0.4.14", markers = "platform_system == 'Windows'" } | |||||
requests = "^2.18.4" | |||||
requests-oauthlib = "^0.8.0" | |||||
requests-toolbelt = "^0.9.1" | |||||
schema = ">=0.7.1" | |||||
tzlocal = "^1.5.1" | |||||
Unidecode = "^1.0.23" | |||||
uritemplate = "^3.0.0" | |||||
urllib3 = "^1.22" | |||||
[tool.poetry.dev-dependencies] | |||||
[tool.poetry.scripts] | |||||
prismedia = 'prismedia.upload:main' | |||||
[build-system] | |||||
requires = ["poetry>=0.12"] | |||||
build-backend = "poetry.masonry.api" |
@ -0,0 +1,130 @@ | |||||
cachetools==3.1.1 \ | |||||
--hash=sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae \ | |||||
--hash=sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a | |||||
certifi==2020.4.5.1 \ | |||||
--hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \ | |||||
--hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519 | |||||
chardet==3.0.4 \ | |||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \ | |||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae | |||||
clint==0.5.1 | |||||
configparser==3.8.1 \ | |||||
--hash=sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18 \ | |||||
--hash=sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17 | |||||
docopt==0.6.2 \ | |||||
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491 | |||||
future==0.17.1 \ | |||||
--hash=sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8 | |||||
google-api-core==1.16.0 \ | |||||
--hash=sha256:92e962a087f1c4b8d1c5c88ade1c1dfd550047dcffb320c57ef6a534a20403e2 \ | |||||
--hash=sha256:859f7392676761f2b160c6ee030c3422135ada4458f0948c5690a6a7c8d86294 | |||||
google-api-python-client==1.8.0 \ | |||||
--hash=sha256:0f5b42a14e2d2f7dee40f2e4514531dbe95ebde9c2173b1c4040a65c427e7900 \ | |||||
--hash=sha256:5032ad1af5046889649b3848f2e871889fbb6ae440198a549fe1699581300386 | |||||
google-auth==1.13.1 \ | |||||
--hash=sha256:a5ee4c40fef77ea756cf2f1c0adcf475ecb53af6700cf9c133354cdc9b267148 \ | |||||
--hash=sha256:cab6c707e6ee20e567e348168a5c69dc6480384f777a9e5159f4299ad177dcc0 | |||||
google-auth-httplib2==0.0.3 \ | |||||
--hash=sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445 \ | |||||
--hash=sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08 | |||||
google-auth-oauthlib==0.2.0 \ | |||||
--hash=sha256:226d1d0960f86ba5d9efd426a70b291eaba96f47d071657e0254ea969025728a \ | |||||
--hash=sha256:81ba22acada4d13b1d83f9371ab19fd61f1250a542d21cf49e4dcf0637a7344a | |||||
googleapis-common-protos==1.51.0 \ | |||||
--hash=sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e | |||||
httplib2==0.12.3 \ | |||||
--hash=sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8 \ | |||||
--hash=sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600 | |||||
idna==2.9 \ | |||||
--hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa \ | |||||
--hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb | |||||
oauthlib==2.1.0 \ | |||||
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b \ | |||||
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162 | |||||
protobuf==3.11.3 \ | |||||
--hash=sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481 \ | |||||
--hash=sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7 \ | |||||
--hash=sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a \ | |||||
--hash=sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961 \ | |||||
--hash=sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80 \ | |||||
--hash=sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306 \ | |||||
--hash=sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a \ | |||||
--hash=sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151 \ | |||||
--hash=sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab \ | |||||
--hash=sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956 \ | |||||
--hash=sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2 \ | |||||
--hash=sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07 \ | |||||
--hash=sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a \ | |||||
--hash=sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee \ | |||||
--hash=sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4 \ | |||||
--hash=sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0 \ | |||||
--hash=sha256:2affcaba328c4662f3bc3c0e9576ea107906b2c2b6422344cdad961734ff6b93 \ | |||||
--hash=sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f \ | |||||
--hash=sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f | |||||
pyasn1==0.4.8 \ | |||||
--hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \ | |||||
--hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \ | |||||
--hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \ | |||||
--hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \ | |||||
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \ | |||||
--hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \ | |||||
--hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \ | |||||
--hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \ | |||||
--hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \ | |||||
--hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \ | |||||
--hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \ | |||||
--hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \ | |||||
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba | |||||
pyasn1-modules==0.2.8 \ | |||||
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \ | |||||
--hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \ | |||||
--hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \ | |||||
--hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \ | |||||
--hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \ | |||||
--hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \ | |||||
--hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \ | |||||
--hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \ | |||||
--hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \ | |||||
--hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \ | |||||
--hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \ | |||||
--hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \ | |||||
--hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd | |||||
python-magic==0.4.15 \ | |||||
--hash=sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5 \ | |||||
--hash=sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375 | |||||
python-magic-bin==0.4.14; platform_system == "Windows" \ | |||||
--hash=sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4 \ | |||||
--hash=sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892 \ | |||||
--hash=sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69 | |||||
pytz==2019.3 \ | |||||
--hash=sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d \ | |||||
--hash=sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be | |||||
requests==2.23.0 \ | |||||
--hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \ | |||||
--hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6 | |||||
requests-oauthlib==0.8.0 \ | |||||
--hash=sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468 \ | |||||
--hash=sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca | |||||
requests-toolbelt==0.9.1 \ | |||||
--hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 \ | |||||
--hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f | |||||
rsa==4.0 \ | |||||
--hash=sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66 \ | |||||
--hash=sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487 | |||||
schema==0.6.8 \ | |||||
--hash=sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687 \ | |||||
--hash=sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74 | |||||
six==1.14.0 \ | |||||
--hash=sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c \ | |||||
--hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a | |||||
tzlocal==1.5.1 \ | |||||
--hash=sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e | |||||
unidecode==1.1.1 \ | |||||
--hash=sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a \ | |||||
--hash=sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8 | |||||
uritemplate==3.0.1 \ | |||||
--hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \ | |||||
--hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae | |||||
urllib3==1.22 \ | |||||
--hash=sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b \ | |||||
--hash=sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f |