3 Commits

17 changed files with 385 additions and 257 deletions
Split View
  1. +0
    -51
      prismedia/cli.py
  2. +58
    -0
      prismedia/configuration.py
  3. +62
    -43
      prismedia/core.py
  4. +6
    -4
      prismedia/pluginInterfaces.py
  5. +0
    -0
      prismedia/plugins/consumers/debug.prismedia-plugin
  6. +10
    -0
      prismedia/plugins/interfaces/cli.prismedia-plugin
  7. +68
    -0
      prismedia/plugins/interfaces/cli.py
  8. +1
    -1
      prismedia/plugins/interfaces/help.prismedia-plugin
  9. +7
    -4
      prismedia/plugins/interfaces/help.py
  10. +0
    -0
      prismedia/plugins/platforms/peertube.prismedia-plugin
  11. +60
    -64
      prismedia/plugins/platforms/peertube.py
  12. +2
    -2
      prismedia/plugins/platforms/youtube.py
  13. +0
    -0
      prismedia/samples/peertube_secret.sample
  14. +0
    -0
      prismedia/samples/youtube_secret.json.sample
  15. +7
    -6
      prismedia/upload.py
  16. +98
    -78
      prismedia/utils.py
  17. +6
    -4
      prismedia/video.py

+ 0
- 51
prismedia/cli.py View File

