diff --git a/voctopublish/api_client/c3tt_rpc_client.py b/voctopublish/api_client/c3tt_rpc_client.py index 3c319b1..669185b 100644 --- a/voctopublish/api_client/c3tt_rpc_client.py +++ b/voctopublish/api_client/c3tt_rpc_client.py @@ -36,7 +36,6 @@ def __init__(self, url, group, host, secret): self.group = group self.host = host self.secret = secret - self.ticket_id = None def _gen_signature(self, method, args): """ @@ -72,16 +71,21 @@ def _gen_signature(self, method, args): hash_ = hmac.new(bytes(self.secret, 'utf-8'), bytes(sig_args, 'utf-8'), hashlib.sha256) return hash_.hexdigest() - def _open_rpc(self, method, args=[]): + def _open_rpc(self, method, ticket=None, args=[]): """ create xmlrpc client :param method: + :param ticket: optional, either a numeric ticket_id or an instance of Ticket class :param args: :return: attributes of the answer """ logging.debug('creating XML RPC proxy: ' + self.url + "?group=" + self.group + "&hostname=" + self.host) - if self.ticket_id: - args.insert(0, self.ticket_id) + if ticket is not None: + # the ticket parameter can be either a numeric ticket_id or an instance of Ticket class + if isinstance(ticket, int) or isinstance(ticket, str): + args.insert(0, ticket) + else: + args.insert(0, ticket.id) try: proxy = xmlrpc.client.ServerProxy(self.url + "?group=" + self.group + "&hostname=" + self.host) @@ -139,49 +143,83 @@ def get_version(self): """ return str(self._open_rpc("C3TT.getVersion")) - def assign_next_unassigned_for_state(self, ticket_type, to_state): + def assign_next_unassigned_for_state(self, ticket_type, to_state, property_filters = []): """ check for new ticket on tracker and get assignment this also sets the ticket id in the c3tt client instance and has therefore be called before any ticket related function :param ticket_type: type of ticket :param to_state: ticket state the returned ticket will be in after this call + :parm property_filters: return only tickets matching given properties :return: ticket id or None in case no ticket is available for the type and state in the request """ - ret = self._open_rpc("C3TT.assignNextUnassignedForState", [ticket_type, to_state]) + ret = self._open_rpc("C3TT.assignNextUnassignedForState", args=[ticket_type, to_state, property_filters]) # if we get no xml here there is no ticket for this job if not ret: return None else: - self.ticket_id = ret['id'] - return ret['id'] + return ret + + def get_assigned_for_state(self, ticket_type, state, property_filters = []): + """ + Get first assigned ticket in state $state + function + :param ticket_type: type of ticket + :param to_state: ticket state the returned ticket will be in after this call + :parm property_filters: return only tickets matching given properties + :return: ticket id or None in case no ticket is available for the type and state in the request + """ + ret = self._open_rpc("C3TT.getAssignedForState", args=[ticket_type, state, property_filters]) + # if we get no xml here there is no ticket for this job + if not ret: + return None + else: + if len(ret) > 1: + logging.warn("multiple tickets assined, fetching first one") + return ret[0] + + def get_tickets_for_state(self, ticket_type, to_state, property_filters = []): + """ + Get all tickets in state $state from projects assigned to the workerGroup, unless workerGroup is halted + function + :param ticket_type: type of ticket + :param to_state: ticket state the returned ticket will be in after this call + :parm property_filters: return only tickets matching given properties + :return: ticket id or None in case no ticket is available for the type and state in the request + """ + ret = self._open_rpc("C3TT.getTicketsForState", args=[ticket_type, to_state, property_filters]) + # if we get no xml here there is no ticket for this job + if not ret: + return None + else: + return ret - def set_ticket_properties(self, properties): + def set_ticket_properties(self, ticket, properties): """ set ticket properties :param properties: :return: Boolean """ - ret = self._open_rpc("C3TT.setTicketProperties", [properties]) + ret = self._open_rpc("C3TT.setTicketProperties", ticket, args=[properties]) if not ret: logging.error("no xml in answer") return False else: return True - def get_ticket_properties(self): + def get_ticket_properties(self, ticket): """ get ticket properties :return: """ - ret = self._open_rpc("C3TT.getTicketProperties") + ret = self._open_rpc("C3TT.getTicketProperties", ticket) if not ret: logging.error("no xml in answer") return None else: return ret - def set_ticket_done(self): + def set_ticket_done(self, ticket): """ set Ticket status on done :return: @@ -189,19 +227,12 @@ def set_ticket_done(self): ret = self._open_rpc("C3TT.setTicketDone") logging.debug(str(ret)) - def set_ticket_failed(self, error): + def set_ticket_failed(self, ticket, error): """ set ticket status on failed an supply a error text :param error: """ - self._open_rpc("C3TT.setTicketFailed", [error.encode('ascii', 'xmlcharrefreplace')]) - - def get_ticket_id(self): - """ - get the id of the ticket assigned to the client instance - :return: Ticket id or None if no ID is assigned yet - """ - return self.ticket_id + self._open_rpc("C3TT.setTicketFailed", ticket, [error.encode('ascii', 'xmlcharrefreplace')]) class C3TTException(Exception): diff --git a/voctopublish/api_client/voctoweb_client.py b/voctopublish/api_client/voctoweb_client.py index 7125ebc..0917a6a 100644 --- a/voctopublish/api_client/voctoweb_client.py +++ b/voctopublish/api_client/voctoweb_client.py @@ -22,7 +22,7 @@ import time import tempfile import operator -import paramiko +#import paramiko import requests import glob @@ -265,6 +265,20 @@ def upload_file(self, local_filename, remote_filename, remote_folder): logging.info("uploading " + remote_filename + " done") + + def get_event(self): + """ + Gets event on the voctoweb API host via GUID + :return: + """ + try: + url = self.api_url[:-4] + "public/events/" + self.t.guid + print(url) + r = requests.get(url) + except requests.exceptions.BaseHTTPError as e_: + raise VoctowebException("error while checking event id with public API") from e_ + return r + def create_or_update_event(self): """ Create a new event on the voctoweb API host @@ -272,11 +286,6 @@ def create_or_update_event(self): """ logging.info('creating event on ' + self.api_url + ' in conference ' + self.t.voctoweb_slug) - # prepare some variables for the api call - url = self.api_url + 'events' - if self.t.voctoweb_event_id: - url += '/' + self.t.voctoweb_event_id - if self.t.url: if self.t.url.startswith('//'): event_url = 'https:' + self.t.url @@ -297,6 +306,17 @@ def create_or_update_event(self): for link in self.t.links: description = '\n\n'.join([description, '' + link + '']) + images = {} + # only publish images if we already have them, which is not the case for relive only events + if hasattr(self.t, 'local_filename_base'): + images = { + 'thumb_filename': self.t.local_filename_base + ".jpg", + 'poster_filename': self.t.local_filename_base + "_preview.jpg", + 'timeline_filename': self.t.local_filename_base + ".timeline.jpg", + 'thumbnails_filename': self.t.local_filename_base + ".thumbnails.vtt", + 'release_date': str(time.strftime("%Y-%m-%d")) + } + # API code https://github.com/voc/voctoweb/blob/master/app/controllers/api/events_controller.rb headers = {'CONTENT-TYPE': 'application/json'} payload = {'api_key': self.api_key, @@ -308,32 +328,36 @@ def create_or_update_event(self): 'subtitle': self.t.subtitle, 'link': event_url, 'original_language': self.t.languages[0], - 'thumb_filename': self.t.local_filename_base + ".jpg", - 'poster_filename': self.t.local_filename_base + "_preview.jpg", - 'timeline_filename': self.t.local_filename_base + ".timeline.jpg", - 'thumbnails_filename': self.t.local_filename_base + ".thumbnails.vtt", - 'conference_id': self.t.voctoweb_slug, + #'conference_id': self.t.voctoweb_slug, 'description': description, 'date': self.t.date, 'persons': self.t.people, 'tags': self.t.voctoweb_tags, 'promoted': False, - 'release_date': str(time.strftime("%Y-%m-%d")) - } - } - logging.debug("api url: " + url + ' header: ' + str(headers) + ' payload: ' + str(payload)) - + **images + } + } # call voctoweb api try: - # TODO make ssl verify a config option - # r = requests.post(url, headers=headers, data=json.dumps(payload), verify=False) + + # prepare some variables for the api call + url = self.api_url + 'events' + logging.debug("api url: " + url + ' header: ' + str(headers) + ' payload: ' + json.dumps(payload)) if self.t.voctoweb_event_id: - r = requests.patch(url, headers=headers, data=json.dumps(payload)) + try: + logging.info("trying to patch event " + self.t.guid) + r = requests.patch(url + '/' + self.t.guid, headers=headers, data=json.dumps(payload)) + except: + logging.info("... faild, trying to create new event " + self.t.guid) + r = requests.post(url, headers=headers, data=json.dumps(payload)) else: + logging.info("trying to create new event " + self.t.guid) r = requests.post(url, headers=headers, data=json.dumps(payload)) except requests.packages.urllib3.exceptions.MaxRetryError as e: raise VoctowebException("Error during creation of event: " + str(e)) from e + + print() return r def create_recording(self, local_filename, filename, folder, language, hq, html5, single_language=False): diff --git a/voctopublish/create-event-by-ticket.py b/voctopublish/create-event-by-ticket.py new file mode 100755 index 0000000..5f31a4f --- /dev/null +++ b/voctopublish/create-event-by-ticket.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# Copyright (C) 2017 derpeter +# Copyright (C) 2019 andi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import configparser +import socket +import sys +import logging +import os +import subprocess +import argparse +import time + +from api_client.c3tt_rpc_client import C3TTClient +from api_client.voctoweb_client import VoctowebClient +from model.ticket_module import Ticket + + + +class RelivePublisher: + """ + This is the main class for the Voctopublish application + It is meant to be used with the c3tt ticket tracker + """ + def __init__(self, args = {}): + # load config + if not os.path.exists('/home/andi/relive/client.conf'): + raise IOError("Error: config file not found") + + self.config = configparser.ConfigParser() + self.config.read('/home/andi/relive/client.conf') + + self.notfail = args.notfail + + # set up logging + logging.addLevelName(logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING)) + logging.addLevelName(logging.ERROR, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.ERROR)) + logging.addLevelName(logging.INFO, "\033[1;32m%s\033[1;0m" % logging.getLevelName(logging.INFO)) + logging.addLevelName(logging.DEBUG, "\033[1;85m%s\033[1;0m" % logging.getLevelName(logging.DEBUG)) + + self.logger = logging.getLogger() + + sh = logging.StreamHandler(sys.stdout) + if self.config['general']['debug']: + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s {%(filename)s:%(lineno)d} %(message)s') + else: + formatter = logging.Formatter('%(asctime)s - %(message)s') + + sh.setFormatter(formatter) + self.logger.addHandler(sh) + self.logger.setLevel(logging.DEBUG) + + level = self.config['general']['debug'] + if level == 'info': + self.logger.setLevel(logging.INFO if not args.verbose else logging.DEBUG) + elif level == 'warning': + self.logger.setLevel(logging.WARNING) + elif level == 'error': + self.logger.setLevel(logging.ERROR) + elif level == 'debug': + self.logger.setLevel(logging.DEBUG) + + if self.config['C3Tracker']['host'] == "None": + self.host = socket.getfqdn() + else: + self.host = self.config['C3Tracker']['host'] + + self.ticket_type = 'encoding' + self.to_state = 'releasing' + + logging.debug('creating C3TTClient') + try: + self.c3tt = C3TTClient(self.config['C3Tracker']['url'], + self.config['C3Tracker']['group'], + self.host, + self.config['C3Tracker']['secret']) + except Exception as e_: + raise PublisherException('Config parameter missing or empty, please check config') from e_ + + def create_event(self, ticket_id): + """ + Decide based on the information provided by the tracker where to publish. + """ + ticket = self._get_ticket_by_id(ticket_id) + + if not ticket: + return + + # voctoweb + if ticket.profile_voctoweb_enable and ticket.voctoweb_enable: + logging.debug( + 'encoding profile media flag: ' + str(ticket.profile_voctoweb_enable) + " project media flag: " + str(ticket.voctoweb_enable)) + self._publish_event_to_voctoweb(ticket) + + def _get_ticket_by_id(self, ticket_id): + """ + Request a ticket from tracker + :return: a ticket object or None in case no ticket is available + """ + logging.info('requesting ticket from tracker') + t = None + + logging.info("Ticket ID:" + str(ticket_id)) + try: + ticket_properties = self.c3tt.get_ticket_properties(ticket_id) + logging.debug("Ticket Properties: " + str(ticket_properties)) + except Exception as e_: + raise e_ + ticket_properties['EncodingProfile.Slug'] = 'relive' + ticket_properties['Publishing.Voctoweb.EnableProfile'] = 'yes' + t = Ticket({'id': ticket_id, 'parent_id': None}, ticket_properties) + + return t + + + + def _get_ticket_from_tracker(self): + """ + Request the next unassigned ticket for the configured states + :return: a ticket object or None in case no ticket is available + """ + logging.info('requesting ticket from tracker') + t = None + + ticket_meta = None + # when we are in debug mode, we first check if we are already assigned to a ticket from previous run + if self.notfail: + ticket_meta = self.c3tt.get_assigned_for_state(self.ticket_type, self.to_state, {'EncodingProfile.Slug': 'relive'}) + # otherwhise, or if that was not successful get the next unassigned one + if not ticket_meta: + ticket_meta = self.c3tt.assign_next_unassigned_for_state(self.ticket_type, self.to_state, {'EncodingProfile.Slug': 'relive'}) + + if ticket_meta: + ticket_id = ticket_meta['id'] + logging.info("Ticket ID:" + str(ticket_id)) + try: + ticket_properties = self.c3tt.get_ticket_properties(ticket_id) + logging.debug("Ticket Properties: " + str(ticket_properties)) + except Exception as e_: + if not args.notfail: + self.c3tt.set_ticket_failed(ticket_id, e_) + raise e_ + t = Ticket(ticket_meta, ticket_properties) + else: + logging.info('No ticket of type ' + self.ticket_type + ' for state ' + self.to_state) + + return t + + def _publish_event_to_voctoweb(self, ticket): + """ + Create a event on an voctomix instance. This includes creating a recording for each media file. + """ + try: + vw = VoctowebClient(ticket, + self.config['voctoweb']['api_key'], + self.config['voctoweb']['api_url'], + self.config['voctoweb']['ssh_host'], + self.config['voctoweb']['ssh_port'], + self.config['voctoweb']['ssh_user']) + except Exception as e_: + raise PublisherException('Error initializing voctoweb client. Config parameter missing') from e_ + + if not(ticket.voctoweb_event_id): + # if this is master ticket we need to check if we need to create an event on voctoweb + + # check if event exists on voctoweb instance, and abort if this is already the case + r = vw.get_event() + if r.status_code == 204: + print(r.content) + print(r.status_code) + #self.c3tt.set_ticket_done(ticket) + print('event already exists, abort!') + print(os.path.join(ticket.voctoweb_url, ticket.slug)) + return + + + logging.debug('this is a master ticket') + r = vw.create_or_update_event() + if r.status_code in [200, 201]: + logging.info("new event created or existing updated") + self.c3tt.set_ticket_properties(ticket.id, {'Voctoweb.EventId': r.json()['id']}) + + ''' + try: + # we need to write the Event ID onto the parent ticket, so the other (master) encoding tickets + # also have acccess to the Voctoweb Event ID + self.c3tt.set_ticket_properties(ticket.parent_id, {'Voctoweb.EventId': r.json()['id']}) + except Exception as e_: + raise PublisherException('failed to Voctoweb EventID to parent ticket') from e_ + ''' + + elif r.status_code == 422: + # If this happens tracker and voctoweb are out of sync regarding the event id + # or we have some input error... + # todo: write voctoweb event_id to ticket properties --Andi + logging.warning("event already exists => please sync event manually") + else: + raise PublisherException('Voctoweb returned an error while creating an event: ' + str(r.status_code) + ' - ' + str(r.content)) + + print(os.path.join(ticket.voctoweb_url, ticket.slug)) + else: + logging.info("nothing to to, is already pubilshed") + print(os.path.join(ticket.voctoweb_url, ticket.slug)) + print("master? ", ticket.master ) + print("event id:", ticket.voctoweb_event_id) + + + + #self.c3tt.set_ticket_done(ticket) + + +class PublisherException(Exception): + pass + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='generate events on voctoweb for relive ') + parser.add_argument('ticket', action="store", help="integer id of an tracker ticket") + parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--notfail', action='store_true', default=False, help='do not mark ticket as failed in tracker when something goes wrong') + + args = parser.parse_args() + + try: + publisher = RelivePublisher(args) + except Exception as e: + logging.error(e) + logging.exception(e) + sys.exit(-1) + + try: + publisher.create_event(args.ticket) + except Exception as e: + exc_type, exc_obj, exc_tb = sys.exc_info() + logging.exception(e) + sys.exit(-1) diff --git a/voctopublish/create-events.py b/voctopublish/create-events.py new file mode 100755 index 0000000..d81f8d4 --- /dev/null +++ b/voctopublish/create-events.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# Copyright (C) 2017 derpeter +# Copyright (C) 2019 andi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import configparser +import socket +import sys +import logging +import os +import subprocess +import argparse +import time + +from api_client.c3tt_rpc_client import C3TTClient +from api_client.voctoweb_client import VoctowebClient +from model.ticket_module import Ticket + + + +class RelivePublisher: + """ + This is the main class for the Voctopublish application + It is meant to be used with the c3tt ticket tracker + """ + def __init__(self, args = {}): + # load config + if not os.path.exists('client.conf'): + raise IOError("Error: config file not found") + + self.config = configparser.ConfigParser() + self.config.read('client.conf') + + self.notfail = args.notfail + + # set up logging + logging.addLevelName(logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING)) + logging.addLevelName(logging.ERROR, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.ERROR)) + logging.addLevelName(logging.INFO, "\033[1;32m%s\033[1;0m" % logging.getLevelName(logging.INFO)) + logging.addLevelName(logging.DEBUG, "\033[1;85m%s\033[1;0m" % logging.getLevelName(logging.DEBUG)) + + self.logger = logging.getLogger() + + sh = logging.StreamHandler(sys.stdout) + if self.config['general']['debug']: + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s {%(filename)s:%(lineno)d} %(message)s') + else: + formatter = logging.Formatter('%(asctime)s - %(message)s') + + sh.setFormatter(formatter) + self.logger.addHandler(sh) + self.logger.setLevel(logging.DEBUG) + + level = self.config['general']['debug'] + if level == 'info': + self.logger.setLevel(logging.INFO if not args.verbose else logging.DEBUG) + elif level == 'warning': + self.logger.setLevel(logging.WARNING) + elif level == 'error': + self.logger.setLevel(logging.ERROR) + elif level == 'debug': + self.logger.setLevel(logging.DEBUG) + + if self.config['C3Tracker']['host'] == "None": + self.host = socket.getfqdn() + else: + self.host = self.config['C3Tracker']['host'] + + self.ticket_type = 'encoding' + self.to_state = 'releasing' + + logging.debug('creating C3TTClient') + try: + self.c3tt = C3TTClient(self.config['C3Tracker']['url'], + self.config['C3Tracker']['group'], + self.host, + self.config['C3Tracker']['secret']) + except Exception as e_: + raise PublisherException('Config parameter missing or empty, please check config') from e_ + + def create_event(self): + """ + Decide based on the information provided by the tracker where to publish. + """ + ticket = self._get_ticket_from_tracker() + + if not ticket: + return + + # voctoweb + if ticket.profile_voctoweb_enable and ticket.voctoweb_enable: + logging.debug( + 'encoding profile media flag: ' + str(ticket.profile_voctoweb_enable) + " project media flag: " + str(ticket.voctoweb_enable)) + self._publish_event_to_voctoweb(ticket) + + + def _get_ticket_from_tracker(self): + """ + Request the next unassigned ticket for the configured states + :return: a ticket object or None in case no ticket is available + """ + logging.info('requesting ticket from tracker') + t = None + + ticket_meta = None + # when we are in debug mode, we first check if we are already assigned to a ticket from previous run + if self.notfail: + ticket_meta = self.c3tt.get_assigned_for_state(self.ticket_type, self.to_state, {'EncodingProfile.Slug': 'relive'}) + # otherwhise, or if that was not successful get the next unassigned one + if not ticket_meta: + ticket_meta = self.c3tt.assign_next_unassigned_for_state(self.ticket_type, self.to_state, {'EncodingProfile.Slug': 'relive'}) + + if ticket_meta: + ticket_id = ticket_meta['id'] + logging.info("Ticket ID:" + str(ticket_id)) + try: + ticket_properties = self.c3tt.get_ticket_properties(ticket_id) + logging.debug("Ticket Properties: " + str(ticket_properties)) + except Exception as e_: + if not args.notfail: + self.c3tt.set_ticket_failed(ticket_id, e_) + raise e_ + t = Ticket(ticket_meta, ticket_properties) + else: + logging.info('No ticket of type ' + self.ticket_type + ' for state ' + self.to_state) + + return t + + def _publish_event_to_voctoweb(self, ticket): + """ + Create a event on an voctomix instance. This includes creating a recording for each media file. + """ + try: + vw = VoctowebClient(ticket, + self.config['voctoweb']['api_key'], + self.config['voctoweb']['api_url'], + self.config['voctoweb']['ssh_host'], + self.config['voctoweb']['ssh_port'], + self.config['voctoweb']['ssh_user']) + except Exception as e_: + raise PublisherException('Error initializing voctoweb client. Config parameter missing') from e_ + + if not(ticket.voctoweb_event_id): + # if this is master ticket we need to check if we need to create an event on voctoweb + + # check if event exists on voctoweb instance, and abort if this is already the case + r = vw.get_event() + if r.status_code == 204: + print(r.content) + print(r.status_code) + #self.c3tt.set_ticket_done(ticket) + print('event already exists, abort!') + print(os.path.join(ticket.voctoweb_url, ticket.slug)) + return + + + logging.debug('this is a master ticket') + r = vw.create_or_update_event() + if r.status_code in [200, 201]: + logging.info("new event created or existing updated") + + ''' + try: + # we need to write the Event ID onto the parent ticket, so the other (master) encoding tickets + # also have acccess to the Voctoweb Event ID + self.c3tt.set_ticket_properties(ticket.parent_id, {'Voctoweb.EventId': r.json()['id']}) + except Exception as e_: + raise PublisherException('failed to Voctoweb EventID to parent ticket') from e_ + ''' + + elif r.status_code == 422: + # If this happens tracker and voctoweb are out of sync regarding the event id + # or we have some input error... + # todo: write voctoweb event_id to ticket properties --Andi + logging.warning("event already exists => please sync event manually") + else: + raise PublisherException('Voctoweb returned an error while creating an event: ' + str(r.status_code) + ' - ' + str(r.content)) + + print(os.path.join(ticket.voctoweb_url, ticket.slug)) + else: + logging.info("nothing to to, is already pubilshed") + print(os.path.join(ticket.voctoweb_url, ticket.slug)) + print("master? ", ticket.master ) + print("event id:", ticket.voctoweb_event_id) + + + + self.c3tt.set_ticket_done(ticket) + + +class PublisherException(Exception): + pass + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='generate events on voctoweb for relive ') + parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--notfail', action='store_true', default=False, help='do not mark ticket as failed in tracker when something goes wrong') + + args = parser.parse_args() + + try: + publisher = RelivePublisher(args) + except Exception as e: + logging.error(e) + logging.exception(e) + sys.exit(-1) + + try: + publisher.create_event() + except Exception as e: + exc_type, exc_obj, exc_tb = sys.exc_info() + logging.exception(e) + sys.exit(-1) diff --git a/voctopublish/model/ticket_module.py b/voctopublish/model/ticket_module.py index 8040010..3a5bf37 100644 --- a/voctopublish/model/ticket_module.py +++ b/voctopublish/model/ticket_module.py @@ -23,28 +23,18 @@ class Ticket: and adds some additional information. """ - def __init__(self, ticket, ticket_id): - if not ticket: - raise TicketException('Ticket was None type') - self.__tracker_ticket = ticket - self.ticket_id = ticket_id + def __init__(self, ticket_meta, ticket_properties): + if not ticket_properties: + raise TicketException('Ticket properties was None type') + self.__tracker_ticket_meta = ticket_meta + self.__tracker_ticket = ticket_properties + + self.id = ticket_meta['id'] + self.parent_id = ticket_meta['parent_id'] # project properties self.acronym = self._validate_('Project.Slug') - # encoding profile properties - if self._validate_('EncodingProfile.IsMaster') == 'yes': - self.master = True - else: - self.master = False - self.profile_extension = self._validate_('EncodingProfile.Extension') - self.profile_slug = self._validate_('EncodingProfile.Slug') - self.filename = self._validate_('EncodingProfile.Basename') + "." + self.profile_extension - self.folder = self._validate_('EncodingProfile.MirrorFolder') - - # encoding properties - self.language_index = self._validate_('Encoding.LanguageIndex', True) - # fahrplan properties self.slug = self._validate_('Fahrplan.Slug') self.guid = self._validate_('Fahrplan.GUID') @@ -53,65 +43,85 @@ def __init__(self, ticket, ticket_id): self.subtitle = self._validate_('Fahrplan.Subtitle', True) self.abstract = self._validate_('Fahrplan.Abstract', True) self.description = self._validate_('Fahrplan.Description', True) - self.date = self._validate_('Fahrplan.Date') - self.local_filename = self.fahrplan_id + "-" + self.profile_slug + "." + self.profile_extension - self.local_filename_base = self.fahrplan_id + "-" + self.profile_slug + self.date = self._validate_('Fahrplan.DateTime') self.room = self._validate_('Fahrplan.Room') self.people = [] - if 'Fahrplan.Person_list' in ticket: + if 'Fahrplan.Person_list' in ticket_properties: self.people = self._validate_('Fahrplan.Person_list').split(', ') self.links = [] - if 'Fahrplan.Links' in ticket: + if 'Fahrplan.Links' in ticket_properties: self.links = self._validate_('Fahrplan.Links', True).split(' ') # the following are arguments that my not be present in every fahrplan self.track = self._validate_('Fahrplan.Track', True) self.day = self._validate_('Fahrplan.Day', True) self.url = self._validate_('Fahrplan.URL', True) - self.date = self._validate_('Fahrplan.Date') - - # recording ticket properties - - # special case languages: if Encoding.Language is present, it overrides Record.Language: - if 'Encoding.Language' in ticket: - self.language = self._validate_('Encoding.Language') - self.languages = dict(enumerate(self._validate_('Encoding.Language').split('-'))) - else: - self.language = self._validate_('Record.Language') - self.languages = {int(k.split('.')[-1]): self._validate_(k) for k in self.__tracker_ticket.keys() if k.startswith('Record.Language.')} - self.language_template = self._validate_('Encoding.LanguageTemplate') # general publishing properties self.publishing_path = self._validate_('Publishing.Path') self.publishing_tags = self._validate_('Publishing.Tags', True) - # youtube properties - if self._validate_('Publishing.YouTube.EnableProfile') == 'yes': - self.profile_youtube_enable = True - else: - self.profile_youtube_enable = False - if self._validate_('Publishing.YouTube.Enable') == 'yes': - self.youtube_enable = True + + # encoding (profile) properties + #if self._validate_('EncodingProfile.IsMaster') == 'yes': + # self.master = True + #else: + self.master = False + self.profile_slug = self._validate_('EncodingProfile.Slug') + print('Ticket Type:', self.profile_slug) + if self.profile_slug == 'relive': + # TODO: map two char language codes to three char ones in a more proper way... + lang_map = {'en': 'eng', 'de': 'deu'} # WORKAROUND + self.language = lang_map[self._validate_('Fahrplan.Language')] + self.languages = {0: self.language} else: - self.youtube_enable = False - # we will fill the following variables only if youtube is enabled - if self.profile_youtube_enable and self.youtube_enable: - self.youtube_token = self._validate_('Publishing.YouTube.Token') - self.youtube_category = self._validate_('Publishing.YouTube.Category', True) - self.youtube_privacy = self._validate_('Publishing.YouTube.Privacy', True) - self.youtube_tags = self._validate_('Publishing.YouTube.Tags', True) - self.youtube_title_prefix = self._validate_('Publishing.YouTube.TitlePrefix', True) - self.youtube_title_prefix_speakers = self._validate_('Publishing.YouTube.TitlePrefixSpeakers', True) - self.youtube_title_suffix = self._validate_('Publishing.YouTube.TitleSuffix', True) - # check if this event has already been published to youtube - if 'YouTube.Url0' in ticket and self._validate_('YouTube.Url0') is not None: - self.has_youtube_url = True + # encoding (profile) properties + self.profile_extension = self._validate_('EncodingProfile.Extension', optional=True) + self.filename = self._validate_('EncodingProfile.Basename') + "." + self.profile_extension + self.folder = self._validate_('EncodingProfile.MirrorFolder') + self.language_index = self._validate_('Encoding.LanguageIndex', True) + self.local_filename = self.fahrplan_id + "-" + self.profile_slug + "." + self.profile_extension + self.local_filename_base = self.fahrplan_id + "-" + self.profile_slug + + # recording ticket properties + + # special case languages: if Encoding.Language is present, it overrides Record.Language: + if 'Encoding.Language' in ticket_properties: + self.language = self._validate_('Encoding.Language') + self.languages = dict(enumerate(self._validate_('Encoding.Language').split('-'))) + else: + self.language = self._validate_('Record.Language') + self.languages = {int(k.split('.')[-1]): self._validate_(k) for k in self.__tracker_ticket.keys() if k.startswith('Record.Language.')} + self.language_template = self._validate_('Encoding.LanguageTemplate') + + + # youtube properties + if self._validate_('Publishing.YouTube.EnableProfile') == 'yes': + self.profile_youtube_enable = True else: - self.has_youtube_url = False - if self._validate_('Publishing.YouTube.Playlists', True) is not None: - self.youtube_playlists = self._validate_('Publishing.YouTube.Playlists', True).split(',') + self.profile_youtube_enable = False + if self._validate_('Publishing.YouTube.Enable') == 'yes': + self.youtube_enable = True else: - self.youtube_playlists = [] - self.youtube_urls = '' + self.youtube_enable = False + # we will fill the following variables only if youtube is enabled + if self.profile_youtube_enable and self.youtube_enable: + self.youtube_token = self._validate_('Publishing.YouTube.Token') + self.youtube_category = self._validate_('Publishing.YouTube.Category', True) + self.youtube_privacy = self._validate_('Publishing.YouTube.Privacy', True) + self.youtube_tags = self._validate_('Publishing.YouTube.Tags', True) + self.youtube_title_prefix = self._validate_('Publishing.YouTube.TitlePrefix', True) + self.youtube_title_prefix_speakers = self._validate_('Publishing.YouTube.TitlePrefixSpeakers', True) + self.youtube_title_suffix = self._validate_('Publishing.YouTube.TitleSuffix', True) + # check if this event has already been published to youtube + if 'YouTube.Url0' in ticket_properties and self._validate_('YouTube.Url0') is not None: + self.has_youtube_url = True + else: + self.has_youtube_url = False + if self._validate_('Publishing.YouTube.Playlists', True) is not None: + self.youtube_playlists = self._validate_('Publishing.YouTube.Playlists', True).split(',') + else: + self.youtube_playlists = [] + self.youtube_urls = '' # voctoweb properties if self._validate_('Publishing.Voctoweb.EnableProfile') == 'yes': @@ -126,16 +136,16 @@ def __init__(self, ticket, ticket_id): self.voctoweb_url = self._validate_('Publishing.Voctoweb.Url', True) # we will fill the following variables only if voctoweb is enabled if self.profile_voctoweb_enable and self.voctoweb_enable: - self.mime_type = self._validate_('Publishing.Voctoweb.MimeType') + self.mime_type = self._validate_('Publishing.Voctoweb.MimeType', True) self.voctoweb_thump_path = self._validate_('Publishing.Voctoweb.Thumbpath') self.voctoweb_path = self._validate_('Publishing.Voctoweb.Path') self.voctoweb_slug = self._validate_('Publishing.Voctoweb.Slug') self.voctoweb_tags = [self.acronym, self.fahrplan_id, self.date.split('-')[0]] if self.track: self.voctoweb_tags.append(self.track) - if 'Publishing.Voctoweb.Tags' in ticket: + if 'Publishing.Voctoweb.Tags' in ticket_properties: self.voctoweb_tags += self._validate_('Publishing.Voctoweb.Tags').replace(' ', '').split(',') - if 'Publishing.Tags' in ticket: + if 'Publishing.Tags' in ticket_properties: self.voctoweb_tags += self._validate_('Publishing.Tags').replace(' ', '').split(',') self.recording_id = self._validate_('Voctoweb.RecordingId.Master', True) self.voctoweb_event_id = self._validate_('Voctoweb.EventId', True) diff --git a/voctopublish/test/api_client_test/test_c3tt_rpc_client.py b/voctopublish/test/api_client_test/test_c3tt_rpc_client.py index c379942..fda2084 100644 --- a/voctopublish/test/api_client_test/test_c3tt_rpc_client.py +++ b/voctopublish/test/api_client_test/test_c3tt_rpc_client.py @@ -15,7 +15,6 @@ def setUp(self): def test_init(self): assert self._client.url == "rpc" # todo shouldn't this be an _url join? - assert self._client.ticket_id is None def test_gen_signature_args_empty(self): hash_ = self._client._gen_signature("test", []) diff --git a/voctopublish/voctopublish.py b/voctopublish/voctopublish.py index e0e6904..c369e2a 100755 --- a/voctopublish/voctopublish.py +++ b/voctopublish/voctopublish.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import configparser +import argparse import socket import sys import logging @@ -35,13 +36,14 @@ class Publisher: This is the main class for the Voctopublish application It is meant to be used with the c3tt ticket tracker """ - def __init__(self): + def __init__(self, args = {}): # load config if not os.path.exists('client.conf'): raise IOError("Error: config file not found") self.config = configparser.ConfigParser() self.config.read('client.conf') + self.notfail = args.notfail # set up logging logging.addLevelName(logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING)) @@ -63,7 +65,7 @@ def __init__(self): level = self.config['general']['debug'] if level == 'info': - self.logger.setLevel(logging.INFO) + self.logger.setLevel(logging.INFO if not args.verbose else logging.DEBUG) elif level == 'warning': self.logger.setLevel(logging.WARNING) elif level == 'error': @@ -126,7 +128,7 @@ def publish(self): "encoding profile youtube flag: " + str(self.ticket.profile_youtube_enable) + ' project youtube flag: ' + str(self.ticket.youtube_enable)) self._publish_to_youtube() - self.c3tt.set_ticket_done() + self.c3tt.set_ticket_done(self.ticket) # Twitter if self.ticket.twitter_enable and self.ticket.master: @@ -143,16 +145,26 @@ def _get_ticket_from_tracker(self): """ logging.info('requesting ticket from tracker') t = None - ticket_id = self.c3tt.assign_next_unassigned_for_state(self.ticket_type, self.to_state) - if ticket_id: + + ticket_meta = None + # when we are in notfail aka debug mode, we first check if we are already assigned to a ticket from previous run + if self.notfail: + ticket_meta = self.c3tt.get_assigned_for_state(self.ticket_type, self.to_state, {'EncodingProfile.Slug': 'relive'}) + # otherwhise, or if that was not successful get the next unassigned one + if not ticket_meta: + ticket_meta = self.c3tt.assign_next_unassigned_for_state(self.ticket_type, self.to_state, {'EncodingProfile.Slug': 'relive'}) + + if ticket_meta: + ticket_id = ticket_meta['id'] logging.info("Ticket ID:" + str(ticket_id)) try: - tracker_ticket = self.c3tt.get_ticket_properties() - logging.debug("Ticket: " + str(tracker_ticket)) + ticket_properties = self.c3tt.get_ticket_properties(ticket_id) + logging.debug("Ticket Properties: " + str(ticket_properties)) except Exception as e_: - self.c3tt.set_ticket_failed(e_) + if not args.notfail: + self.c3tt.set_ticket_failed(ticket_id, e_) raise e_ - t = Ticket(tracker_ticket, ticket_id) + t = Ticket(ticket_meta, ticket_properties) else: logging.info('No ticket of type ' + self.ticket_type + ' for state ' + self.to_state) @@ -190,7 +202,7 @@ def _publish_to_voctoweb(self): logging.debug('response: ' + str(r.json())) try: # todo: only set recording id when new recording was created, and not when it was only updated - self.c3tt.set_ticket_properties({'Voctoweb.EventId': r.json()['id']}) + self.c3tt.set_ticket_properties(self.ticket, {'Voctoweb.EventId': r.json()['id']}) except Exception as e_: raise PublisherException('failed to Voctoweb EventID to ticket') from e_ @@ -199,7 +211,7 @@ def _publish_to_voctoweb(self): # todo: write voctoweb event_id to ticket properties --Andi logging.warning("event already exists => publishing") else: - raise PublisherException('Voctoweb returned an error while creating an event: ' + str(r.status_code) + ' - ' + str(r.content)) + raise PublisherException('Voctoweb returned an error while creating an event: ' + str(r.status_code) + ' - ' + str(r.content)) # in case of a multi language release we create here the single language files if len(self.ticket.languages) > 1: @@ -239,7 +251,7 @@ def _publish_to_voctoweb(self): # when the ticket was created, and not only updated: write recording_id to ticket if recording_id: - self.c3tt.set_ticket_properties({'Voctoweb.RecordingId.Master': recording_id}) + self.c3tt.set_ticket_properties(self.ticket, {'Voctoweb.RecordingId.Master': recording_id}) def _mux_to_single_language(self, vw): """ @@ -276,7 +288,7 @@ def _mux_to_single_language(self, vw): try: # when the ticket was created, and not only updated: write recording_id to ticket if recording_id: - self.c3tt.set_ticket_properties({'Voctoweb.RecordingId.' + self.ticket.languages[language]: str(recording_id)}) + self.c3tt.set_ticket_properties(self.ticket, {'Voctoweb.RecordingId.' + self.ticket.languages[language]: str(recording_id)}) except Exception as e_: raise PublisherException('failed to set RecordingId to ticket') from e_ @@ -294,7 +306,7 @@ def _publish_to_youtube(self): for i, youtubeUrl in enumerate(youtube_urls): props['YouTube.Url' + str(i)] = youtubeUrl - self.c3tt.set_ticket_properties(props) + self.c3tt.set_ticket_properties(self.ticket, props) self.ticket.youtube_urls = props # now, after we reported everything back to the tracker, we try to add the videos to our own playlists @@ -317,6 +329,11 @@ class PublisherException(Exception): if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--notfail', action='store_true', default=False, help='do not mark ticket as failed in tracker when something goes wrong') + + args = parser.parse_args() try: publisher = Publisher() except Exception as e: @@ -328,6 +345,7 @@ class PublisherException(Exception): publisher.publish() except Exception as e: exc_type, exc_obj, exc_tb = sys.exc_info() - publisher.c3tt.set_ticket_failed('%s: %s' % (exc_type.__name__, e)) + if not args.notfail: + publisher.c3tt.set_ticket_failed(publisher.ticket.id, '%s: %s' % (exc_type.__name__, e)) logging.exception(e) sys.exit(-1)