diff --git a/.gitignore b/.gitignore index 515fce8..84d0c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,9 @@ .cache/ .vscode/ .idea/ +.eggs/ build/ -dist/ \ No newline at end of file +dist/ +qencode.egg-info/ +sample-code/drm/buydrm/keys/user_private_key.pem +sample-code/drm/buydrm/keys/user_public_cert.pem 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 193ea64..f11b550 100644 --- a/qencode/__init__.py +++ b/qencode/__init__.py @@ -1,39 +1,49 @@ - 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 from tools import generate_aws_signed_url, fps_drm, cenc_drm -__version__ = "1.0.1" +__version__ = "1.0.4" __status__ = "Production/Stable" __author__ = "Qencode" - - - diff --git a/qencode/client.py b/qencode/client.py index 993043c..af487ae 100644 --- a/qencode/client.py +++ b/qencode/client.py @@ -2,50 +2,64 @@ 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 + + def tasks(self, start_date='', end_date='', project_name='', status='', limit=10, offset=0, sort='date:desc'): + params = { + 'access_token': self.access_token, + 'start_date': start_date, + 'end_date': end_date, + 'project_name': project_name, + 'status': status, + 'limit': limit, + 'offset': offset, + 'sort': sort + } + response = self.connect.request('tasks', params) + return response 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/__init__.py b/qencode/drm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/qencode/drm/buydrm.py b/qencode/drm/buydrm.py new file mode 100644 index 0000000..8ad9fb9 --- /dev/null +++ b/qencode/drm/buydrm.py @@ -0,0 +1,128 @@ +from lxml import etree +from signxml import XMLSigner +import os + +NSMAP = { + 'cpix': 'urn:dashif:org:cpix', + '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#', +} + +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, +): + document_public_cert_path = ( + os.path.dirname(__file__) + '/keys/buydrm_qencode_public_cert.pem' + ) + """Creates CPIX request XML signed end user + + Arguments: + key_ids {list} -- List of Key IDs and corresponding track quality types. The list is of + the following format - { 'kid': [string in GUID/UUID format], 'track_type': [string track type]}. + + media_id {string} -- Some random name for your asset which is shown in KeyOS console and reports. + + nsmap {list} -- List of namespaces. + + Returns: + string -- CPIX request XML signed end user + """ + nsmap = nsmap if nsmap is not None else NSMAP + + # Own private key and end user's private key used to sign the document + end_user_private_key = open(user_private_key_path, 'rb').read() + + # Own public certificate and end user's public certificate to include into the CPIX request + end_user_public_cert = open(user_public_cert_path, 'rb').read() + document_public_cert = open(document_public_cert_path, 'rb').read() + + root = etree.Element('{%s}CPIX' % nsmap['cpix'], name=media_id, nsmap=nsmap) + root.set('{%s}schemaLocation' % nsmap['xsi'], 'urn:dashif:org:cpix cpix.xsd') + + # 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_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', '') + ) + + # Content key list + content_key_list = etree.SubElement(root, '{%s}ContentKeyList' % nsmap['cpix']) + + # Content key usage rules + content_key_usage_list = etree.SubElement( + root, '{%s}ContentKeyUsageRuleList' % nsmap['cpix'] + ) + + # DRM systems list + drm_system_list = etree.SubElement(root, '{%s}DRMSystemList' % nsmap['cpix']) + + for data in key_ids: + etree.SubElement( + content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid'] + ) + if use_playready: + etree.SubElement( + 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, + ) + if use_fairplay: + etree.SubElement( + 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'], + ) + + # 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', + ).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/drm/buydrm_v4.py b/qencode/drm/buydrm_v4.py new file mode 100644 index 0000000..1b376d9 --- /dev/null +++ b/qencode/drm/buydrm_v4.py @@ -0,0 +1,77 @@ +# for python2.7 +import os +from lxml import etree +from signxml import XMLSigner, XMLVerifier +# +import const + +def create_cpix_user_request( + key_ids, media_id, + content_id, commonEncryptionScheme, + private_key_path, public_cert_path, delivery_public_cert_path=None, + use_playready=False, use_widevine=False, use_fairplay=False, + nsmap=const.NSMAP + ): + private_key = open(private_key_path, 'r').read() + public_cert = open(public_cert_path, 'r').read() + if delivery_public_cert_path is None: + delivery_public_cert_path = (os.path.dirname(__file__) + '/keys/qencode-public_cert.pem') + delivery_public_cert = open(delivery_public_cert_path, 'rb').read() + + root = etree.Element('{%s}CPIX' % nsmap['cpix'], + name=media_id, contentId=content_id, nsmap=nsmap) + root.set('{%s}schemaLocation' % nsmap['xsi'], + 'urn:dashif:org:cpix cpix.xsd') + + delivery_data_list = etree.SubElement(root, '{%s}DeliveryDataList' % nsmap['cpix']) + delivery_data = etree.SubElement(delivery_data_list,'{%s}DeliveryData' % nsmap['cpix']) + delivery_key = etree.SubElement(delivery_data, '{%s}DeliveryKey' % nsmap['cpix']) + + x509_data = etree.SubElement(delivery_key, '{%s}X509Data' % nsmap['ds']) + x509_cert = etree.SubElement(x509_data, '{%s}X509Certificate' % nsmap['ds']) + x509_cert.text = delivery_public_cert.replace( + '-----BEGIN CERTIFICATE-----', '' + ).replace( + '-----END CERTIFICATE-----', '' + ).replace('\n', '') + + content_key_list = etree.SubElement(root, + '{%s}ContentKeyList' % nsmap['cpix']) + content_key_usage_list = etree.SubElement(root, + '{%s}ContentKeyUsageRuleList' % nsmap['cpix']) + drm_system_list = etree.SubElement(root, + '{%s}DRMSystemList' % nsmap['cpix']) + + for data in key_ids: + if commonEncryptionScheme == 'default': + etree.SubElement(content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid']) + else: + etree.SubElement(content_key_list, '{%s}ContentKey' % nsmap['cpix'], kid=data['kid'], + commonEncryptionScheme=commonEncryptionScheme) + + if use_playready: + etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=const.SYSTEM_ID_PLAYREADY) + + if use_widevine: + etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=const.SYSTEM_ID_WIDEVINE) + + if use_fairplay: + etree.SubElement(drm_system_list, '{%s}DRMSystem' % nsmap['cpix'], kid=data['kid'], + systemId=const.SYSTEM_ID_FAIRPLAY) + + etree.SubElement(content_key_usage_list, + '{%s}ContentKeyUsageRule' % nsmap['cpix'], + kid=data['kid'], + intendedTrackType=data['track_type']) + + signed_root = XMLSigner( + c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315', + signature_algorithm='rsa-sha256', + digest_algorithm='sha512' + ).sign(root, key=private_key, cert=public_cert) + + xml_text = etree.tostring(signed_root, encoding='utf-8') + + return xml_text diff --git a/qencode/drm/const.py b/qencode/drm/const.py new file mode 100644 index 0000000..a126053 --- /dev/null +++ b/qencode/drm/const.py @@ -0,0 +1,29 @@ +# +CPIX_API_URL_V4 = 'https://cpix-integration.keyos.com/api/v4/getKeys' +LA_URL_V4 = 'https://widevine.keyos.com/api/v4/getLicense' + +NSMAP = { + 'cpix': 'urn:dashif:org:cpix', + 'enc': '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#' +} + +SYSTEM_ID_PLAYREADY = '9a04f079-9840-4286-ab92-e65be0885f95' +SYSTEM_ID_WIDEVINE = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed' +SYSTEM_ID_FAIRPLAY = '94ce86fb-07ff-4f43-adb8-93d2fa968ca2' + +TRACK_TYPES = [ + ('SD', 0), # (<720p) + ('HD', 720), # (720p) + ('FULLHD', 1080), # (1080p) + ('2KUHD', 1440), # (1440p) + ('4KUHD', 2160), # (2160p) + ('8KUHD', 4320), # (4320p) + ('16KUHD', 8640), # (8640p) +] + +DEFAULT_TRACK_TYPE = 'SD' +AUDIO_TRACK_TYPE = 'AUDIO' # SD - old + diff --git a/qencode/drm/keys/buydrm_qencode_public_cert.pem b/qencode/drm/keys/buydrm_qencode_public_cert.pem new file mode 100644 index 0000000..a20877e --- /dev/null +++ b/qencode/drm/keys/buydrm_qencode_public_cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFUzCCAzugAwIBAgIJALYz0aDtoodZMA0GCSqGSIb3DQEBBQUAMEAxCzAJBgNV +BAYTAlVTMRAwDgYDVQQKDAdRZW5jb2RlMR8wHQYDVQQDDBZRZW5jb2RlIEtleU9T +IENQSVggQVBJMB4XDTIwMTIyMTE5MDAyNFoXDTI0MTIyMDE5MDAyNFowQDELMAkG +A1UEBhMCVVMxEDAOBgNVBAoMB1FlbmNvZGUxHzAdBgNVBAMMFlFlbmNvZGUgS2V5 +T1MgQ1BJWCBBUEkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQD2D5aP +XSFJC3ErYVBNS54x8AnQ4997NY2Md3QtvWy1UT/8wzs87EJ+3uwoFgfj8Tpl3hZD +amvW9BH6SlSX2uPu5uuPovxnjMVQTg6RAdfQf+dOff4dCX9ubUD1iWen17DODUoY +BeZWky6zGTy5n6o9RGPJsE6Er/mIFWUJdKPrXMpnqJ+hfC2fWurMj8DZ2U+TiuBF +PK8cPkYzF8+L6BH7vkYK8Tvcoy42WPBMwO/fmFT5nTFCtmMROt/x/DaIdafF4tnx +X+liJ3q7rABZfHY66zXv4owQRrpZXupGLVS7vQtoTIHgA6wc/MWVFEyXZN66D0RI +5/uxRD7/GLYAbGcdCVuyJ8i/eXQ3B7OWcEiOxz3TfojlbQcaO8vb9hym5/B4H8Mh +XC99ddB1hSgNX/bWiA6kfCYtw8jR4g86vyDcU/K2e2IcliZsd6ehFWEGluSLqHJq +2g4d49gP72aFO7x5k95id271MMxhF8CW2VjARSqgHG6G+8cWHqaLnB2JJJE1606P +irDrEAcsSuYLsse393pfZMhOkKdq6FXRK/E5nnbh2VKeWP491lJaXakhMVqcjVQ5 +NRqHc8iXCS7qDUYDxJubwC00b2qVj+HqN+k3gx9kyU1If2S0dXOEyJEBmCScKw6k +glN+a6N5Wv723TKiMjUq+RgcZBbkCvbuoHdN7QIDAQABo1AwTjAdBgNVHQ4EFgQU +n6hFowsmBOhl3vm3lYj6Uu0baDIwHwYDVR0jBBgwFoAUn6hFowsmBOhl3vm3lYj6 +Uu0baDIwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEAvVIt4QBYfCi9 +Ae1SVHGFN6dVSlnSQwC4pBTt8ucrsDs8kE93xyVNMz4OTDZSP0x/AxwV1LFtZOdZ +zauPGygYMf4puVwkns1DqiUkMaGR8PMPQR+SDPeM6qwIhuYLsAyzhKdRx/n+cKmJ +/lzFi20ZJv5y6ozFwUMIskfWnE2RvfV0bbyraR+LQgOA+fy2Vd0IeTOTgXtuNIi/ +q/VMWH2TGjh1vZQmuO1tg8dGU57XFjeK3ZBtcetNwXz6iXzZ5lprGvPG3tr4QRf5 +XiUoS+mJ5mJBBOMWlgGFELA7xAB3cYj00bTE5iE1YZCeE2WHyIiDw6OrILffeU9H +5+bo7R7z5uwn+h5Z+XYJHxS2wg3v7wpoRMFeQXFHlJhdVXlI9cpf0NgNPecnSrs5 +F2hZt1LmbrrFXVzE3BKVvxUILBW+sN9+79fcXAHewL7yPRy6G307Sy187N2RLyJT +mQdOlEaawR2Cgv4QI7NmAuY0Jy0uYqDCP591ZKg+j1lsWi48Eirf3/hMr/TXNDIk +A1MhY+SPW7mGbz6hVQkkJZNYyisAZcelLWyX5X0RwFo1rMQL/BWemdKU3kV4bk8p +cWmsYLPBMN69npjGWLLzovznOWQ4sVAOBt790pgrHDC+giPeqzx2tmmm4jP14IRj +xSPcwUZxCh50J//DsQ2PpVQxrHPnL94= +-----END CERTIFICATE----- diff --git a/qencode/drm/keys/qencode-public_cert.pem b/qencode/drm/keys/qencode-public_cert.pem new file mode 100644 index 0000000..4986de0 --- /dev/null +++ b/qencode/drm/keys/qencode-public_cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFTzCCAzegAwIBAgIUWkfteBfJlQamCwn6AyXY1X4mhTkwDQYJKoZIhvcNAQEL +BQAwNzELMAkGA1UEBhMCVVMxEDAOBgNVBAoMB1FlbmNvZGUxFjAUBgNVBAMMDSou +cWVuY29kZS5jb20wHhcNMjUxMTA2MTc1NjQ0WhcNMjkxMTA2MTc1NjQ0WjA3MQsw +CQYDVQQGEwJVUzEQMA4GA1UECgwHUWVuY29kZTEWMBQGA1UEAwwNKi5xZW5jb2Rl +LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMHuQqzvoFV7mFsE +SNOefpN1hyXQvnQ5qRLt/hyX+0CM4wx+HgCylijPuL4tujcxGdC1QhrD3+ntv9yk +e8Et5PxXIsoxanra6LXXP9UAKljmw2ktfEIBPCyblzCgETeedGH/QqDy+ys8cgQ8 +D/y4wDCwJdWU888qFQaFXgY432tzVLpz4pWqJwrV/EacVk5gVSmT/uFIal+D4rDg +OFi9IAsc9FKVwKnZQoSWsVLoqal/pMVqW6jPMqqQhMClBFVvONBBIC4ztEckLhXu +prkToKHE/sffPvOPoZSzlnz5wTD0WZXjj+RbcFU1mPjZ8lwUBgCn/VTD8SOO5YZx +lpagF+3Qf/nCKjYBupBxvvI9Dfdoai6HvP0clT/99igG9yl0v5viynAW+fNuUhg5 +Krc6rZfw8AgQeJPHgUYElA+rcrhbVamVKLhYhCBAoai12sxMa+EvCwW++I+d/SA0 +9Gft2E1fEWgCyFgzXRKJPPI9CONYe5hL4FitERZhe0oktwbnvT0x9YRU1wb56sG/ +3ykTSaVToGROnjn8reuLRqWgR06cuJjtHqtB+yBbRc5jv1OPL97atzyqePnX5wIN +hiMcse+4IRGR/jtD45AAXQkjwT/MaHjcHpcjaHWUO5w3nV6QIxam7dmXadNgoEbg +a33gqLmj29/RX++nvFOq+9HFD18JAgMBAAGjUzBRMB0GA1UdDgQWBBT8cKty8KH6 +TxiSTunAN7RbyQVkNjAfBgNVHSMEGDAWgBT8cKty8KH6TxiSTunAN7RbyQVkNjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA5XSQVhTpuSdEAhG+e +/tQlbS0yZOuPZPMApzyy8+axTZjitpRZPULvoRKnpldmTDFVRqPwBFXhKWBLufxo +Ghr6GKAG8IZgcr66dx8XquFHIQLooyHpFAx9t7J/SfdWKoKpIugaSDH6Jg4WywoZ +lKjmql9f5QMTRNgOVhDDlMv+vDMQ+30idUODhMwZ36NOlcb0XCeOxE5iGUH3CE4b +XG6RRS2u3rpRH36PL4KFd28uMy73zsxp5ocSrSpDAIibec3qpYSIRVeiGxBZf9q1 +B1q7Ta1BIWzWr7GtkIXxxxLwfnki/QiiEhCX4fUNAXeBH9/XHbDbBUa/qCLsCJ+m +NqLe6AJqea7xo/+KIy+qM3gmn+RFRLhf1NbxX5J+MKIyBdPqtdAvFCcRsbaRSgcP +nIEUYrHJhH+2d3bTKb7Tb9jx1PAXbWXpt4QpnmhQUHtmXGVgoQFIUgNUEsGBnN3n +DMO8n8pav3s1Rwd9c34DKpgC3PkFxosKKNeH2W/r5nmSBNVGAeIPM/81B7VkjmpE +Lrc9yc0V4risNsekYIJpMY+k6nrSklofyyCDalS263toxPrYNrxHjd899zsekBvn +2X+1hvxRQ5BxFgmIBgTS6/vhs/vFDE80cX0te852cV7toNDRcX/72x/lO18FpjUt +cLKMgddCWrOK6HjBetTK1CkKHA== +-----END CERTIFICATE----- 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 5a3d89f..30f11ff 100644 --- a/qencode/httptools.py +++ b/qencode/httptools.py @@ -2,45 +2,51 @@ import urllib import urllib2 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) - try: - res = urllib2.urlopen(request) - 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/sample-code/drm/buydrm/buydrm_widevine_playready.py b/sample-code/drm/buydrm/buydrm_widevine_playready.py new file mode 100644 index 0000000..eb24523 --- /dev/null +++ b/sample-code/drm/buydrm/buydrm_widevine_playready.py @@ -0,0 +1,79 @@ +import base64 +import json +import uuid +import time +import qencode +from qencode.drm.buydrm_v4 import create_cpix_user_request +from qencode import QencodeClientException, QencodeTaskException + +# replace with your API KEY (can be found in your Project settings on Qencode portal) +# https://portal.qencode.com/ +API_KEY = 'your-api-qencode-key' + +# specify path to your BuyDRM certificate files, +# for example create dir keys/ and put keys into +USER_PVT_KEY_PATH = 'keys/user-private_key.pem' +USER_PUB_CERT_PATH = 'keys/user-public_cert.pem' + +# Qencode query template for job with {cpix_request} +query_json = 'query.json' +# correspond to stream resolution in query.json +key_ids = [ + { 'kid': str(uuid.uuid4()), 'track_type': 'SD' }, + { 'kid': str(uuid.uuid4()), 'track_type': 'HD' } +] + +media_id = 'my first stream' +content_id = 'my movies group' +common_encryption = 'cenc' + +# unified with the new BuyDRM API params +drm_list = { + 'PR': True, # use_playready + 'WV': True, # use_widevine + 'FP': False # use_fairplay +} + +def start_encode(): + cpix_request = create_cpix_user_request( + key_ids, media_id, + content_id, common_encryption, + USER_PVT_KEY_PATH, USER_PUB_CERT_PATH, + use_playready=drm_list['PR'], use_widevine=drm_list['WV'], use_fairplay=drm_list['FP'] + ) + + client = qencode.client(API_KEY) + if client.error: + raise QencodeClientException(client.message) + + print('The client created. Expire date: %s' % client.expire) + + task = client.create_task() + + if task.error: + raise QencodeTaskException(task.message) + + template = open(query_json, 'r').read() + + query = template.replace('{cpix_request}', base64.b64encode(cpix_request)) + + task.custom_start(query) + + if task.error: + raise QencodeTaskException(task.message) + task_token = task.task_token + print('Start encode. Task: %s' % task_token) + + while True: + status = task.status() + print('Job %s status: \n %s' % (task_token, json.dumps(status, indent=2, sort_keys=True))) + if status['error'] or status['status'] == 'completed': + break + time.sleep(5) + status = task.extend_status() + print('Job %s finished with status "%s" and error: %s' % \ + (task_token, status['status'], status['error']) + ) + +if __name__ == '__main__': + start_encode() diff --git a/sample-code/drm/buydrm/query.json b/sample-code/drm/buydrm/query.json new file mode 100644 index 0000000..476879a --- /dev/null +++ b/sample-code/drm/buydrm/query.json @@ -0,0 +1,23 @@ +{ + "query": { + "encoder_version": "2", + "format": [ + { + "output": "advanced_dash", + "stream": [ + { + "video_codec": "libx264", + "height": 360, + "audio_bitrate": 128, + "keyframe": 25, + "bitrate": 950 + } + ], + "buydrm_drm": { + "request": "{cpix_request}" + } + } + ], + "source": "https://nyc3.s3.qencode.com/qencode/bbb_30s.mp4" + } +} \ No newline at end of file diff --git a/sample-code/start_custom_hls.py b/sample-code/start_custom_hls.py index 3d9728d..0c5411d 100644 --- a/sample-code/start_custom_hls.py +++ b/sample-code/start_custom_hls.py @@ -36,7 +36,7 @@ FORMAT.destination = DESTINATION #replace with a link to your input video -params.source = 'https://qencode.com/static/1.mp4' +params.source = 'https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4' params.format = [FORMAT] diff --git a/sample-code/start_custom_mp4.py b/sample-code/start_custom_mp4.py index 9541e4c..3dd94e4 100644 --- a/sample-code/start_custom_mp4.py +++ b/sample-code/start_custom_mp4.py @@ -29,7 +29,7 @@ FORMAT.destination = DESTINATION #replace with a link to your input video -params.source = 'https://qencode.com/static/1.mp4' +params.source = 'https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4' params.format = [FORMAT] diff --git a/sample-code/start_custom_with_dict.py b/sample-code/start_custom_with_dict.py index 32601c3..c5b38ff 100644 --- a/sample-code/start_custom_with_dict.py +++ b/sample-code/start_custom_with_dict.py @@ -14,7 +14,7 @@ API_KEY = 'your-api-qencode-key' #replace with a link to your input video -source_url = "https://qencode.com/static/1.mp4" +source_url = "https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4" format_240 = dict( output="mp4", diff --git a/sample-code/start_custom_with_json.py b/sample-code/start_custom_with_json.py index 11d8f01..5101cae 100644 --- a/sample-code/start_custom_with_json.py +++ b/sample-code/start_custom_with_json.py @@ -14,7 +14,7 @@ params = """ {"query": { - "source": "https://qencode.com/static/1.mp4", + "source": "https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4", "format": [ { "output": "mp4", diff --git a/sample-code/start_with_callback.py b/sample-code/start_with_callback.py index 35e3cfc..072d09d 100644 --- a/sample-code/start_with_callback.py +++ b/sample-code/start_with_callback.py @@ -14,7 +14,7 @@ params = """ {"query": { - "source": "https://qencode.com/static/1.mp4", + "source": "https://nyc3.digitaloceanspaces.com/qencode/bbb_30s.mp4", "format": [ { "output": "mp4", diff --git a/setup.py b/setup.py index ab77250..2d00414 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='qencode', - version='1.0.1', + version='1.0.4', description="Client library for main features and functionality of Qencode for Python v2.x.", long_description=long_description, long_description_content_type='text/markdown', @@ -25,9 +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'] + packages=['qencode', 'qencode.drm'], + package_data={'qencode.drm': ['keys/buydrm_qencode_public_cert.pem']}, + 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()