Browse Source

WIP make Peertube platform’s plugin upload videos. Light mode

plugins
Zykino 3 years ago
parent
commit
311771a555
10 changed files with 329 additions and 310 deletions
  1. +2
    -4
      README.md
  2. +7
    -4
      prismedia/cli.py
  3. +54
    -15
      prismedia/core.py
  4. +3
    -3
      prismedia/pluginInterfaces.py
  5. +1
    -1
      prismedia/plugins/interfaces/help.py
  6. +213
    -256
      prismedia/plugins/platforms/peertube.py
  7. +2
    -0
      prismedia/plugins/platforms/peertube.yapsy-plugin
  8. +2
    -6
      prismedia/plugins/platforms/youtube.py
  9. +21
    -14
      prismedia/utils.py
  10. +24
    -7
      prismedia/video.py

+ 2
- 4
README.md View File

@ -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 😘
Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘

+ 7
- 4
prismedia/cli.py View File

@ -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`

+ 54
- 15
prismedia/core.py View File

@ -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>"]:
interface = pluginManager.getPluginByName(options["<interface>"], 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()

+ 3
- 3
prismedia/pluginInterfaces.py View File

@ -13,7 +13,7 @@ class IPrismediaBasePlugin(IPlugin):
Base for prismedias 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

+ 1
- 1
prismedia/plugins/interfaces/help.py View File

@ -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["<parameters>"]:

+ 213
- 256
prismedia/plugins/platforms/peertube.py View File

@ -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 [<plugin_name>] 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)

+ 2
- 0
prismedia/plugins/platforms/peertube.yapsy-plugin View File

@ -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.

+ 2
- 6
prismedia/plugins/platforms/youtube.py View File

@ -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.')

+ 21
- 14
prismedia/utils.py View File

@ -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:

+ 24
- 7
prismedia/video.py View File

@ -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):

Loading…
Cancel
Save