diff --git a/README.md b/README.md index 03c9de6..e82af17 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ poetry install Generate configuration files by running `prismedia-init`. -Then, edit them to fill your credential as explained below. +Then, edit them to fill your credential as explained below. ### Peertube Configuration is in **peertube_secret** file. @@ -64,8 +64,6 @@ You need your usual credentials and Peertube instance URL, in addition with API You can get client_id and client_secret by logging in your peertube instance and reaching the URL: https://domain.example/api/v1/oauth-clients/local -*Alternatively, you can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)* - ### Youtube Configuration is in **youtube_secret.json** file. Youtube uses combination of oauth and API access to identify. @@ -233,4 +231,4 @@ Available strict options: Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload) ## Contributors -Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘 \ No newline at end of file +Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘 diff --git a/prismedia/cli.py b/prismedia/cli.py index d40ab26..368d473 100644 --- a/prismedia/cli.py +++ b/prismedia/cli.py @@ -26,10 +26,13 @@ def parseOptions(options): video.name = utils.getOption(options, "--name", video.name) video.description = utils.getOption(options, "--description", video.description) video.playlistName = utils.getOption(options, "--playlist", video.playlistName) - video.privacy = utils.getOption(options, "--privacy", video.privacy) - video.category = utils.getOption(options, "--category", video.category) - video.tags = utils.getOption(options, "--tag", video.tags) - video.language = utils.getOption(options, "--language", video.language) + video.privacy = utils.getOption(options, "--privacy", video.privacy).lower() + video.category = utils.getOption(options, "--category", video.category).lower() + tags = utils.getOption(options, "--tag", video.tags) + if isinstance(tags, str): + tags = tags.split(",") + video.tags = tags + video.language = utils.getOption(options, "--language", video.language).lower() video.originalDate = utils.getOption(options, "--original-date", video.originalDate) # TODO: set as an object: { "all": date1, "platformX": date2, …}? # Maybe the publishAt by platform is better placed in `self.platform` diff --git a/prismedia/core.py b/prismedia/core.py index c82909c..ad275db 100644 --- a/prismedia/core.py +++ b/prismedia/core.py @@ -4,6 +4,7 @@ # NOTE: Since we use config file to set some defaults values, it is not possible to use the standard syntax with brackets, we use parenthesis instead. # If we were to use them we would override configuration file values with default values of cli. # TODO: change `youtube-at` and `peertube-at` that are not easely expendable as options in my opinion +# TODO: remove `--url-only` and `--batch` """ prismedia - tool to upload videos to different platforms (historicaly Peertube and Youtube) @@ -105,6 +106,7 @@ Languages: import cli import pluginInterfaces as pi +import utils import video as vid from docopt import docopt @@ -132,7 +134,10 @@ def loadPlugins(basePluginsPath): pluginManager.collectPlugins() +# TODO: cut this function into smaller ones def main(): + logger = logging.getLogger('Prismedia') + basePluginsPath = [os.path.dirname(os.path.abspath(__file__)) + "/plugins"] loadPlugins(basePluginsPath) pluginManager = PluginManagerSingleton.get() @@ -147,32 +152,60 @@ def main(): video = cli.parseOptions(options) if options[""]: interface = pluginManager.getPluginByName(options[""], pi.PluginTypes.INTERFACE) - if not interface.plugin_object.prepareOptions(video, options): - # The plugin asked to stop execution. - exit(os.EX_OK) + try: + if not interface.plugin_object.prepare_options(video, options): + # The plugin asked to stop execution. + exit(os.EX_OK) + except Exception as e: + logger.critical(utils.get_exception_string(e)) + exit(os.EX_CONFIG) - if options["--platform"]: - platforms = pluginManager.getPluginsOf(categories=pi.PluginTypes.PLATFORM, name=[options["--platform"].split(",")]) + list = utils.getOption(options, "--platform", []) + if list: + platforms = pluginManager.getPluginsOf(categories=pi.PluginTypes.PLATFORM, name=[list.split(",")]) else: platforms = pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM) - if options["--consumer"]: - consumers = pluginManager.getPluginsOf(categories=pi.PluginTypes.CONSUMER, name=[options["--consumer"].split(",")]) + list = utils.getOption(options, "--consumer", None) + if list: + consumers = pluginManager.getPluginsOf(categories=pi.PluginTypes.CONSUMER, name=[list.split(",")]) else: consumers = pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER) # Let each plugin check its options before starting any upload - for plugin in [*platforms, *consumers]: - print("DEBUG:", plugin.name) + # We cannot merge this loop with the one from interface since the interface can change which plugin to use + # We need to create each platform object in video, so we cannot merge this loop with the following one + for plugin in platforms: + # TODO: Check this is needed or not: in case of no plugin or wrong name maybe the list is empty instead of there being a None value + if plugin is None: + # TODO: log instead to error ? critical ? + print("No plugin installed name `" + plugin.name + "`.") + exit(os.EX_USAGE) + + try: + video.platform[plugin.name] = vid.Platform() + if not plugin.plugin_object.prepare_options(video, options): + # A plugin found ill formed options, it should have logged the precises infos + print(plugin.name + " found a malformed option.") + exit(os.EX_CONFIG) + except Exception as e: + logger.critical(utils.get_exception_string(e)) + exit(os.EX_CONFIG) + + for plugin in consumers: # TODO: Check this is needed or not: in case of no plugin or wrong name maybe the list is empty instead of there being a None value if plugin is None: # TODO: log instead to error ? critical ? print("No plugin installed name `" + plugin.name + "`.") exit(os.EX_USAGE) - if not plugin.plugin_object.prepareOptions(video, options): - # A plugin found ill formed options, it should have logged the precises infos - print(plugin.name + " found a malformed option.") + try: + if not plugin.plugin_object.prepare_options(video, options): + # A plugin found ill formed options, it should have logged the precises infos + print(plugin.name + " found a malformed option.") + exit(os.EX_CONFIG) + except Exception as e: + logger.critical(utils.get_exception_string(e)) exit(os.EX_CONFIG) if video.path == "": @@ -184,13 +217,19 @@ def main(): for platform in platforms: print("Uploading to: " + platform.name) - platform.plugin_object.upload(video) + try: + platform.plugin_object.upload(video, options) + except Exception as e: # TODO: Maybe not catch every Exception? + logger.critical(utils.get_exception_string(e)) + video.platform[platform.name].error = e + video.platform[platform.name].publishAt = None + video.platform[platform.name].url = None print("All uploads have been done, calling consumers plugins") for consumer in consumers: - print("Calling consumer: " + platform.name) - consumer.plugin_object.finished(video) + print("Calling consumer: " + consumer.name) + consumer.plugin_object.finished(video, options) main() diff --git a/prismedia/pluginInterfaces.py b/prismedia/pluginInterfaces.py index ec90d44..a244ff7 100644 --- a/prismedia/pluginInterfaces.py +++ b/prismedia/pluginInterfaces.py @@ -13,7 +13,7 @@ class IPrismediaBasePlugin(IPlugin): Base for prismedia’s plugin. """ - def prepareOptions(self, video, options): + def prepare_options(self, video, options): """ Return a falsy value to exit the program. - `video`: video object to be uploaded @@ -47,7 +47,7 @@ class IPlatformPlugin(IPrismediaBasePlugin): """ raise NotImplementedError("`hearthbeat` must be reimplemented by %s" % self) - def upload(self, video): + def upload(self, video, options): """ The upload function """ @@ -62,7 +62,7 @@ class IConsumerPlugin(IPrismediaBasePlugin): Interface for the Consumer plugin category. """ - def finished(self, video): + def finished(self, video, options): """ 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 diff --git a/prismedia/plugins/interfaces/help.py b/prismedia/plugins/interfaces/help.py index a8452a0..ed8a559 100644 --- a/prismedia/plugins/interfaces/help.py +++ b/prismedia/plugins/interfaces/help.py @@ -9,7 +9,7 @@ class Help(pi.IInterfacePlugin): For example `prismedia help help` bring this help. """ - def prepareOptions(self, video, options): + def prepare_options(self, video, options): pluginManager = PluginManagerSingleton.get() if options[""]: diff --git a/prismedia/plugins/platforms/peertube.py b/prismedia/plugins/platforms/peertube.py index 8f337fb..7d86ba6 100644 --- a/prismedia/plugins/platforms/peertube.py +++ b/prismedia/plugins/platforms/peertube.py @@ -1,6 +1,10 @@ #!/usr/bin/env python # coding: utf-8 +import pluginInterfaces as pi +import utils +import video as vid + import os import mimetypes import json @@ -8,124 +12,125 @@ import logging import sys import datetime import pytz -import pluginInterfaces as pi -from os.path import splitext, basename, abspath +from os.path import splitext, basename, abspath # TODO: remove me, we already import `os` or at least choose one 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 oauthlib.oauth2 import LegacyApplicationClient 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): """ Plugin to upload to the Peertube platform. The connetions files should be set as # TODO: EXPLAIN HOW TO SETUP THE SECRET FILES - `publish-at-peertube=DATE`: overrides the default `publish-at=DATE` for this platform. # TODO: Maybe we will use a [] section on the config fire, explain that. """ - - def prepareOptions(self, video, options): + SECRETS_FILE = 'peertube_secret' + 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" + } + + def __init__(self): + self.channelCreate = False + self.name = "peertube" # TODO: find if it is possible to get the plugin’s name from inside the plugin + self.oauth = {} + self.secret = {} + + def prepare_options(self, video, options): # TODO: get the `publish-at-peertube=DATE` option + # TODO: get the `channel` and `channel-create` options + video.platform[self.name].channel = "" + + self.secret = RawConfigParser() + self.secret.read(self.SECRETS_FILE) + self.get_authenticated_service() + return True - def get_authenticated_service(secret): - peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/") + def get_authenticated_service(self): + instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip("/") oauth_client = LegacyApplicationClient( - client_id=str(secret.get('peertube', 'client_id')) + client_id=str(self.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 + self.oauth = OAuth2Session(client=oauth_client) + self.oauth.fetch_token( + token_url=str(instance_url + '/api/v1/users/token'), + # lower as peertube does not store uppercase for pseudo + username=str(self.secret.get('peertube', 'username').lower()), + password=str(self.secret.get('peertube', 'password')), + client_id=str(self.secret.get('peertube', 'client_id')), + client_secret=str(self.secret.get('peertube', 'client_secret')) + ) + + + def convert_peertube_date(self, 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 get_default_channel(user_info): + def get_default_channel(self, user_info): return user_info['videoChannels'][0]['id'] - def get_channel_by_name(user_info, options): + def get_channel_by_name(self, user_info, video): for channel in user_info["videoChannels"]: - if channel['displayName'] == options.get('--channel'): + if channel['displayName'] == video.platform[self.name].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): + def create_channel(self, instance_url, video): template = ('Peertube: Channel %s does not exist, creating it.') - logger.info(template % (str(options.get('--channel')))) - channel_name = utils.cleanString(str(options.get('--channel'))) + logger.info(template % (video.platform[self.name].channel)) + channel_name = utils.cleanString(video.platform[self.name].channel) # Peertube allows 20 chars max for channel name channel_name = channel_name[:19] data = '{"name":"' + channel_name + '", \ - "displayName":"' + options.get('--channel') + '", \ + "displayName":"' + video.platform[self.name].channel + '", \ "description":null, \ "support":null}' @@ -133,14 +138,12 @@ class Peertube(pi.IPlatformPlugin): 'Content-Type': "application/json; charset=UTF-8" } try: - response = oauth.post(url + "/api/v1/video-channels/", + response = self.oauth.post(instance_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)) + logger.error("Peertube: " + utils.get_exception_string(e)) + if response is not None: if response.status_code == 200: jresponse = response.json() @@ -163,42 +166,41 @@ class Peertube(pi.IPlatformPlugin): return user_info['videoChannels'][0]['id'] - def get_playlist_by_name(oauth, url, username, options): + def get_playlist_by_name(instance_url, username, video): start = 0 - user_playlists = json.loads(oauth.get( - url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content) + user_playlists = json.loads(self.oauth.get( + instance_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'): + if playlist['displayName'] == video.playlistName: 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) + user_playlists = json.loads(self.oauth.get( + instance_url + "/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content) data = user_playlists["data"] - def create_playlist(oauth, url, options, channel): + def create_playlist(instance_url, video, channel): template = ('Peertube: Playlist %s does not exist, creating it.') - logger.info(template % (str(options.get('--playlist')))) + logger.info(template % (str(video.playlistName))) # 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'))), + files = {'displayName': (None, str(video.playlistName)), 'privacy': (None, "1"), 'description': (None, "null"), 'videoChannelId': (None, str(channel)), 'thumbnailfile': (None, "null")} + try: - response = oauth.post(url + "/api/v1/video-playlists/", + response = self.oauth.post(instance_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)) + logger.error("Peertube: " + utils.get_exception_string(e)) + if response is not None: if response.status_code == 200: jresponse = response.json() @@ -210,22 +212,21 @@ class Peertube(pi.IPlatformPlugin): exit(1) - def set_playlist(oauth, url, video_id, playlist_id): + def set_playlist(instance_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", + response = self.oauth.post(instance_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)) + logger.error("Peertube: " + utils.get_exception_string(e)) + if response is not None: if response.status_code == 200: logger.info('Peertube: Video is successfully added to the playlist.') @@ -235,133 +236,108 @@ class Peertube(pi.IPlatformPlugin): exit(1) - def upload_video(oauth, secret, options): + def upload_video(self, video, options): - def get_userinfo(): - return json.loads(oauth.get(url+"/api/v1/users/me").content) + def get_userinfo(instance_url): + return json.loads(self.oauth.get(instance_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()) + path = video.path + instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip('/') + user_info = get_userinfo(instance_url) + username = str(self.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')), - ("licence", "1"), - ("description", options.get('--description') or "default description"), - ("nsfw", str(int(options.get('--nsfw')) or "0")), + ("name", video.name), + ("licence", "1"), # TODO: get licence from video object + ("description", video.description), + ("category", str(self.CATEGORY[video.category])), + ("language", str(self.LANGUAGE[video.language])), + ("commentsEnabled", "0" if video.disableComments else "1"), + ("nsfw", "1" if video.nsfw else "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() + tag_number = 0 + for strtag in video.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 peertubeAt exists, use instead of publishAt - if options.get('--peertubeAt'): - publishAt = options.get('--peertubeAt') - elif options.get('--publishAt'): - publishAt = options.get('--publishAt') + if video.platform[self.name].publishAt: + publishAt = video.platform[self.name].publishAt + elif video.publishAt: + publishAt = video.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"]))) + fields.append(("scheduleUpdate[privacy]", str(self.PRIVACY["public"]))) + fields.append(("privacy", str(self.PRIVACY["private"]))) else: - fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"]))) + fields.append(("privacy", str(self.PRIVACY[video.privacy]))) - # Set originalDate except if the user force no originalDate - if options.get('--originalDate'): - originalDate = convert_peertube_date(options.get('--originalDate')) + if video.originalDate: + originalDate = convert_peertube_date(video.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 video.thumbnail: + fields.append(("thumbnailfile", get_file(video.thumbnail))) + fields.append(("previewfile", get_file(video.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) + if hasattr(video.platform[self.name], "channel"): # TODO: Should always be present + channel_id = self.get_channel_by_name(user_info, video) + if not channel_id and self.channelCreate: + channel_id = self.create_channel(instance_url, video) elif not channel_id: - logger.warning("Peertube: Channel `" + options.get('--channel') + "` is unknown, using default channel.") - channel_id = get_default_channel(user_info) + logger.warning("Peertube: Channel `" + video.platform[self.name].channel + "` is unknown, using default channel.") # TODO: debate if we should have the same message and behavior than playlist : "does not exist, please set --channelCreate" + channel_id = self.get_default_channel(user_info) else: - channel_id = get_default_channel(user_info) + channel_id = self.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) + if video.playlistName: + playlist_id = get_playlist_by_name(instance_url, username, video) + if not playlist_id and video.playlistCreate: + playlist_id = create_playlist(instance_url, video, channel_id) elif not playlist_id: - logger.critical("Peertube: Playlist `" + options.get('--playlist') + "` does not exist, please set --playlistCreate" + logger.critical("Peertube: Playlist `" + video.playlistName + "` 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) + # 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", + response = self.oauth.post(instance_url + "/api/v1/videos/upload", data=multipart_data, headers=headers) @@ -372,57 +348,53 @@ class Peertube(pi.IPlatformPlugin): 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)) + logger.info("Peertube: Video was successfully uploaded.") + template_url = "%s/videos/watch/%s" + video.platform[self.name].url = template_url % (instance_url, uuid) + logger.info("Peertube: Watch it at " + video.platform[self.name].url + ".") # Upload is successful we may set playlist - if options.get('--playlist'): - set_playlist(oauth, url, video_id, playlist_id) + if 'playlist_id' in locals(): + set_playlist(instance_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 + # 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): @@ -435,20 +407,5 @@ class Peertube(pi.IPlatformPlugin): 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)) + logger.info('Peertube: Uploading video...') + self.upload_video(video, options) diff --git a/prismedia/plugins/platforms/peertube.yapsy-plugin b/prismedia/plugins/platforms/peertube.yapsy-plugin index bc5698f..6b9c14e 100644 --- a/prismedia/plugins/platforms/peertube.yapsy-plugin +++ b/prismedia/plugins/platforms/peertube.yapsy-plugin @@ -7,3 +7,5 @@ Author = Le Cygne Noir Version = 0.1 Website = https://git.lecygnenoir.info/LecygneNoir/prismedia Description = Upload to the peertube platform + + **NOT SECURE:** If the peertube instance you want to upload to has no SSL certificate (http but not https), you can set the environment variable `OAUTHLIB_INSECURE_TRANSPORT=1`. Keep in mind that using this option makes your credentials vulnerable to interception by a malicious 3rd party. Use this only with dummy credential on a test instance. diff --git a/prismedia/plugins/platforms/youtube.py b/prismedia/plugins/platforms/youtube.py index 5b084d0..04ff19b 100644 --- a/prismedia/plugins/platforms/youtube.py +++ b/prismedia/plugins/platforms/youtube.py @@ -314,12 +314,8 @@ def set_playlist(youtube, playlist_id, video_id): 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.critical("Youtube: " + utils.get_exception_string(e)) + exit(1) logger.info('Youtube: Video is correctly added to the playlist.') diff --git a/prismedia/utils.py b/prismedia/utils.py index 3f91394..25970ef 100644 --- a/prismedia/utils.py +++ b/prismedia/utils.py @@ -8,10 +8,9 @@ import unidecode import logging import datetime -logger = logging.getLogger('Prismedia') +logger = logging.getLogger("Prismedia") - -VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted') +VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") VALID_CATEGORIES = ( "music", "films", "vehicles", "sports", "travels", "gaming", "people", @@ -19,20 +18,25 @@ VALID_CATEGORIES = ( "how to", "education", "activism", "science & technology", "science", "technology", "animals" ) -VALID_LANGUAGES = ('arabic', 'english', 'french', - 'german', 'hindi', 'italian', - 'japanese', 'korean', 'mandarin', - 'portuguese', 'punjabi', 'russian', 'spanish') -VALID_PROGRESS = ('percentage', 'bigfile', 'accurate') - +VALID_LANGUAGES = ("arabic", "english", "french", + "german", "hindi", "italian", + "japanese", "korean", "mandarin", + "portuguese", "punjabi", "russian", "spanish") +VALID_PROGRESS = ("percentage", "bigfile", "accurate") + +def get_exception_string(e): + if hasattr(e, "message"): + return str(e.message) + else: + return str(e) def validateVideo(path): - supported_types = ['video/mp4'] + 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) + print("File", path, "detected type is `" + detected_type + "` which is not one of", supported_types) - force_file = ['y', 'yes'] + 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 @@ -69,12 +73,15 @@ def validateLanguage(language): else: return False +def validateDate(date): + return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S") + 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') + publishAt = validateDate(publishDate) if now >= publishAt: return False except ValueError: @@ -86,7 +93,7 @@ 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') + originalDate = validateDate(originalDate) if now <= originalDate: return False except ValueError: diff --git a/prismedia/video.py b/prismedia/video.py index 03fcd06..71a9d60 100644 --- a/prismedia/video.py +++ b/prismedia/video.py @@ -1,15 +1,29 @@ -from os.path import dirname, splitext, basename, isfile +from os.path import dirname, splitext, basename, isfile, normpath, expanduser +class Platform(object): + """ + Store data representing a Platform. + """ + + def __init__(self): + self.error = None + self.publishAt = None + self.url = None + +# TODO: Add container for `with-*` and a `isValid` method to check that all `with-*` options are present # TODO: We need some list (using enum?) for the commons licences, language, privacy, categories options class Video(object): - """Store data representing a Video.""" + """ + Store data representing a Video. + """ def __init__(self): self.path = "" self.thumbnail = None self.name = None - self.description = "default description" + self.description = "Video uploaded with Prismedia" self.playlistName = None + self.playlistCreate = False self.privacy = "private" self.category = "films" self.tags = [] @@ -27,7 +41,6 @@ class Video(object): self.nsfw = False # Each platform should insert here the upload state - # TODO: Create a platform object to have an common object/interface to use for each plugin? self.platform = {} @property @@ -36,12 +49,16 @@ class Video(object): @path.setter def path(self, value): - if value == "" or isfile(value): - self._path = value + path = normpath(expanduser(value)) + + if value == "": + self._path = "" + elif isfile(path): + self._path = path else: # TODO: log instead to debug ? info ? print("The path `" + value + "` does not point to a video") - self.path = "" + self._path = "" @property def thumbnail(self):