diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0097e9f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +skip-string-normalization = true diff --git a/qencode/__init__.py b/qencode/__init__.py index 045c2d4..f11b550 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,31 +1,44 @@ - def client(api_key, api_url=None, version=None, **kwargs): from client import QencodeApiClient + return QencodeApiClient(api_key, api_url=api_url, version=version, **kwargs) + def custom_params(): - from custom_params import CustomTranscodingParams - return CustomTranscodingParams() + from custom_params import CustomTranscodingParams + + return CustomTranscodingParams() + def format(): - from custom_params import Format - return Format() + from custom_params import Format + + return Format() + def destination(): - from custom_params import Destination - return Destination() + from custom_params import Destination + + return Destination() + def stream(): - from custom_params import Stream - return Stream() + from custom_params import Stream + + return Stream() + def x264_video_codec(): - from custom_params import Libx264_VideoCodecParameters - return Libx264_VideoCodecParameters() + from custom_params import Libx264_VideoCodecParameters + + return Libx264_VideoCodecParameters() + def x265_video_codec(): - from custom_params import Libx265_VideoCodecParameters - return Libx265_VideoCodecParameters() + from custom_params import Libx265_VideoCodecParameters + + return Libx265_VideoCodecParameters() + from exeptions import QencodeClientException, QencodeTaskException @@ -34,6 +47,3 @@ def x265_video_codec(): __version__ = "1.0.4" __status__ = "Production/Stable" __author__ = "Qencode" - - - diff --git a/qencode/client.py b/qencode/client.py index 993043c..69d5869 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -2,50 +2,50 @@ from task import Task from metadata import Metadata + class QencodeApiClient(object): - """ - :return: encoder object - - """ - def __init__(self, api_key, api_url=None, version=None): - self.api_key = api_key - self.api_url = api_url if api_url else 'https://api.qencode.com/' - self.version = version if version else 'v1' - self.connect = Http(self.version, self.api_url) - self.access_token = None - self.expire = None - self.error = None - self.code = None - self.message = '' - self._get_access_token() - - - def create_task(self, **kwargs): - return Task(self.access_token, self.connect, **kwargs) - - def refresh_access_token(self): - response = self.connect.request('access_token', dict(api_key=self.api_key)) - if not response['error']: - self.access_token = response['token'] - self.expire = response['expire'] - else: - self.error = response['error'] - self.code = response['error'] - self.message = response.get('message') - - - def _get_access_token(self): - response = self.connect.request('access_token', dict(api_key=self.api_key)) - if not response['error']: - self.access_token = response['token'] - self.expire = response['expire'] - else: - self.error = response['error'] - self.code = response['error'] - self.message = response.get('message') - - def get_metadata(self, uri): - metadata = Metadata(self.access_token, self.connect) - video_info = metadata.get(uri) - return video_info \ No newline at end of file + """ + :return: encoder object + + """ + + def __init__(self, api_key, api_url=None, version=None): + self.api_key = api_key + self.api_url = api_url if api_url else 'https://api.qencode.com/' + self.version = version if version else 'v1' + self.connect = Http(self.version, self.api_url) + self.access_token = None + self.expire = None + self.error = None + self.code = None + self.message = '' + self._get_access_token() + + def create_task(self, **kwargs): + return Task(self.access_token, self.connect, **kwargs) + + def refresh_access_token(self): + response = self.connect.request('access_token', dict(api_key=self.api_key)) + if not response['error']: + self.access_token = response['token'] + self.expire = response['expire'] + else: + self.error = response['error'] + self.code = response['error'] + self.message = response.get('message') + + def _get_access_token(self): + response = self.connect.request('access_token', dict(api_key=self.api_key)) + if not response['error']: + self.access_token = response['token'] + self.expire = response['expire'] + else: + self.error = response['error'] + self.code = response['error'] + self.message = response.get('message') + + def get_metadata(self, uri): + metadata = Metadata(self.access_token, self.connect) + video_info = metadata.get(uri) + return video_info diff --git a/qencode/const.py b/qencode/const.py index 353804b..f72355e 100644 --- a/qencode/const.py +++ b/qencode/const.py @@ -3,22 +3,26 @@ SLEEP_ERROR = 60 COMPLETED_STATUS = ['completed', 'saved'] -ERROR_OK = 0 -ERROR_SERVER_INTERNAL = 1 -ERROR_BAD_APP_ID = 2 -ERROR_APP_ID_NOT_FOUND = 3 -ERROR_BAD_TOKEN = 4 -ERROR_TOKEN_NOT_FOUND = 5 -ERROR_TARIFF_NOT_PAID = 6 -ERROR_MASTER_NOT_FOUND = 7 -ERROR_SYSTEM_BUSY = 8 -ERROR_BAD_PAYLOAD = 9 +ERROR_OK = 0 +ERROR_SERVER_INTERNAL = 1 +ERROR_BAD_APP_ID = 2 +ERROR_APP_ID_NOT_FOUND = 3 +ERROR_BAD_TOKEN = 4 +ERROR_TOKEN_NOT_FOUND = 5 +ERROR_TARIFF_NOT_PAID = 6 +ERROR_MASTER_NOT_FOUND = 7 +ERROR_SYSTEM_BUSY = 8 +ERROR_BAD_PAYLOAD = 9 ERROR_PROJECT_NOT_FOUND = 10 -ERROR_BAD_PROFILE = 11 +ERROR_BAD_PROFILE = 11 ERROR_PROFILE_NOT_FOUND = 12 -ERROR_BAD_TOKENS = 13 -ERROR_FIELD_REQUIRED = 14 +ERROR_BAD_TOKENS = 13 +ERROR_FIELD_REQUIRED = 14 -FPS_DRM_KEYGENERATOR_URI_TEMPLATE = 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=2' -CENC_DRM_KEYGENERATOR_URI_TEMPLATE = 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=1' -DRM_KEY_URL_TEMPLATE = 'skd://fps.ezdrm.com/;%s' \ No newline at end of file +FPS_DRM_KEYGENERATOR_URI_TEMPLATE = ( + 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=2' +) +CENC_DRM_KEYGENERATOR_URI_TEMPLATE = ( + 'https://cpix.ezdrm.com/KeyGenerator/cpix.aspx?k=%s&u=%s&p=%s&c=resourcename&m=1' +) +DRM_KEY_URL_TEMPLATE = 'skd://fps.ezdrm.com/;%s' diff --git a/qencode/custom_params.py b/qencode/custom_params.py index 847f0a7..f23aef5 100644 --- a/qencode/custom_params.py +++ b/qencode/custom_params.py @@ -2,143 +2,151 @@ from json import JSONEncoder from utils import rm_attributes_if_null + class CustomTranscodingParams(object): - """CustomTranscodingParams + """CustomTranscodingParams :var source: String. Source video URI. Can be http(s) url or tus uri :var format: String. A list of objects, each describing params for a single output video stream (MP4, WEBM, HLS or MPEG-DASH) - """ - def __init__(self): - self.source = None - self.format = None - self.callback_url = None - rm_attributes_if_null(self) + """ + + def __init__(self): + self.source = None + self.format = None + self.callback_url = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - def remove_null_params(self): - rm_attributes_if_null(self) class Format(object): - """ - :var - :var + """ + :var + :var """ - def __init__(self): - self.output = None - self.file_extension = None - self.destination = None - self.segment_duration = None - self.stream = None - self.logo = None - self.start_time = None - self.duration = None - self.is_watermark = None - self.size = None - self.video_codec = None - self.audio_codec = None - self.aspect_ratio = None - self.quality = None - self.interval = None - self.width = None - self.height = None - self.time = None - self.path = None - self.resize_mod = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + + def __init__(self): + self.output = None + self.file_extension = None + self.destination = None + self.segment_duration = None + self.stream = None + self.logo = None + self.start_time = None + self.duration = None + self.is_watermark = None + self.size = None + self.video_codec = None + self.audio_codec = None + self.aspect_ratio = None + self.quality = None + self.interval = None + self.width = None + self.height = None + self.time = None + self.path = None + self.resize_mod = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Destination(object): - def __init__(self): - self.url = None - self.key = None - self.secret = None - self.permissions = None - self.storage_class = None - rm_attributes_if_null(self) + def __init__(self): + self.url = None + self.key = None + self.secret = None + self.permissions = None + self.storage_class = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) - def remove_null_params(self): - rm_attributes_if_null(self) class Stream(object): - def __init__(self): - self.size = None - self.video_codec = None - self.bitrate = None - self.quality = None - self.rotate = None - self.framerate = None - self.pix_format = None - self.profile = None - self.video_codec_parameters = None - self.keyframe = None - self.segment_duration = None - self.start_time = None - self.duration = None - self.audio_bitrate = None - self.audio_sample_rate = None - self.audio_channels_number = None - self.audio_codec = None - self.downmix_mode = None - self.logo = None - self.aspect_ratio = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + def __init__(self): + self.size = None + self.video_codec = None + self.bitrate = None + self.quality = None + self.rotate = None + self.framerate = None + self.pix_format = None + self.profile = None + self.video_codec_parameters = None + self.keyframe = None + self.segment_duration = None + self.start_time = None + self.duration = None + self.audio_bitrate = None + self.audio_sample_rate = None + self.audio_channels_number = None + self.audio_codec = None + self.downmix_mode = None + self.logo = None + self.aspect_ratio = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Libx264_VideoCodecParameters(object): - def __init__(self): - self.vprofile = None - self.level = None - self.coder = None - self.flags2 = None - self.partitions = None - self.bf = None - self.directpred = None - self.me_method = None - rm_attributes_if_null(self) - - def remove_null_params(self): - rm_attributes_if_null(self) + def __init__(self): + self.vprofile = None + self.level = None + self.coder = None + self.flags2 = None + self.partitions = None + self.bf = None + self.directpred = None + self.me_method = None + rm_attributes_if_null(self) + + def remove_null_params(self): + rm_attributes_if_null(self) + class Libx265_VideoCodecParameters(object): - def __init__(self): - pass + def __init__(self): + pass class MyEncoder(JSONEncoder): - def default(self, obj): - return obj.__dict__ + def default(self, obj): + return obj.__dict__ class Query(object): - def __init__(self): - self.params = None - self.error = None - self.message = '' - self.query = None - - def prepare_params(self): - query = dict(query=self.params) - try: - self.query = json.dumps(query, cls=MyEncoder, encoding='utf-8') - except Exception as e: - self.error = True - self.message = repr(e) - - def validate_params(self): - if not self.params: - self.error = True - self.message = 'Params is required' - return - if not 'source' in self.params.__dict__: - self.error = True - self.message = 'Params: source is required' - return - if not 'format' in self.params.__dict__: - self.error = True - self.message = 'Params: format is required' - return \ No newline at end of file + def __init__(self): + self.params = None + self.error = None + self.message = '' + self.query = None + + def prepare_params(self): + query = dict(query=self.params) + try: + self.query = json.dumps(query, cls=MyEncoder, encoding='utf-8') + except Exception as e: + self.error = True + self.message = repr(e) + + def validate_params(self): + if not self.params: + self.error = True + self.message = 'Params is required' + return + if not 'source' in self.params.__dict__: + self.error = True + self.message = 'Params: source is required' + return + if not 'format' in self.params.__dict__: + self.error = True + self.message = 'Params: format is required' + return diff --git a/qencode/drm/buydrm.py b/qencode/drm/buydrm.py index ad960d1..8ad9fb9 100644 --- a/qencode/drm/buydrm.py +++ b/qencode/drm/buydrm.py @@ -7,19 +7,27 @@ 'xenc': 'http://www.w3.org/2001/04/xmlenc#', 'pskc': 'urn:ietf:params:xml:ns:keyprov:pskc', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'ds': 'http://www.w3.org/2000/09/xmldsig#' + 'ds': 'http://www.w3.org/2000/09/xmldsig#', } SYSTEM_ID_PLAYREADY = '9a04f079-9840-4286-ab92-e65be0885f95' SYSTEM_ID_WIDEVINE = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed' SYSTEM_ID_FAIRPLAY = '94ce86fb-07ff-4f43-adb8-93d2fa968ca2' + def create_cpix_user_request( - key_ids, media_id, user_private_key_path, user_public_cert_path, - use_playready=False, use_widevine=False, use_fairplay=False, - nsmap=None + key_ids, + media_id, + user_private_key_path, + user_public_cert_path, + use_playready=False, + use_widevine=False, + use_fairplay=False, + nsmap=None, ): - document_public_cert_path = os.path.dirname(__file__) + '/keys/buydrm_qencode_public_cert.pem' + document_public_cert_path = ( + os.path.dirname(__file__) + '/keys/buydrm_qencode_public_cert.pem' + ) """Creates CPIX request XML signed end user Arguments: @@ -47,14 +55,20 @@ def create_cpix_user_request( # Delivery data list delivery_data_list = etree.SubElement(root, '{%s}DeliveryDataList' % nsmap['cpix']) - delivery_data = etree.SubElement(delivery_data_list, '{%s}DeliveryData' % nsmap['cpix']) + delivery_data = etree.SubElement( + delivery_data_list, '{%s}DeliveryData' % nsmap['cpix'] + ) delivery_key = etree.SubElement(delivery_data, '{%s}DeliveryKey' % nsmap['cpix']) # The public certificate of a partner. This certificate's public key will be used # to encrypt Document Key which will later be used to encrypt Contnet Keys. x509_data = etree.SubElement(delivery_key, '{%s}X509Data' % nsmap['ds']) x509_cert = etree.SubElement(x509_data, '{%s}X509Certificate' % nsmap['ds']) - x509_cert.text = document_public_cert.replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').replace('\n', '') + x509_cert.text = ( + document_public_cert.replace('-----BEGIN CERTIFICATE-----', '') + .replace('-----END CERTIFICATE-----', '') + .replace('\n', '') + ) # Content key list content_key_list = etree.SubElement(root, '{%s}ContentKeyList' % nsmap['cpix']) @@ -73,34 +87,42 @@ def create_cpix_user_request( ) if use_playready: etree.SubElement( - drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], - systemId=SYSTEM_ID_PLAYREADY + drm_system_list, + '{%s}DRMSystem' % nsmap['cpix'], + kid=data['kid'], + systemId=SYSTEM_ID_PLAYREADY, ) if use_widevine: etree.SubElement( - drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], - systemId=SYSTEM_ID_WIDEVINE + drm_system_list, + '{%s}DRMSystem' % nsmap['cpix'], + kid=data['kid'], + systemId=SYSTEM_ID_WIDEVINE, ) if use_fairplay: etree.SubElement( - drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], - systemId=SYSTEM_ID_FAIRPLAY + drm_system_list, + '{%s}DRMSystem' % nsmap['cpix'], + kid=data['kid'], + systemId=SYSTEM_ID_FAIRPLAY, ) etree.SubElement( - content_key_usage_list, '{%s}ContentKeyUsageRule' % nsmap['cpix'], - kid=data['kid'], intendedTrackType=data['track_type'] + content_key_usage_list, + '{%s}ContentKeyUsageRule' % nsmap['cpix'], + kid=data['kid'], + intendedTrackType=data['track_type'], ) # Signing document with end user's data end_user_signed_root = XMLSigner( c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315', - signature_algorithm='rsa-sha256', digest_algorithm='sha512' + signature_algorithm='rsa-sha256', + digest_algorithm='sha512', ).sign(root, key=end_user_private_key, cert=end_user_public_cert) x509_sign_cert = end_user_signed_root.xpath( '//ds:X509Certificate', namespaces=nsmap )[1] x509_sign_cert.text = x509_sign_cert.text.replace('\n', '') -# + # return etree.tostring(end_user_signed_root).decode('utf-8') - diff --git a/qencode/exeptions.py b/qencode/exeptions.py index 36b5fdc..fa5b795 100644 --- a/qencode/exeptions.py +++ b/qencode/exeptions.py @@ -1,18 +1,15 @@ class QencodeException(Exception): - def __init__(self, message, *args): - super(QencodeException, self).__init__(message, *args) - self.error = message - self.arg = [i for i in args] + def __init__(self, message, *args): + super(QencodeException, self).__init__(message, *args) + self.error = message + self.arg = [i for i in args] + class QencodeClientException(QencodeException): - def __init__(self, message, *args): - super(QencodeClientException, self).__init__(message, *args) + def __init__(self, message, *args): + super(QencodeClientException, self).__init__(message, *args) class QencodeTaskException(QencodeException): - def __init__(self, message, *args): - super(QencodeTaskException, self).__init__(message, *args) - - - - + def __init__(self, message, *args): + super(QencodeTaskException, self).__init__(message, *args) diff --git a/qencode/httptools.py b/qencode/httptools.py index a276517..30f11ff 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -4,45 +4,49 @@ from urlparse import urljoin import ssl + class Http(object): - def __init__(self, version, url, debug=False): - self.version = version - self.url = url - self._debug = debug + def __init__(self, version, url, debug=False): + self.version = version + self.url = url + self._debug = debug - def _call_server(self, url, post_data): - if not url: - response = dict(error=True, message='AttributeError: Bad URL') - return json.dumps(response) - data = urllib.urlencode(post_data) - request = urllib2.Request(url, data) - context = ssl._create_unverified_context() - try: - res = urllib2.urlopen(request, context=context) - except urllib2.HTTPError as e: - headers = e.headers if self._debug else '' - response = dict(error=True, message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers)) - response = json.dumps(response) - except urllib2.URLError as e: - response = dict(error=True, message='URLError: {0}'.format(e.reason)) - response = json.dumps(response) - else: - response = res.read() - return response + def _call_server(self, url, post_data): + if not url: + response = dict(error=True, message='AttributeError: Bad URL') + return json.dumps(response) + data = urllib.urlencode(post_data) + request = urllib2.Request(url, data) + context = ssl._create_unverified_context() + try: + res = urllib2.urlopen(request, context=context) + except urllib2.HTTPError as e: + headers = e.headers if self._debug else '' + response = dict( + error=True, + message='HTTPError: {0} {1} {2}'.format(e.code, e.reason, headers), + ) + response = json.dumps(response) + except urllib2.URLError as e: + response = dict(error=True, message='URLError: {0}'.format(e.reason)) + response = json.dumps(response) + else: + response = res.read() + return response - def request(self, api_name, data): - path = '{version}/{api_name}'.format(version=self.version, api_name=api_name) - response = self._call_server(urljoin(self.url, path), data) - try: - response = json.loads(response) - except ValueError as e: - response = dict(error=True, message=repr(e)) - return response + def request(self, api_name, data): + path = '{version}/{api_name}'.format(version=self.version, api_name=api_name) + response = self._call_server(urljoin(self.url, path), data) + try: + response = json.loads(response) + except ValueError as e: + response = dict(error=True, message=repr(e)) + return response - def post(self, url, data): - response = self._call_server(url, data) - try: - response = json.loads(response) - except ValueError as e: - response = dict(error=True, message=repr(e)) - return response \ No newline at end of file + def post(self, url, data): + response = self._call_server(url, data) + try: + response = json.loads(response) + except ValueError as e: + response = dict(error=True, message=repr(e)) + return response diff --git a/qencode/metadata.py b/qencode/metadata.py index d91176d..fe0ca70 100644 --- a/qencode/metadata.py +++ b/qencode/metadata.py @@ -2,16 +2,19 @@ from qencode import QencodeTaskException import urllib2 -class Metadata(Task): +class Metadata(Task): def get(self, uri): - params = """ + params = ( + """ {"query": { "source": "%s", "format": [ {"output": "metadata", "metadata_version": "4.1.5"} ] } } - """ % uri + """ + % uri + ) self.custom_start(params) while True: status = self.status() @@ -36,5 +39,3 @@ def get(self, uri): data = urllib2.urlopen(url).read() return data - - diff --git a/qencode/task.py b/qencode/task.py index b45f032..194a9cb 100644 --- a/qencode/task.py +++ b/qencode/task.py @@ -6,198 +6,202 @@ class Task(object): - def __init__(self, access_token, connect, debug=False, **kwargs): - self.connect = connect - self.status_url = None - self.main_status_url = '{0}/{1}/status'.format(self.connect.url, self.connect.version) - self.task_token = None - self.upload_url = None - self.access_token = access_token - self._debug = debug - self.message = '' - self.error = None - self.repeat = kwargs.get('repeats') if kwargs.get('repeats') else REPEAT - self._create_task(1) - - - def start(self, profiles, video_url, **kwargs): - """Creating task and starting encode - - :param profiles: String or List object. Profile uuid - :param transfer_method: String. Transfer method uuid - :param video_url: String. Url of source video - :param payload: String. - :return: None - - """ - if not self.error: - # self._create_task(1) - data = self._prepare_data(profiles, video_url, **kwargs) - - if not self.error and self.task_token: - self._start_encode('start_encode', data) - - def custom_start(self, data, **kwargs): - """Creating task and starting encode - - :param query: JSON object for query param. For examples: https://docs.qencode.com - :param payload: String. - :return: None - - """ - if data is None: - self.error = True - self.message = 'Params is required' - - #if not self.error: - # self._create_task(1) - - if not self.error: - query = self._prepare_query(data) - - if not self.error: - data = self._prepare_data_custom(query, **kwargs) - - if not self.error and self.task_token: - self._start_encode('start_encode2', data) - - def status(self): - return self._status() - - def extend_status(self): - return self._extend_status() - - def progress_changed(self, callback, *args, **kwargs): - while 1: - status = self._status() - if status['error']: - return callback(status, *args, **kwargs) - callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - break - time.sleep(SLEEP_REGULAR) - - def task_completed(self, callback, *args, **kwargs): - while 1: - status = self._status() - if status['error']: - return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - return callback(status, *args, **kwargs) - if status.get('status') in COMPLETED_STATUS: - break - time.sleep(SLEEP_REGULAR) - - def _prepare_query(self, params): - if isinstance(params, CustomTranscodingParams): - query_obj = Query() - query_obj.params = params - query_obj.validate_params() - if query_obj.error: - self.error = query_obj.error - self.message = query_obj.message - query_obj.prepare_params() - if query_obj.error: - self.error = query_obj.error - self.message = query_obj.message - return query_obj.query - - if isinstance(params, dict): - query = rm_key_if_null(params) - return json.dumps(query) - - if isinstance(params, basestring): - if is_json(params): - query = rm_key_if_null(params) - return query - else: - self.error = True - try: - self.message = "JSON is not well formatted: {0} Is not defined".format(params) - except Exception as e: - pass - finally: - self.message = "JSON is not well formatted" - - def _prepare_data(self, profiles, video_url, **kwargs): - data = dict( - task_token=self.task_token, - profiles=', '.join(profiles) if type(profiles).__name__ == 'list' else profiles - ) - if isinstance(video_url, list): - try: - data.update(stitch=json.dumps(video_url)) - except Exception: - data.update(stitch=video_url) - else: - data.update(uri=video_url) - if kwargs: - data.update(kwargs) - return data - - def _prepare_data_custom(self, query_json, **kwargs): - data = dict( - task_token=self.task_token, - query=query_json - ) - if kwargs: - data.update(kwargs) - return data - - def _create_task(self, count): - res = self.connect.request('create_task', dict(token=self.access_token)) - if not res['error']: - self.task_token = res.get('task_token') - self.upload_url = res.get('upload_url') - else: - self.error = res['error'] - self.message = res.get('message') - - if self.error and self.error == 8: - if count < REPEAT: - time.sleep(SLEEP_ERROR) - self._create_task(count + 1) - - - def _start_encode(self, api_name, data): - res = self.connect.request(api_name, data) - if not res['error'] and res.get('status_url'): - self.status_url = res['status_url'] - else: - self.status_url = self.main_status_url - self.error = res.get('error') - self.message = res.get('message') - - def _status(self): - response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) - status = None - - if response['error'] == ERROR_BAD_TOKENS: - raise ValueError('Bad token: ' + str(self.task_token)) - - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - if not status and self.status_url != self.main_status_url: - self.status_url = self.main_status_url - response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - if status and 'status_url' in status: - self.status_url = status['status_url'] - - return status - - def _extend_status(self): - response = self.connect.post(self.main_status_url, dict(task_tokens=self.task_token)) - status = None - - if response['error'] == ERROR_BAD_TOKENS: - raise ValueError('Bad token: ' + str(self.task_token)) - - if 'statuses' in response and self.task_token in response['statuses']: - status = response['statuses'][self.task_token] - - return status - + def __init__(self, access_token, connect, debug=False, **kwargs): + self.connect = connect + self.status_url = None + self.main_status_url = '{0}/{1}/status'.format( + self.connect.url, self.connect.version + ) + self.task_token = None + self.upload_url = None + self.access_token = access_token + self._debug = debug + self.message = '' + self.error = None + self.repeat = kwargs.get('repeats') if kwargs.get('repeats') else REPEAT + self._create_task(1) + + def start(self, profiles, video_url, **kwargs): + """Creating task and starting encode + + :param profiles: String or List object. Profile uuid + :param transfer_method: String. Transfer method uuid + :param video_url: String. Url of source video + :param payload: String. + :return: None + + """ + if not self.error: + # self._create_task(1) + data = self._prepare_data(profiles, video_url, **kwargs) + + if not self.error and self.task_token: + self._start_encode('start_encode', data) + + def custom_start(self, data, **kwargs): + """Creating task and starting encode + + :param query: JSON object for query param. For examples: https://docs.qencode.com + :param payload: String. + :return: None + + """ + if data is None: + self.error = True + self.message = 'Params is required' + + # if not self.error: + # self._create_task(1) + + if not self.error: + query = self._prepare_query(data) + + if not self.error: + data = self._prepare_data_custom(query, **kwargs) + + if not self.error and self.task_token: + self._start_encode('start_encode2', data) + + def status(self): + return self._status() + + def extend_status(self): + return self._extend_status() + + def progress_changed(self, callback, *args, **kwargs): + while 1: + status = self._status() + if status['error']: + return callback(status, *args, **kwargs) + callback(status, *args, **kwargs) + if status.get('status') in COMPLETED_STATUS: + break + time.sleep(SLEEP_REGULAR) + + def task_completed(self, callback, *args, **kwargs): + while 1: + status = self._status() + if status['error']: + return callback(status, *args, **kwargs) + if status.get('status') in COMPLETED_STATUS: + return callback(status, *args, **kwargs) + if status.get('status') in COMPLETED_STATUS: + break + time.sleep(SLEEP_REGULAR) + + def _prepare_query(self, params): + if isinstance(params, CustomTranscodingParams): + query_obj = Query() + query_obj.params = params + query_obj.validate_params() + if query_obj.error: + self.error = query_obj.error + self.message = query_obj.message + query_obj.prepare_params() + if query_obj.error: + self.error = query_obj.error + self.message = query_obj.message + return query_obj.query + + if isinstance(params, dict): + query = rm_key_if_null(params) + return json.dumps(query) + + if isinstance(params, basestring): + if is_json(params): + query = rm_key_if_null(params) + return query + else: + self.error = True + try: + self.message = ( + "JSON is not well formatted: {0} Is not defined".format(params) + ) + except Exception as e: + pass + finally: + self.message = "JSON is not well formatted" + + def _prepare_data(self, profiles, video_url, **kwargs): + data = dict( + task_token=self.task_token, + profiles=', '.join(profiles) + if type(profiles).__name__ == 'list' + else profiles, + ) + if isinstance(video_url, list): + try: + data.update(stitch=json.dumps(video_url)) + except Exception: + data.update(stitch=video_url) + else: + data.update(uri=video_url) + if kwargs: + data.update(kwargs) + return data + + def _prepare_data_custom(self, query_json, **kwargs): + data = dict(task_token=self.task_token, query=query_json) + if kwargs: + data.update(kwargs) + return data + + def _create_task(self, count): + res = self.connect.request('create_task', dict(token=self.access_token)) + if not res['error']: + self.task_token = res.get('task_token') + self.upload_url = res.get('upload_url') + else: + self.error = res['error'] + self.message = res.get('message') + + if self.error and self.error == 8: + if count < REPEAT: + time.sleep(SLEEP_ERROR) + self._create_task(count + 1) + + def _start_encode(self, api_name, data): + res = self.connect.request(api_name, data) + if not res['error'] and res.get('status_url'): + self.status_url = res['status_url'] + else: + self.status_url = self.main_status_url + self.error = res.get('error') + self.message = res.get('message') + + def _status(self): + response = self.connect.post(self.status_url, dict(task_tokens=self.task_token)) + status = None + + if response['error'] == ERROR_BAD_TOKENS: + raise ValueError('Bad token: ' + str(self.task_token)) + + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + if not status and self.status_url != self.main_status_url: + self.status_url = self.main_status_url + response = self.connect.post( + self.status_url, dict(task_tokens=self.task_token) + ) + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + if status and 'status_url' in status: + self.status_url = status['status_url'] + + return status + + def _extend_status(self): + response = self.connect.post( + self.main_status_url, dict(task_tokens=self.task_token) + ) + status = None + + if response['error'] == ERROR_BAD_TOKENS: + raise ValueError('Bad token: ' + str(self.task_token)) + + if 'statuses' in response and self.task_token in response['statuses']: + status = response['statuses'][self.task_token] + + return status diff --git a/qencode/tools.py b/qencode/tools.py index d45052e..3f0a7af 100644 --- a/qencode/tools.py +++ b/qencode/tools.py @@ -8,7 +8,10 @@ import xml.etree.cElementTree as et import uuid -def generate_aws_signed_url(region, bucket, object_key, access_key, secret_key, expiration, endpoint=None): + +def generate_aws_signed_url( + region, bucket, object_key, access_key, secret_key, expiration, endpoint=None +): # request elements http_method = 'GET' @@ -34,11 +37,21 @@ def createSignatureKey(key, datestamp, region, service): timestamp = time.strftime('%Y%m%dT%H%M%SZ') datestamp = time.strftime('%Y%m%d') - standardized_querystring = ('X-Amz-Algorithm=AWS4-HMAC-SHA256' + - '&X-Amz-Credential=' + access_key + '/' + datestamp + '/' + region + '/s3/aws4_request' + - '&X-Amz-Date=' + timestamp + - '&X-Amz-Expires=' + str(expiration) + - '&X-Amz-SignedHeaders=host') + standardized_querystring = ( + 'X-Amz-Algorithm=AWS4-HMAC-SHA256' + + '&X-Amz-Credential=' + + access_key + + '/' + + datestamp + + '/' + + region + + '/s3/aws4_request' + + '&X-Amz-Date=' + + timestamp + + '&X-Amz-Expires=' + + str(expiration) + + '&X-Amz-SignedHeaders=host' + ) standardized_querystring_url_encoded = quote(standardized_querystring, safe='&=') standardized_resource = '/' + object_key @@ -48,35 +61,51 @@ def createSignatureKey(key, datestamp, region, service): standardized_headers = 'host:' + host signed_headers = 'host' - standardized_request = (http_method + '\n' + - standardized_resource + '\n' + - standardized_querystring_url_encoded + '\n' + - standardized_headers + '\n' + - '\n' + - signed_headers + '\n' + - payload_hash).encode('utf-8') + standardized_request = ( + http_method + + '\n' + + standardized_resource + + '\n' + + standardized_querystring_url_encoded + + '\n' + + standardized_headers + + '\n' + + '\n' + + signed_headers + + '\n' + + payload_hash + ).encode('utf-8') # assemble string-to-sign hashing_algorithm = 'AWS4-HMAC-SHA256' credential_scope = datestamp + '/' + region + '/' + 's3' + '/' + 'aws4_request' - sts = (hashing_algorithm + '\n' + - timestamp + '\n' + - credential_scope + '\n' + - hashlib.sha256(standardized_request).hexdigest()) + sts = ( + hashing_algorithm + + '\n' + + timestamp + + '\n' + + credential_scope + + '\n' + + hashlib.sha256(standardized_request).hexdigest() + ) # generate the signature signature_key = createSignatureKey(secret_key, datestamp, region, 's3') - signature = hmac.new(signature_key, - (sts).encode('utf-8'), - hashlib.sha256).hexdigest() + signature = hmac.new( + signature_key, (sts).encode('utf-8'), hashlib.sha256 + ).hexdigest() # create and send the request # the 'requests' package autmatically adds the required 'host' header - request_url = (endpoint + '/' + - object_key + '?' + - standardized_querystring_url_encoded + - '&X-Amz-Signature=' + - signature) + request_url = ( + endpoint + + '/' + + object_key + + '?' + + standardized_querystring_url_encoded + + '&X-Amz-Signature=' + + signature + ) def hex_hash(key, msg): return hmac.new(b'key', msg.encode('utf-8'), hashlib.sha256).hexdigest() @@ -112,17 +141,17 @@ def fps_drm(username, password, uid=None): def cenc_drm(username, password, uid=None): - asset_id = uid if uid else uuid.uuid4() - url = CENC_DRM_KEYGENERATOR_URI_TEMPLATE % (asset_id, username, password) - response = requests.post(url, {}) - tree = et.ElementTree(et.fromstring(response.content)) - root = tree.getroot() - key_id = root[0][0].get('kid') - key = root[0][0][0][0][0].text - pssh = root[1][0][0].text - key_id_hex = key_id.replace('-', '') - key_hex = key.decode('base64').encode('hex') - key_url = DRM_KEY_URL_TEMPLATE % key_id - payload = dict(AssetID=asset_id) - data = dict(key=key_hex, key_id=key_id_hex, pssh=pssh, key_url=key_url) - return data, payload + asset_id = uid if uid else uuid.uuid4() + url = CENC_DRM_KEYGENERATOR_URI_TEMPLATE % (asset_id, username, password) + response = requests.post(url, {}) + tree = et.ElementTree(et.fromstring(response.content)) + root = tree.getroot() + key_id = root[0][0].get('kid') + key = root[0][0][0][0][0].text + pssh = root[1][0][0].text + key_id_hex = key_id.replace('-', '') + key_hex = key.decode('base64').encode('hex') + key_url = DRM_KEY_URL_TEMPLATE % key_id + payload = dict(AssetID=asset_id) + data = dict(key=key_hex, key_id=key_id_hex, pssh=pssh, key_url=key_url) + return data, payload diff --git a/qencode/tus_uploader.py b/qencode/tus_uploader.py index 23ca32d..748094a 100644 --- a/qencode/tus_uploader.py +++ b/qencode/tus_uploader.py @@ -2,31 +2,34 @@ from tusclient import client from utils import get_tus_from_url + class UploadStatus(object): - def __init__(self, error = None, url= None, status= None): - self.url = url - self.error = error - self.status = status + def __init__(self, error=None, url=None, status=None): + self.url = url + self.error = error + self.status = status def upload(file_path=None, url=None, chunk_size=None, log_func=None): """ - Returns upload status and url using tus protocol + Returns upload status and url using tus protocol - :fileUrl: + :fileUrl: - Url address where to upload the file + Url address where to upload the file - :Args: - see tusclient.uploader.Uploader for required and optional arguments. + :Args: + see tusclient.uploader.Uploader for required and optional arguments. """ try: my_client = client.TusClient(url=url) - uploader = my_client.uploader(file_path=file_path, chunk_size=chunk_size, log_func=log_func) + uploader = my_client.uploader( + file_path=file_path, chunk_size=chunk_size, log_func=log_func + ) uploader.upload() url_storage = uploader.url tus_url = get_tus_from_url(url_storage) return UploadStatus(url=tus_url, status='Ok', error='') except: - print('Error uploading file to ' + url) - raise \ No newline at end of file + print('Error uploading file to ' + url) + raise diff --git a/qencode/utils.py b/qencode/utils.py index 41db2f8..58cdcf7 100644 --- a/qencode/utils.py +++ b/qencode/utils.py @@ -2,80 +2,95 @@ import logging import json + def is_number(s): - try: - float(s) - return True - except: - return False + try: + float(s) + return True + except: + return False + def get_percent(p): - if is_number(p): - return round(p) - return 0 + if is_number(p): + return round(p) + return 0 + def is_json(value): - try: - json.loads(value) - except ValueError: - return False - return True + try: + json.loads(value) + except ValueError: + return False + return True + def rm_attributes_if_null(class_obj): - for key, val in class_obj.__dict__.items(): - if not val: - class_obj.__dict__.pop(key) + for key, val in class_obj.__dict__.items(): + if not val: + class_obj.__dict__.pop(key) + def rm_key_if_null(obj): - if isinstance(obj, dict): - return _rm_key(obj) - elif isinstance(obj, basestring): - res = _rm_key(json.loads(obj)) - return json.dumps(res) + if isinstance(obj, dict): + return _rm_key(obj) + elif isinstance(obj, basestring): + res = _rm_key(json.loads(obj)) + return json.dumps(res) + def _rm_key(_dict): - for key, val in _dict.items(): - if not val: - _dict.pop(key) - return _dict + for key, val in _dict.items(): + if not val: + _dict.pop(key) + return _dict + def progress_bar(self, custom_message=None): - message = custom_message if custom_message else '' - while 1: - barLength, status = 20, "" - progress = float(self.percent) / 100.0 - if progress >= 1.: - progress, status = 1, "\r\n" - block = int(round(barLength * progress)) - text = "\r{} [{}] {:.0f}% {}".format(message, - "#" * block + "-" * (barLength - block), round(progress * 100, 0), status) - sys.stdout.write(text) - sys.stdout.flush() - if self.task_completed: - break + message = custom_message if custom_message else '' + while 1: + barLength, status = 20, "" + progress = float(self.percent) / 100.0 + if progress >= 1.0: + progress, status = 1, "\r\n" + block = int(round(barLength * progress)) + text = "\r{} [{}] {:.0f}% {}".format( + message, + "#" * block + "-" * (barLength - block), + round(progress * 100, 0), + status, + ) + sys.stdout.write(text) + sys.stdout.flush() + if self.task_completed: + break + def log(self, path=None, name=None, log_format=None): - format = '[%(asctime)s] %(levelname)s %(message)s' if not log_format else log_format - name = name if name else '{0}.log'.format(self.task.token) - path = path if path else '' - log_name = '{0}{1}'.format(path, name) - logging.basicConfig(filename=log_name, format=format()) - logging.getLogger().setLevel(logging.INFO) - log = logging.getLogger() - while 1: - log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) - if self.task_completed: - break + format = ( + '[%(asctime)s] %(levelname)s %(message)s' if not log_format else log_format + ) + name = name if name else '{0}.log'.format(self.task.token) + path = path if path else '' + log_name = '{0}{1}'.format(path, name) + logging.basicConfig(filename=log_name, format=format()) + logging.getLogger().setLevel(logging.INFO) + log = logging.getLogger() + while 1: + log.info('{0} | {1} | {2}'.format(self.status, self.percent, self.message)) + if self.task_completed: + break + def get_tus_from_url(url=''): - try: - if url.find('tus:') == 0: - return url - else: - x = url.split('/')[-1] - if x == url: - return url + try: + if url.find('tus:') == 0: + return url else: - return 'tus:' + x - except: - return url \ No newline at end of file + x = url.split('/')[-1] + if x == url: + return url + else: + return 'tus:' + x + except: + return url diff --git a/setup.py b/setup.py index 028d1bc..2d00414 100644 --- a/setup.py +++ b/setup.py @@ -25,11 +25,10 @@ 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: Other/Proprietary License', - 'Programming Language :: Python :: 2.7' - + 'Programming Language :: Python :: 2.7', ], keywords='qencode, qencode.com, cloud.qencode.com', packages=['qencode', 'qencode.drm'], package_data={'qencode.drm': ['keys/buydrm_qencode_public_cert.pem']}, - include_package_data=True + include_package_data=True, ) diff --git a/tests/test_client.py b/tests/test_client.py index 3d92c88..599b5b6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,15 +2,15 @@ class TestQencodeApiClient(unittest.TestCase): - def setUp(self): - pass + pass def tearDown(self): - pass + pass def test_create_task(self): pass + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()