go in the prismedia directory, and launch `python core.py`plugins
@ -0,0 +1,13 @@ | |||
For our plugins we are using [yaspy](http://yapsy.sourceforge.net). | |||
# Types | |||
For an example of the exact methods required to be recognized as a particular type of plugin, see the concerned interface definition. | |||
## Interface | |||
Plugins that present an interface (cli, gui, configuration folders, …) for the user to tell Prismedia wich video needs to be uploaded, the infos of the videos, … | |||
## Platform | |||
Also called uploaders, they are the one doing the actual work of uploading video to a particular platform. | |||
## Consumer | |||
Thoses do actions once the upload is finished (successful or failed). |
@ -0,0 +1,62 @@ | |||
from yapsy.PluginManager import PluginManager | |||
import pluginInterfaces as pi | |||
import logging | |||
# logging.basicConfig(level=logging.DEBUG) | |||
def loadPlugins(type): | |||
# Load the plugins from the plugin directory. | |||
# TODO: subdirectories too? | |||
manager = PluginManager() | |||
manager.setPluginPlaces(["plugins"]) # TODO: Generate the absolute path | |||
# Define the various categories corresponding to the different | |||
# kinds of plugins you have defined | |||
manager.setCategoriesFilter({ | |||
"Interface" : pi.IInterfacePlugin, | |||
"Platform" : pi.IPlatformPlugin, | |||
}) | |||
manager.collectPlugins() | |||
# Loop round the plugins and print their names. | |||
print("debug") | |||
print(manager.getAllPlugins()) | |||
print("all plugins") | |||
for plugin in manager.getAllPlugins(): | |||
plugin.plugin_object.print_name() | |||
print("Category: Interface") | |||
for plugin in manager.getPluginsOfCategory("Interface"): | |||
plugin.plugin_object.print_name() | |||
print("Category: Platform") | |||
for plugin in manager.getPluginsOfCategory("Platform"): | |||
plugin.plugin_object.print_name() | |||
# discovered_plugins = { | |||
# name: importlib.import_module(name) | |||
# for finder, name, ispkg | |||
# in pkgutil.iter_modules(["/home/zykino/Documents/0DocPerso/Code/prismedia/plugins"]) | |||
# if name.startswith("prismedia_" + type + "_") | |||
# } | |||
#def test_loadPlugins(arg): | |||
platforms = loadPlugins("platform") | |||
print (platforms) | |||
def startInterface(): | |||
interface = loadPlugins("interface") | |||
options = interface["default"].run() | |||
if options.get('--interface'): | |||
if interface[options.get('--interface')]: | |||
options = interface[options.get('--interface')].run(options) | |||
else: | |||
options = interface["cli"].run(options) | |||
options = interface["nfo"].run(options) | |||
def uploadToPlatforms(options): | |||
platforms = loadPlugins("platform") | |||
for platform in options.get('--platform'): | |||
platforms[platform].run(options) |
@ -0,0 +1,60 @@ | |||
from yapsy.IPlugin import IPlugin | |||
### | |||
# Interface | |||
### | |||
# TODO: The interface is not thought out yet | |||
class IInterfacePlugin(IPlugin): | |||
""" | |||
Interface for the Interface plugin category. | |||
""" | |||
def getOptions(self, args): | |||
""" | |||
Returns the options user has set. | |||
- `args` the command line arguments passed to Prismedia | |||
""" | |||
raise NotImplementedError("`getOptions` must be reimplemented by %s" % self) | |||
### | |||
# Platform | |||
### | |||
class IPlatformPlugin(IPlugin): | |||
""" | |||
Interface for the Platform plugin category. | |||
""" | |||
# def dryrun(self, video, options): | |||
# """ | |||
# Simulate an upload but without really uploading anything. | |||
# """ | |||
# raise NotImplementedError("`dryrun` must be reimplemented by %s" % self) | |||
def hearthbeat(self): | |||
""" | |||
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use. | |||
""" | |||
raise NotImplementedError("`hearthbeat` must be reimplemented by %s" % self) | |||
def upload(self, video, options): | |||
""" | |||
The upload function | |||
""" | |||
raise NotImplementedError("`upload` must be reimplemented by %s" % self) | |||
### | |||
# Consumer | |||
### | |||
# TODO: The interface is not thought out yet | |||
class IConsumerPlugin(IPlugin): | |||
""" | |||
Interface for the Consumer plugin category. | |||
""" | |||
def finished(self, video): | |||
""" | |||
What to do once the uploads are done. | |||
- `video` is an object containing the video details. The `platforms` key contain a list of the platforms the video has been uploaded to and the status | |||
""" | |||
raise NotImplementedError("`getOptions` must be reimplemented by %s" % self) |
@ -0,0 +1,447 @@ | |||
#!/usr/bin/env python | |||
# coding: utf-8 | |||
import os | |||
import mimetypes | |||
import json | |||
import logging | |||
import sys | |||
import datetime | |||
import pytz | |||
import pluginInterfaces as pi | |||
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 | |||
import utils | |||
logger = logging.getLogger('Prismedia') | |||
PEERTUBE_SECRETS_FILE = 'peertube_secret' | |||
PEERTUBE_PRIVACY = { | |||
"public": 1, | |||
"unlisted": 2, | |||
"private": 3 | |||
} | |||
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 | |||
} | |||
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" | |||
} | |||
class Peertube(pi.IPlatformPlugin): | |||
"""docstring for Peertube.""" | |||
def print_name(self): | |||
print("This is plugin peertube") | |||
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(CATEGORY[options.get('--category').lower()]))) | |||
else: | |||
# if no category, set default to 2 (Films) | |||
fields.append(("category", "2")) | |||
if options.get('--language'): | |||
fields.append(("language", str(LANGUAGE[options.get('--language').lower()]))) | |||
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 hearthbeat(self): | |||
""" | |||
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use. | |||
""" | |||
pass | |||
def upload(self, video, options): | |||
# 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,9 @@ | |||
[Core] | |||
Name = Peertube | |||
Module = peertube | |||
[Documentation] | |||
Author = Le Cygne Noir | |||
Version = 0.1 | |||
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia | |||
Description = Upload to the peertube platform |
@ -1,398 +0,0 @@ | |||
#!/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)) |