@ -1,51 +0,0 @@
import pluginInterfaces as pi
import utils
import video as vid
from yapsy.PluginManager import PluginManagerSingleton
def helperFunctionnality(options):
pluginManager = PluginManagerSingleton.get()
optionName = "--hearthbeat"
if options.get(optionName):
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM):
plugin.plugin_object.hearthbeat()
return False
else:
options.pop(optionName)
return True
def parseOptions(options):
video = vid.Video()
video.path = utils.getOption(options, "--file", video.path)
video.thumbnail = utils.getOption(options, "--thumbnail", video.thumbnail)
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).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`
# And so publishAt would only contains the global date.
video.publishAt = utils.getOption(options, "--publish-at", video.publishAt)
# TODO: Add a list of licences
video.licence = utils.getOption(options, "--licence", video.licence)
video.disableComments = utils.getOption(options, "--disable-comments", video.disableComments)
video.nsfw = utils.getOption(options, "--nsfw", video.nsfw)
autoOriginalDate = utils.getOption(options, "--auto-original-date", False)
if autoOriginalDate:
# TODO: Implement
raise NotImplementedError("--auto-original-date functionnality is not yet implemented.")
return video

+ 58
- 0
prismedia/configuration.py View File

@ -0,0 +1,58 @@
from configparser import RawConfigParser
from os.path import splitext, basename, dirname, abspath
class Configuration:
"""
Configuration manager that read the configuration from multiples nfo files that are commons for all interfaces
The configuration will be read and overridden in the following order:
NFO.txt -> nfo.txt -> directory_name.txt -> video_file.txt -> video_name.txt
(value in the rightmost file override values in preceding files)
Your interface may add to this list from either side. Refer to the plugin's description.
A plugin can also override completely this configuration by using other means. For example the cli plugins takes
command line arguments with more importance than any nfo file.
Attributes:
CONFIG_FILE Filename for the base configuration file that should be used to set global behavior for
prismedia and its plugins
"""
CONFIG_FILE = "prismedia.config" # TODO: replace with "config.txt"? something else?
def __init__(self):
self.root_path = dirname(abspath(__file__))
self.config_parser = RawConfigParser()
self.configuration_file_list = []
self.base_configuration_file = self.root_path + "/config/" + self.CONFIG_FILE
self.config_parser.read(self.base_configuration_file)
def read_commons_nfo(self, video_path, video_name):
video_directory = dirname(video_path)
directory_name = basename(video_directory)
video_file = splitext(basename(video_path))[0]
self.configuration_file_list.append(video_directory + "/" + "NFO.txt")
self.configuration_file_list.append(video_directory + "/" + "nfo.txt")
self.configuration_file_list.append(video_directory + "/" + directory_name + ".txt")
self.configuration_file_list.append(video_directory + "/" + video_file + ".txt")
if video_name and video_name != video_file:
self.configuration_file_list.append(video_directory + "/" + video_name + ".txt")
self.config_parser.read(self.configuration_file_list)
# Do not use this in actual production ready code. Prismedia should not write any file
# This can be used in local to see how a plugin's variable needs to be written in the global NFO
# I am afraid that trying to save a plugin info will also save the current config for the video
def _write_config(self):
"""
Write the content of the ConfigParser in a file.
"""
# Do not use `assert` since we want to do the opposite and optimize the other way around
if not __debug__:
raise AssertionError("The method `Configuration._write_config` should not be called in production")
with open(self.base_configuration_file + "_generated", "w") as configFile:
self.config_parser.write(configFile)
configuration_instance = Configuration()

+ 62
- 43
prismedia/core.py View File

@ -3,15 +3,16 @@
# 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: change `youtube-at` and `peertube-at` that are not easily expendable as options in my opinion
# TODO: remove `--url-only` and `--batch`
"""
prismedia - tool to upload videos to different platforms (historicaly Peertube and Youtube)
prismedia - tool to upload videos to different platforms (historically Peertube and Youtube)
Usage:
prismedia [options] --file=<file> | [<interface> [<parameters>...]]
prismedia --hearthbeat
prismedia -h | --help | -V | --version
prismedia [cli] [options] --file=<file>
prismedia <interface> [<parameters>...]
prismedia --heartbeat
prismedia --help | -h | --version | -V
Options:
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option except if you provide the name of a plugin interface (see <interface>).
@ -22,7 +23,7 @@ Options:
WARN: tags with punctuation (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
-c, --category=STRING Category for the videos, see below. (default: Films)
--licence=STRING Creative Common licence tag (for exemple: CC-BY-SA) (default: proprietary)
--licence=STRING Creative Common licence tag (for example: CC-BY-SA) (default: proprietary)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
@ -52,7 +53,7 @@ Options:
Only relevant if --playlist is set.
--progress=STRING Set the progress bar view, one of percentage, bigFile, accurate. [default: percentage]
--hearthbeat Use some credits to show some activity for you apikey so the platform know it is used and would not inactivate your keys.
--heartbeat Use some credits to show some activity for you apikey so the platform know it is used and would not inactivate your keys.
-h, --help Show this help. Note that calling `help` without the `--` calls a plugin showing a different help for the plugins.
-V, --version Show the version.
@ -104,71 +105,89 @@ Languages:
Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
"""
import cli
import os
import logging
import pluginInterfaces as pi
import configuration
import utils
import video as vid
from docopt import docopt
from yapsy.PluginManager import PluginManagerSingleton
import os
import logging
# logging.basicConfig(level=logging.DEBUG)
VERSION = "prismedia v1.0.0-plugins-alpha"
def loadPlugins(basePluginsPath):
def loadPlugins():
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
config = configuration.configuration_instance
basePluginsPath = [config.root_path + "/plugins"]
# TODO: check if AutoInstallPluginManager can help install new plugins or if it is already easy enough to download
# and unzip a file.
PluginManagerSingleton.setBehaviour([ConfigurablePluginManager])
pluginManager = PluginManagerSingleton.get()
pluginManager.setPluginPlaces(basePluginsPath)
pluginManager.setPluginPlaces(directories_list=basePluginsPath)
pluginManager.setPluginInfoExtension("prismedia-plugin")
pluginManager.setConfigParser(config.config_parser, pluginManager.config_has_changed)
# Define the various categories corresponding to the different
# kinds of plugins you have defined
pluginManager.setCategoriesFilter({
pi.PluginTypes.ALL : pi.IPrismediaBasePlugin,
pi.PluginTypes.INTERFACE : pi.IInterfacePlugin,
pi.PluginTypes.PLATFORM : pi.IPlatformPlugin,
pi.PluginTypes.CONSUMER : pi.IConsumerPlugin,
pi.PluginTypes.ALL: pi.IPrismediaBasePlugin,
pi.PluginTypes.INTERFACE: pi.IInterfacePlugin,
pi.PluginTypes.PLATFORM: pi.IPlatformPlugin,
pi.PluginTypes.CONSUMER: pi.IConsumerPlugin,
})
pluginManager.collectPlugins()
return pluginManager
# 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()
# TODO: Check: Maybe this does not work good when installed via pip.
pluginManager = loadPlugins()
# TODO: add the arguments’s verification (copy/adapt the Schema table)
options = docopt(__doc__, version=VERSION)
# Helper functionnalities help the user but do not upload anything
if not cli.helperFunctionnality(options):
# Helper functionalities help the user but do not upload anything
if not utils.helperFunctionalities(options):
exit(os.EX_OK)
video = cli.parseOptions(options)
if options["<interface>"]:
interface = pluginManager.getPluginByName(options["<interface>"], pi.PluginTypes.INTERFACE)
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)
# Get all arguments needed by core only before calling any plugin
listPlatforms = utils.getOption(options, "--platform")
listConsumers = utils.getOption(options, "--consumer")
list = utils.getOption(options, "--platform", [])
if list:
platforms = pluginManager.getPluginsOf(categories=pi.PluginTypes.PLATFORM, name=[list.split(",")])
if options["<interface>"]:
interface_name = utils.getOption("<interface>")
else:
interface_name = "cli"
interface = pluginManager.getPluginByName(interface_name, pi.PluginTypes.INTERFACE)
video = vid.Video()
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 listPlatforms:
platforms = pluginManager.getPluginsOf(categories=pi.PluginTypes.PLATFORM, name=[listPlatforms.split(",")])
else:
platforms = pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM)
list = utils.getOption(options, "--consumer", None)
if list:
consumers = pluginManager.getPluginsOf(categories=pi.PluginTypes.CONSUMER, name=[list.split(",")])
if listConsumers:
consumers = pluginManager.getPluginsOf(categories=pi.PluginTypes.CONSUMER, name=[listConsumers.split(",")])
else:
consumers = pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER)
@ -178,30 +197,30 @@ def main():
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 ?
# 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
# A plugin found ill formed options, it should have logged the precises info
print(plugin.name + " found a malformed option.")
exit(os.EX_CONFIG)
except Exception as e:
logger.critical(utils.get_exception_string(e))
logger.critical("Error while preparing plugin `" + plugin.name + "`: " + 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 ?
# TODO: log instead to error? critical?
print("No plugin installed name `" + plugin.name + "`.")
exit(os.EX_USAGE)
try:
if not plugin.plugin_object.prepare_options(video, options):
# A plugin found ill formed options, it should have logged the precises infos
# A plugin found ill formed options, it should have logged the precises info
print(plugin.name + " found a malformed option.")
exit(os.EX_CONFIG)
except Exception as e:
@ -209,7 +228,7 @@ def main():
exit(os.EX_CONFIG)
if video.path == "":
# TODO: log instead to error ? critical ?
# TODO: log instead to error? critical?
print("No valid path to a video file has been provided.")
exit(os.EX_USAGE)

+ 6
- 4
prismedia/pluginInterfaces.py View File

@ -1,6 +1,7 @@
from enum import Enum
from yapsy.IPlugin import IPlugin
class PluginTypes(Enum):
"""Plugin Types possibles to instantiate in this program."""
ALL = "All"
@ -8,6 +9,7 @@ class PluginTypes(Enum):
PLATFORM = "Platform"
CONSUMER = "Consumer"
class IPrismediaBasePlugin(IPlugin):
"""
Base for prismedias plugin.
@ -19,7 +21,7 @@ class IPrismediaBasePlugin(IPlugin):
- `video`: video object to be uploaded
- `options`: a dictionary of options to be used by Prismedia and other plugins
"""
raise NotImplementedError("`getOptions` must be reimplemented by %s" % self)
raise NotImplementedError("`prepare_options` must be reimplemented by %s" % self)
###
@ -41,11 +43,11 @@ class IPlatformPlugin(IPrismediaBasePlugin):
Interface for the Platform plugin category.
"""
def hearthbeat(self):
def heartbeat(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)
raise NotImplementedError("`heartbeat` must be reimplemented by %s" % self)
def upload(self, video, options):
"""
@ -67,4 +69,4 @@ class IConsumerPlugin(IPrismediaBasePlugin):
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)
raise NotImplementedError("`finished` must be reimplemented by %s" % self)

prismedia/plugins/consumers/debug.yapsy-plugin → prismedia/plugins/consumers/debug.prismedia-plugin View File


+ 10
- 0
prismedia/plugins/interfaces/cli.prismedia-plugin View File

@ -0,0 +1,10 @@
[Core]
Name = cli
Module = cli
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = This interface plugin is used to get the videos details from the Command Line Interface (cli).
To work properly it needs to have the `--file="<path/to/you/vide/file>"` argument

+ 68
- 0
prismedia/plugins/interfaces/cli.py View File

@ -0,0 +1,68 @@
import pluginInterfaces as pi
import utils
import configuration
class Cli(pi.IInterfacePlugin):
"""
This is the default interface plugin. It is used when no interface plugin is specified.
Its core functionality is available as a function call to `prepare_options(video, options)` if you do not need the
Cli object.
This can be useful to let other plugins to rely on the defaults behaviors proposed by this one and extend it.
"""
def prepare_options(self, video, options):
prepare_options(video, options)
def prepare_options(video, options):
# TODO: Add the configuration file from the `--nfo` cli argument
_store_docopt_to_configuration(video, options)
_populate_configuration_into_video(video)
def _store_docopt_to_configuration(options):
items = {}
for key, value in options:
if key.startswith("--"):
options.pop(key)
items[key.strip("- ")] = value
configuration.configuration_instance.config_parser.read_dict(items)
def _populate_configuration_into_video(self, video):
config = configuration.configuration_instance
video.path = utils.getOption(config, "file", video.path)
video.thumbnail = utils.getOption(config, "thumbnail", video.thumbnail)
video.name = utils.getOption(config, "name", video.name)
video.description = utils.getOption(config, "description", video.description)
video.playlistName = utils.getOption(config, "playlist", video.playlistName)
video.privacy = utils.getOption(config, "privacy", video.privacy).lower()
video.category = utils.getOption(config, "category", video.category).lower()
tags = utils.getOption(config, "tag", video.tags)
if isinstance(tags, str):
tags = tags.split(",")
video.tags = tags
video.language = utils.getOption(config, "language", video.language).lower()
video.originalDate = utils.getOption(config, "original-date", video.originalDate)
# TODO: set as an object: { "all": date1, "platformX": date2, … }?
# Maybe the publishAt by platform is better placed in `self.platform`
# And so publishAt would only contains the global date.
video.publishAt = utils.getOption(config, "publish-at", video.publishAt)
# TODO: Add a list of licences
video.licence = utils.getOption(config, "licence", video.licence)
video.disableComments = utils.getOption(config, "disable-comments", video.disableComments)
video.nsfw = utils.getOption(config, "nsfw", video.nsfw)
autoOriginalDate = utils.getOption(config, "auto-original-date", False)
if autoOriginalDate:
# TODO: Implement
raise NotImplementedError("auto-original-date functionality is not yet implemented.")
return video

prismedia/plugins/interfaces/help.yapsy-plugin → prismedia/plugins/interfaces/help.prismedia-plugin View File

@ -6,4 +6,4 @@ Module = help
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Give informations about plugins usage by using the syntax `prismedia help [<plugin_name>...]`.
Description = Give information about plugins usage by using the syntax `prismedia help [<plugin_name>...]`.

+ 7
- 4
prismedia/plugins/interfaces/help.py View File

@ -2,14 +2,16 @@ import pluginInterfaces as pi
from yapsy.PluginManager import PluginManagerSingleton
class Help(pi.IInterfacePlugin):
"""
The help plugin print the usage informations of prismedias plugins.
Use it by simply caling `prismedia help <plugin_name>`.
The help plugin print the usage information of prismedias plugins.
Use it by simply calling `prismedia help <plugin_name>...`.
For example `prismedia help help` bring this help.
"""
def prepare_options(self, video, options):
print(__name__)
pluginManager = PluginManagerSingleton.get()
if options["<parameters>"]:
@ -20,13 +22,14 @@ class Help(pi.IInterfacePlugin):
for p in parameters:
plugin = pluginManager.getPluginByName(p, pi.PluginTypes.ALL)
if plugin is None:
# TODO: log instead to warning ? error ?
# TODO: log instead to warning? error?
print("No plugin was found with name:", p)
continue
print(plugin.name + "\t" + plugin.description)
print("Usage:", plugin.plugin_object.__doc__)
# Generic help this plugin is able to give for the
if p == "help":
print("The plugins are stored in the following folders:", pluginManager.getPluginLocator().plugins_places)
@ -42,7 +45,7 @@ class Help(pi.IInterfacePlugin):
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER):
print("\t" + plugin.name)
# Print a line breack between each plugin help.
# Print a line break between each plugin help.
print()
return False

prismedia/plugins/platforms/peertube.yapsy-plugin → prismedia/plugins/platforms/peertube.prismedia-plugin View File


+ 60
- 64
prismedia/plugins/platforms/peertube.py View File

@ -3,16 +3,13 @@
import pluginInterfaces as pi
import utils
import video as vid
import os
import mimetypes
import json
import logging
import sys
import datetime
import pytz
from os.path import splitext, basename, abspath # TODO: remove me, we already import `os` or at least choose one
from os.path import splitext, basename, abspath
from tzlocal import get_localzone
from configparser import RawConfigParser
@ -20,16 +17,19 @@ from requests_oauthlib import OAuth2Session
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from oauthlib.oauth2 import LegacyApplicationClient
from clint.textui.progress import Bar as ProgressBar
from yapsy.PluginManager import PluginManagerSingleton
logger = logging.getLogger('Prismedia')
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
The connections 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.
"""
SECRETS_FILE = 'peertube_secret'
NAME = "peertube" # TODO: find if it is possible to get the plugin’s name from inside the plugin
SECRETS_FILE = "peertube_secret"
PRIVACY = {
"public": 1,
"unlisted": 2,
@ -74,14 +74,17 @@ class Peertube(pi.IPlatformPlugin):
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):
pluginManager = PluginManagerSingleton.get()
# TODO: get the `publish-at-peertube=DATE` option
# TODO: get the `channel` and `channel-create` options
video.platform[self.name].channel = ""
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "publish-at", "2034-05-07T19:00:00")
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "channel", "toto")
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "channel-create", False)
video.platform[self.NAME].channel = ""
self.secret = RawConfigParser()
self.secret.read(self.SECRETS_FILE)
@ -105,32 +108,28 @@ class Peertube(pi.IPlatformPlugin):
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(self, user_info):
return user_info['videoChannels'][0]['id']
def get_channel_by_name(self, user_info, video):
for channel in user_info["videoChannels"]:
if channel['displayName'] == video.platform[self.name].channel:
if channel['displayName'] == video.platform[self.NAME].channel:
return channel['id']
def create_channel(self, instance_url, video):
template = ('Peertube: Channel %s does not exist, creating it.')
logger.info(template % (video.platform[self.name].channel))
channel_name = utils.cleanString(video.platform[self.name].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":"' + video.platform[self.name].channel + '", \
"displayName":"' + video.platform[self.NAME].channel + '", \
"description":null, \
"support":null}'
@ -139,8 +138,8 @@ class Peertube(pi.IPlatformPlugin):
}
try:
response = self.oauth.post(instance_url + "/api/v1/video-channels/",
data=data.encode('utf-8'),
headers=headers)
data=data.encode('utf-8'),
headers=headers)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
@ -151,25 +150,24 @@ class Peertube(pi.IPlatformPlugin):
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.')
+ 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)
'%s') % response)
exit(1)
def get_default_playlist(user_info):
def get_default_playlist(self, user_info):
return user_info['videoChannels'][0]['id']
def get_playlist_by_name(instance_url, username, video):
def get_playlist_by_name(self, instance_url, username, video):
start = 0
user_playlists = json.loads(self.oauth.get(
instance_url + "/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content)
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)
@ -179,11 +177,11 @@ class Peertube(pi.IPlatformPlugin):
return playlist['id']
start = start + 100
user_playlists = json.loads(self.oauth.get(
instance_url + "/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content)
instance_url + "/api/v1/accounts/" + username + "/video-playlists?start=" + str(
start) + "&count=100").content)
data = user_playlists["data"]
def create_playlist(instance_url, video, channel):
def create_playlist(self, instance_url, video, channel):
template = ('Peertube: Playlist %s does not exist, creating it.')
logger.info(template % (str(video.playlistName)))
# We use files for form-data Content
@ -197,7 +195,7 @@ class Peertube(pi.IPlatformPlugin):
try:
response = self.oauth.post(instance_url + "/api/v1/video-playlists/",
files=files)
files=files)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
@ -208,11 +206,10 @@ class Peertube(pi.IPlatformPlugin):
return jresponse['id']
else:
logger.critical(('Peertube: Creating the playlist failed with an unexpected response: '
'%s') % response)
'%s') % response)
exit(1)
def set_playlist(instance_url, video_id, playlist_id):
def set_playlist(self, instance_url, video_id, playlist_id):
logger.info('Peertube: add video to playlist.')
data = '{"videoId":"' + str(video_id) + '"}'
@ -221,9 +218,9 @@ class Peertube(pi.IPlatformPlugin):
}
try:
response = self.oauth.post(instance_url + "/api/v1/video-playlists/"+str(playlist_id)+"/videos",
data=data,
headers=headers)
response = self.oauth.post(instance_url + "/api/v1/video-playlists/" + str(playlist_id) + "/videos",
data=data,
headers=headers)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
@ -232,19 +229,18 @@ class Peertube(pi.IPlatformPlugin):
logger.info('Peertube: Video is successfully added to the playlist.')
else:
logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: '
'%s') % response)
'%s') % response)
exit(1)
def upload_video(self, video, options):
def get_userinfo(instance_url):
return json.loads(self.oauth.get(instance_url + "/api/v1/users/me").content)
def get_userinfo(base_url):
return json.loads(self.oauth.get(base_url + "/api/v1/users/me").content)
def get_file(path):
def get_file(video_path):
mimetypes.init()
return (basename(path), open(abspath(path), 'rb'),
mimetypes.types_map[splitext(path)[1]])
return (basename(video_path), open(abspath(video_path), 'rb'),
mimetypes.types_map[splitext(video_path)[1]])
path = video.path
instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip('/')
@ -257,7 +253,7 @@ class Peertube(pi.IPlatformPlugin):
# https://github.com/requests/toolbelt/issues/205
fields = [
("name", video.name),
("licence", "1"), # TODO: get licence from video object
("licence", "1"), # TODO: get licence from video object
("description", video.description),
("category", str(self.CATEGORY[video.category])),
("language", str(self.LANGUAGE[video.language])),
@ -274,7 +270,8 @@ class Peertube(pi.IPlatformPlugin):
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: 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
@ -285,8 +282,8 @@ class Peertube(pi.IPlatformPlugin):
fields.append(("tags[]", strtag))
# If peertubeAt exists, use instead of publishAt
if video.platform[self.name].publishAt:
publishAt = video.platform[self.name].publishAt
if video.platform[self.NAME].publishAt:
publishAt = video.platform[self.NAME].publishAt
elif video.publishAt:
publishAt = video.publishAt
@ -306,12 +303,13 @@ class Peertube(pi.IPlatformPlugin):
fields.append(("thumbnailfile", get_file(video.thumbnail)))
fields.append(("previewfile", get_file(video.thumbnail)))
if hasattr(video.platform[self.name], "channel"): # TODO: Should always be present
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 `" + 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"
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 = self.get_default_channel(user_info)
@ -323,8 +321,9 @@ class Peertube(pi.IPlatformPlugin):
if not playlist_id and video.playlistCreate:
playlist_id = create_playlist(instance_url, video, channel_id)
elif not playlist_id:
logger.critical("Peertube: Playlist `" + video.playlistName + "` does not exist, please set --playlistCreate"
" if you want to create it")
logger.critical(
"Peertube: Playlist `" + video.playlistName + "` does not exist, please set --playlistCreate"
" if you want to create it")
exit(1)
encoder = MultipartEncoder(fields)
@ -338,8 +337,8 @@ class Peertube(pi.IPlatformPlugin):
'Content-Type': multipart_data.content_type
}
response = self.oauth.post(instance_url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
data=multipart_data,
headers=headers)
if response is not None:
if response.status_code == 200:
@ -350,17 +349,16 @@ class Peertube(pi.IPlatformPlugin):
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 + ".")
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 '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)
'%s') % response)
exit(1)
# upload_finished = False
# def create_callback(encoder, progress_type):
# upload_size_MB = encoder.len * (1 / (1024 * 1024))
@ -396,16 +394,14 @@ class Peertube(pi.IPlatformPlugin):
#
# return callback
def hearthbeat(self):
def heartbeat(self):
"""
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use.
"""
print("Hearthbeat for peertube (nothing to do)")
print("heartbeat for peertube (nothing to do)")
pass
def upload(self, video, options):
# def run(options):
def upload(self, video, options):
logger.info('Peertube: Uploading video...')
self.upload_video(video, options)

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

