import time import os import re import hashlib import datetime import requests import urllib.parse import logging import functools import traceback from oss2.exceptions import OssError from aliyunsdkcore.acs_exception.exceptions import ServerException from aliyunsdkcore.acs_exception.exceptions import ClientException from django.utils.crypto import get_random_string from django.urls import reverse from .exceptions import VideoException from aliyun_video.settings import video_settings logger = logging.getLogger(__name__) class VideoUtils: """ VOD上传SDK的工具类,提供截取字符串、获取扩展名、获取文件名等静态函数 """ # 截取字符串,在不超过最大字节数前提下确保中文字符不被截断出现乱码(先转换成unicode,再取子串,然后转换成utf-8) @staticmethod def subString(strVal, maxBytes, charSet='utf-8'): i = maxBytes while len(strVal.encode(charSet)) > maxBytes: if i < 0: return '' strVal = strVal[:i] i -= 1 return strVal @staticmethod def getFileExtension(fileName): end = fileName.rfind('?') if end <= 0: end = len(fileName) i = fileName.rfind('.') if i >= 0: return fileName[i + 1:end].lower() else: return None @staticmethod def urlDecode(fileUrl): return urllib.parse.unquote(fileUrl) @staticmethod def urlEncode(fileUrl): return urllib.parse.urlencode(fileUrl) # 获取Url的摘要地址(去除?后的参数,如果有)以及文件名 @staticmethod def getFileBriefPath(fileUrl): # fileUrl = VideoUtils.urlDecode(fileUrl) i = fileUrl.rfind('?') if i > 0: briefPath = fileUrl[:i] else: briefPath = fileUrl briefName = os.path.basename(briefPath) return briefPath, VideoUtils.urlDecode(briefName) @staticmethod def getStringMd5(strVal, isEncode=True): m = hashlib.md5() m.update(strVal.encode('utf-8') if isEncode else strVal) return m.hexdigest() @staticmethod def getCurrentTimeStr(): now = datetime.datetime.now() return now.strftime("%Y-%m-%d %H:%M:%S") # 将oss地址转换为内网地址(如果脚本部署的ecs与oss bucket在同一区域) @staticmethod def convertOssInternal(ossUrl, ecsRegion=None, isVpc=False): if (not ossUrl) or (not ecsRegion): return ossUrl availableRegions = [ 'cn-qingdao', 'cn-beijing', 'cn-zhangjiakou', 'cn-huhehaote', 'cn-hangzhou', 'cn-shanghai', 'cn-shenzhen', 'cn-hongkong', 'ap-southeast-1', 'ap-southeast-2', 'ap-southeast-3', 'ap-northeast-1', 'us-west-1', 'us-east-1', 'eu-central-1', 'me-east-1'] if ecsRegion not in availableRegions: return ossUrl ossUrl = ossUrl.replace("https:", "http:") if isVpc: return ossUrl.replace( "oss-%s.aliyuncs.com" % (ecsRegion), "vpc100-oss-%s.aliyuncs.com" % (ecsRegion)) else: return ossUrl.replace( "oss-%s.aliyuncs.com" % (ecsRegion), "oss-%s-internal.aliyuncs.com" % (ecsRegion)) # 把输入转换为unicode @staticmethod def toUnicode(data): if isinstance(data, bytes): return data.decode('utf-8') else: return data # 替换路径中的文件名;考虑分隔符为"/" 或 "\"(windows) @staticmethod def replaceFileName(filePath, replace): if len(filePath) <= 0 or len(replace) <= 0: return filePath filePath = VideoUtils.urlDecode(filePath) separator = '/' start = filePath.rfind(separator) if start < 0: separator = '\\' start = filePath.rfind(separator) if start < 0: return None result = "%s%s%s" % (filePath[0:start], separator, replace) return result # 创建文件中的目录 @staticmethod def mkDir(filePath): if len(filePath) <= 0: return -1 separator = '/' i = filePath.rfind(separator) if i < 0: separator = '\\' i = filePath.rfind(separator) if i < 0: return -2 dirs = filePath[:i] if os.path.exists(dirs) and os.path.isdir(dirs): return 0 os.makedirs(dirs) return 1 def catch_error(method): """ 装饰器,将内部异常转换成统一的异常类AliyunVodException """ @functools.wraps(method) def wrapper(self, *args, **kwargs): try: return method(self, *args, **kwargs) except ServerException as e: # 可能原因:AK错误、账号无权限、参数错误等 logger.error("ServerException: %s", e) raise VideoException( 'ServerException', e.get_error_code(), e.get_error_msg(), e.get_http_status(), e.get_request_id() ) except ClientException as e: # 可能原因:本地网络故障(如不能连接外网)等 logger.error("ClientException: %s", e) raise VideoException( 'ClientException', e.get_error_code(), e.get_error_msg()) except OssError as e: # 可能原因:上传凭证过期等 logger.error("OssError: %s", e) raise VideoException('OssError', e.code, e.message, e.status, e.request_id) except IOError as e: # 可能原因:文件URL不能访问、本地文件无法读取等 logger.error("IOError: %s", traceback.format_exc()) raise VideoException('IOError', repr(e), traceback.format_exc()) except OSError as e: # 可能原因:本地文件不存在等 raise VideoException('OSError', repr(e), traceback.format_exc()) logger.error("OSError: %s", traceback.format_exc()) except VideoException as e: # 可能原因:参数错误 logger.error("VodException: %s", e) raise e except Exception as e: logger.error("UnkownException: %s", traceback.format_exc()) raise VideoException('UnkownException', repr(e), traceback.format_exc()) return wrapper def get_play_id_from_web_m3u8(m3u8_url): content = requests.get(m3u8_url).content ts_url = '' for line in content.decode('utf-8').split('\n'): if not line.startswith('#'): ts_url = get_path_from_cdn_url(line) break if not ts_url: raise Exception('invalid m3u8 file, `ts url` not found in file') return re.split(r'\-\d+\.ts', ts_url)[0] def build_m3u8_index(lesson_id, video_id, play_id, duration, ts_duration): # what is m3u8: https://en.wikipedia.org/wiki/M3U m3u8_content = """#EXTM3U #EXT-X-VERSION:3 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:{:1.0f} #EXT-X-MEDIA-SEQUENCE:0 """.format(ts_duration) for i in range(int(duration // ts_duration)): m3u8_content += '#EXTINF:10.000000,\n' play_path = reverse( 'lecture:play-ts', kwargs={ 'lesson_id': lesson_id, 'video_id': video_id, 'play_id': play_id, 'seq_num': '{:05d}'.format(i + 1) } ) m3u8_content += '{}\n'.format(play_path) if duration - (i + 1) * ts_duration > 0.001: m3u8_content += '#EXTINF:{:.6f},\n'.format(duration - (i + 1) * ts_duration) play_path = reverse( 'lecture:play-ts', kwargs={ 'lesson_id': lesson_id, 'video_id': video_id, 'play_id': play_id, 'seq_num': '{:05d}'.format(i + 2) } ) m3u8_content += '{}\n'.format(play_path) m3u8_content += '#EXT-X-ENDLIST' return m3u8_content def get_path_from_cdn_url(cdn_url): raw_url = urllib.parse.urlparse(cdn_url) return raw_url.path def get_valid_cdn_url(raw_url): # document: https://www.alibabacloud.com/help/zh/doc-detail/57007.htm raw_url = urllib.parse.urlparse(raw_url) now = int(time.time()) random_string = get_random_string(allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789') return '{cdn_url_prefix}{path}?auth_key={now}-{random_string}-0-{url_hash}'.format( cdn_url_prefix=video_settings.CDN_URL_PREFIX, path=raw_url.path, now=now, random_string=random_string, url_hash=get_cdn_url_hash(raw_url.path, now, random_string) ) def get_cdn_url_hash(path, timestamp, random_string): return hashlib.md5('{path}-{timestamp}-{random_string}-0-{cdn_key}'.format( path=path, timestamp=timestamp, random_string=random_string, cdn_key=video_settings.CDN_ACCESS_TOKEN).encode('utf-8')).hexdigest()