The regex patterns for dailymotion and youku were too permissive and not specific enough for the way they were intended to be used. The new regexes should correctly isolate the video ID from the URL string and cover all embedding needs for the two providers. There are also new test cases for specific embed URL variations. For the initiating discussion and PR, see following link: https://github.com/odoo/odoo/pull/44537#discussion_r753250276 task-2696033 closes odoo/odoo#82369 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
146 lines
5.7 KiB
Python
146 lines
5.7 KiB
Python
# -*- encoding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import re
|
|
import requests
|
|
|
|
from markupsafe import Markup
|
|
from werkzeug.urls import url_encode
|
|
|
|
from odoo import _
|
|
from odoo.tools import image_process
|
|
|
|
# To detect if we have a valid URL or not
|
|
valid_url_regex = r'^(http://|https://|//)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?$'
|
|
|
|
# Regex for few of the widely used video hosting services
|
|
player_regexes = {
|
|
'youtube': r'^(?:(?:https?:)?//)?(?:www\.)?(?:youtu\.be/|youtube(-nocookie)?\.com/(?:embed/|v/|watch\?v=|watch\?.+&v=))((?:\w|-){11})\S*$',
|
|
'vimeo': r'//(player.)?vimeo.com/([a-z]*/)*([0-9]{6,11})[?]?.*',
|
|
'dailymotion': r'(https?:\/\/)(www\.)?(dailymotion\.com\/(embed\/video\/|embed\/|video\/|hub\/.*#video=)|dai\.ly\/)(?P<id>[A-Za-z0-9]{6,7})',
|
|
'instagram': r'(?:(.*)instagram.com|instagr\.am)/p/(.[a-zA-Z0-9-_\.]*)',
|
|
'youku': r'(?:(https?:\/\/)?(v\.youku\.com/v_show/id_|player\.youku\.com/player\.php/sid/|player\.youku\.com/embed/|cloud\.youku\.com/services/sharev\?vid=|video\.tudou\.com/v/)|youku:)(?P<id>[A-Za-z0-9]+)(?:\.html|/v\.swf|)',
|
|
}
|
|
|
|
|
|
def get_video_source_data(video_url):
|
|
""" Computes the valid source, document ID and regex match from given URL
|
|
(or None in case of invalid URL).
|
|
"""
|
|
if not video_url:
|
|
return None
|
|
|
|
if re.search(valid_url_regex, video_url):
|
|
youtube_match = re.search(player_regexes['youtube'], video_url)
|
|
if youtube_match:
|
|
return ('youtube', youtube_match[2], youtube_match)
|
|
vimeo_match = re.search(player_regexes['vimeo'], video_url)
|
|
if vimeo_match:
|
|
return ('vimeo', vimeo_match[3], vimeo_match)
|
|
dailymotion_match = re.search(player_regexes['dailymotion'], video_url)
|
|
if dailymotion_match:
|
|
return ('dailymotion', dailymotion_match.group("id"), dailymotion_match)
|
|
instagram_match = re.search(player_regexes['instagram'], video_url)
|
|
if instagram_match:
|
|
return ('instagram', instagram_match[2], instagram_match)
|
|
youku_match = re.search(player_regexes['youku'], video_url)
|
|
if youku_match:
|
|
return ('youku', youku_match.group("id"), youku_match)
|
|
return None
|
|
|
|
|
|
def get_video_url_data(video_url, autoplay=False, loop=False, hide_controls=False, hide_fullscreen=False, hide_yt_logo=False, hide_dm_logo=False, hide_dm_share=False):
|
|
""" Computes the platform name and embed_url from given URL
|
|
(or error message in case of invalid URL).
|
|
"""
|
|
source = get_video_source_data(video_url)
|
|
if source is None:
|
|
return {'error': True, 'message': _('The provided url is invalid')}
|
|
|
|
embed_url = video_url
|
|
platform, video_id, platform_match = source
|
|
|
|
params = {}
|
|
|
|
if platform == 'youtube':
|
|
params['rel'] = 0
|
|
params['autoplay'] = autoplay and 1 or 0
|
|
if autoplay:
|
|
params['mute'] = 1
|
|
if hide_controls:
|
|
params['controls'] = 0
|
|
if loop:
|
|
params['loop'] = 1
|
|
params['playlist'] = video_id
|
|
if hide_fullscreen:
|
|
params['fs'] = 0
|
|
if hide_yt_logo:
|
|
params['modestbranding'] = 1
|
|
yt_extra = platform_match[1] or ''
|
|
embed_url = f'//www.youtube{yt_extra}.com/embed/{video_id}'
|
|
elif platform == 'vimeo':
|
|
params['autoplay'] = autoplay and 1 or 0
|
|
if autoplay:
|
|
params['muted'] = 1
|
|
if loop:
|
|
params['loop'] = 1
|
|
embed_url = f'//player.vimeo.com/video/{video_id}'
|
|
elif platform == 'dailymotion':
|
|
params['autoplay'] = autoplay and 1 or 0
|
|
if autoplay:
|
|
params['mute'] = 1
|
|
if hide_controls:
|
|
params['controls'] = 0
|
|
if hide_dm_logo:
|
|
params['ui-logo'] = 0
|
|
if hide_dm_share:
|
|
params['sharing-enable'] = 0
|
|
embed_url = f'//www.dailymotion.com/embed/video/{video_id}'
|
|
elif platform == 'instagram':
|
|
embed_url = f'//www.instagram.com/p/{video_id}/embed/'
|
|
elif platform == 'youku':
|
|
embed_url = f'//player.youku.com/embed/{video_id}'
|
|
|
|
if params:
|
|
embed_url = f'{embed_url}?{url_encode(params)}'
|
|
|
|
return {'platform': platform, 'embed_url': embed_url}
|
|
|
|
|
|
def get_video_embed_code(video_url):
|
|
""" Computes the valid iframe from given URL that can be embedded
|
|
(or None in case of invalid URL).
|
|
"""
|
|
data = get_video_url_data(video_url)
|
|
if 'error' in data:
|
|
return None
|
|
return Markup('<iframe class="embed-responsive-item" src="%s" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen="true" frameborder="0"></iframe>') % data['embed_url']
|
|
|
|
|
|
def get_video_thumbnail(video_url):
|
|
""" Computes the valid thumbnail image from given URL
|
|
(or None in case of invalid URL).
|
|
"""
|
|
source = get_video_source_data(video_url)
|
|
if source is None:
|
|
return None
|
|
|
|
response = None
|
|
platform, video_id = source[:2]
|
|
if platform == 'youtube':
|
|
response = requests.get(f'https://img.youtube.com/vi/{video_id}/0.jpg', timeout=10)
|
|
elif platform == 'vimeo':
|
|
res = requests.get(f'http://vimeo.com/api/oembed.json?url={video_url}', timeout=10)
|
|
if res.ok:
|
|
data = res.json()
|
|
response = requests.get(data['thumbnail_url'], timeout=10)
|
|
elif platform == 'dailymotion':
|
|
response = requests.get(f'https://www.dailymotion.com/thumbnail/video/{video_id}', timeout=10)
|
|
elif platform == 'instagram':
|
|
response = requests.get(f'https://www.instagram.com/p/{video_id}/media/?size=t', timeout=10)
|
|
|
|
if response and response.ok:
|
|
return image_process(response.content)
|
|
return None
|