@ -373,7 +373,7 @@ def resumable_upload(request, resource, method, options):
time.sleep(sleep_seconds)
def hearthbeat():
def heartbeat():
"""Use the minimums credits possibles of the API so google does not readuce to 0 the allowed credits.
This apparently happens after 90 days without any usage of credits.
For more info see the official documentations:
@ -384,7 +384,7 @@ def hearthbeat():
try:
get_playlist_by_name(youtube, "Foo")
except HttpError as e:
logger.error('Youtube: An HTTP error %d occurred on hearthbeat:\n%s' %
logger.error('Youtube: An HTTP error %d occurred on heartbeat:\n%s' %
(e.resp.status, e.content))

prismedia/config/peertube_secret.sample → prismedia/samples/peertube_secret.sample View File


prismedia/config/youtube_secret.json.sample → prismedia/samples/youtube_secret.json.sample View File


+ 7
- 6
prismedia/upload.py View File

@ -7,7 +7,7 @@ prismedia - tool to upload videos to Peertube and Youtube
Usage:
prismedia --file=<FILE> [options]
prismedia -f <FILE> --tags=STRING [options]
prismedia --hearthbeat
prismedia --heartbeat
prismedia -h | --help
prismedia --version
@ -52,7 +52,7 @@ Options:
Only relevant if --playlist is set.
--progress=STRING Set the progress bar view, one of percentage, bigFile (MB), accurate (KB).
--hearthbeat Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently)
--heartbeat Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently)
-h --help Show this help.
--version Show version.
@ -109,7 +109,6 @@ if sys.version_info[0] < 3:
import os
import datetime
import logging
logger = logging.getLogger('Prismedia')
from docopt import docopt
@ -117,6 +116,8 @@ from . import yt_upload
from . import pt_upload
from . import utils
logger = logging.getLogger('Prismedia')
try:
# noinspection PyUnresolvedReferences
from schema import Schema, And, Or, Optional, SchemaError, Hook, Use
@ -289,15 +290,15 @@ def main():
Optional('--playlist'): Or(None, str),
Optional('--playlistCreate'): bool,
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
'--hearthbeat': bool,
'--heartbeat': bool,
'--help': bool,
'--version': bool,
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
object: object
})
if options.get('--hearthbeat'):
yt_upload.hearthbeat()
if options.get('--heartbeat'):
yt_upload.heartbeat()
exit(0)
# We need to validate early options first as withNFO and logs options should be prioritized

+ 98
- 78
prismedia/utils.py View File

@ -2,7 +2,9 @@
# coding: utf-8
from configparser import RawConfigParser, NoOptionError, NoSectionError
from os.path import dirname, splitext, basename, isfile, getmtime
from os.path import dirname, splitext, basename, isfile, getmtime, exists
from yapsy.PluginManager import PluginManagerSingleton
import pluginInterfaces as pi
import re
import unidecode
import logging
@ -24,12 +26,28 @@ VALID_LANGUAGES = ("arabic", "english", "french",
"portuguese", "punjabi", "russian", "spanish")
VALID_PROGRESS = ("percentage", "bigfile", "accurate")
def helperFunctionalities(options):
pluginManager = PluginManagerSingleton.get()
optionName = "--heartbeat"
if options.get(optionName):
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM):
plugin.plugin_object.heartbeat()
return False
else:
options.pop(optionName)
return True
def get_exception_string(e):
if hasattr(e, "message"):
return str(e.message)
else:
return str(e)
def validateVideo(path):
supported_types = ["video/mp4"]
detected_type = magic.from_file(path, mime=True)
@ -73,6 +91,7 @@ def validateLanguage(language):
else:
return False
def validateDate(date):
return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
@ -103,7 +122,7 @@ def validateOriginalDate(originalDate):
def validateThumbnail(thumbnail):
supported_types = ['image/jpg', 'image/jpeg']
if os.path.exists(thumbnail) and \
if exists(thumbnail) and \
magic.from_file(thumbnail, mime=True) in supported_types:
return thumbnail
else:
@ -148,81 +167,81 @@ def searchOriginalDate(options):
return datetime.datetime.fromtimestamp(int(fileModificationDate[0])).isoformat()
# return the nfo as a RawConfigParser object
def loadNFO(filename):
try:
logger.info("Loading " + filename + " as NFO")
nfo = RawConfigParser()
nfo.read(filename, encoding='utf-8')
return nfo
except Exception as e:
logger.critical("Problem loading NFO file " + filename + ": " + str(e))
exit(1)
return False
def parseNFO(options):
video_directory = dirname(options.get('--file'))
directory_name = basename(video_directory)
nfo_txt = False
nfo_directory = False
nfo_videoname = False
nfo_file = False
nfo_cli = False
if isfile(video_directory + "/" + "nfo.txt"):
nfo_txt = loadNFO(video_directory + "/" + "nfo.txt")
elif isfile(video_directory + "/" + "NFO.txt"):
nfo_txt = loadNFO(video_directory + "/" + "NFO.txt")
if isfile(video_directory + "/" + directory_name + ".txt"):
nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt")
if options.get('--name'):
if isfile(video_directory + "/" + options.get('--name')):
nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt")
video_file = splitext(basename(options.get('--file')))[0]
if isfile(video_directory + "/" + video_file + ".txt"):
nfo_file = loadNFO(video_directory + "/" + video_file + ".txt")
if options.get('--nfo'):
if isfile(options.get('--nfo')):
nfo_cli = loadNFO(options.get('--nfo'))
else:
logger.critical("Given NFO file does not exist, please check your path.")
exit(1)
# If there is no NFO and strict option is enabled, then stop there
if options.get('--withNFO'):
if not isinstance(nfo_cli, RawConfigParser) and \
not isinstance(nfo_file, RawConfigParser) and \
not isinstance(nfo_videoname, RawConfigParser) and \
not isinstance(nfo_directory, RawConfigParser) and \
not isinstance(nfo_txt, RawConfigParser):
logger.critical("You have required the strict presence of NFO but none is found, please use a NFO.")
exit(1)
# We need to load NFO in this exact order to keep the priorities
# options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt
for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]:
if nfo:
# We need to check all options and replace it with the nfo value if not defined (None or False)
for key, value in options.items():
key = key.replace("--", "")
try:
# get string options
if value is None and nfo.get('video', key):
options['--' + key] = nfo.get('video', key)
# get boolean options
elif value is False and nfo.getboolean('video', key):
options['--' + key] = nfo.getboolean('video', key)
except NoOptionError:
continue
except NoSectionError:
logger.critical(nfo + " misses section [video], please check syntax of your NFO.")
exit(1)
return options
# # return the nfo as a RawConfigParser object
# def loadNFO(filename):
# try:
# logger.info("Loading " + filename + " as NFO")
# nfo = RawConfigParser()
# nfo.read(filename, encoding='utf-8')
# return nfo
# except Exception as e:
# logger.critical("Problem loading NFO file " + filename + ": " + str(e))
# exit(1)
# return False
#
#
# def parseNFO(options):
# video_directory = dirname(options.get('--file'))
# directory_name = basename(video_directory)
# nfo_txt = False
# nfo_directory = False
# nfo_videoname = False
# nfo_file = False
# nfo_cli = False
#
# if isfile(video_directory + "/" + "nfo.txt"):
# nfo_txt = loadNFO(video_directory + "/" + "nfo.txt")
# elif isfile(video_directory + "/" + "NFO.txt"):
# nfo_txt = loadNFO(video_directory + "/" + "NFO.txt")
#
# if isfile(video_directory + "/" + directory_name + ".txt"):
# nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt")
#
# if options.get('--name'):
# if isfile(video_directory + "/" + options.get('--name')):
# nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt")
#
# video_file = splitext(basename(options.get('--file')))[0]
# if isfile(video_directory + "/" + video_file + ".txt"):
# nfo_file = loadNFO(video_directory + "/" + video_file + ".txt")
#
# if options.get('--nfo'):
# if isfile(options.get('--nfo')):
# nfo_cli = loadNFO(options.get('--nfo'))
# else:
# logger.critical("Given NFO file does not exist, please check your path.")
# exit(1)
#
# # If there is no NFO and strict option is enabled, then stop there
# if options.get('--withNFO'):
# if not isinstance(nfo_cli, RawConfigParser) and \
# not isinstance(nfo_file, RawConfigParser) and \
# not isinstance(nfo_videoname, RawConfigParser) and \
# not isinstance(nfo_directory, RawConfigParser) and \
# not isinstance(nfo_txt, RawConfigParser):
# logger.critical("You have required the strict presence of NFO but none is found, please use a NFO.")
# exit(1)
#
# # We need to load NFO in this exact order to keep the priorities
# # options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt
# for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]:
# if nfo:
# # We need to check all options and replace it with the nfo value if not defined (None or False)
# for key, value in options.items():
# key = key.replace("--", "")
# try:
# # get string options
# if value is None and nfo.get('video', key):
# options['--' + key] = nfo.get('video', key)
# # get boolean options
# elif value is False and nfo.getboolean('video', key):
# options['--' + key] = nfo.getboolean('video', key)
# except NoOptionError:
# continue
# except NoSectionError:
# logger.critical(nfo + " misses section [video], please check syntax of your NFO.")
# exit(1)
# return options
def cleanString(toclean):
@ -231,7 +250,8 @@ def cleanString(toclean):
return cleaned
def getOption(options, optionName, defaultValue = None):
def getOption(options, optionName, defaultValue=None):
value = options.get(optionName)
options.pop(optionName)

+ 6
- 4
prismedia/video.py View File

@ -1,5 +1,6 @@
from os.path import dirname, splitext, basename, isfile, normpath, expanduser
class Platform(object):
"""
Store data representing a Platform.
@ -13,6 +14,7 @@ class Platform(object):
def __repr__(self):
return str(self.__dict__)
# 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):
@ -59,13 +61,13 @@ class Video(object):
elif isfile(path):
self._path = path
else:
# TODO: log instead to debug ? info ?
# TODO: log instead to debug? info?
print("The path `" + value + "` does not point to a video")
self._path = ""
@property
def thumbnail(self):
if not self._thumbnail is None:
if self._thumbnail is not None:
return self._thumbnail
else:
result = None
@ -89,7 +91,7 @@ class Video(object):
elif isfile(video_directory + video_file + ".jpeg"):
result = video_directory + video_file + ".jpeg"
# TODO: move to caller. Logging the output is its resporsability
# TODO: move to caller. Logging the output is its responsibility
# Display some info after research
# if not result:
# logger.debug("No thumbnail has been found, continuing")
@ -104,7 +106,7 @@ class Video(object):
@property
def name(self):
if not self._name is None:
if self._name is not None:
return self._name
else:
return splitext(basename(self.path))[0]

Loading…
Cancel
Save