[IMP] website, *: improve video providers handling
*: web_editor, website_sale This commit adds the abibility to get an image thumbnail based on the video provider URL and to avoid duplicating the regexes. It also improves those URL regexes. All the regexes are kept in a python tools file and are called in JS using an RPC. task-2154812 closes odoo/odoo#44537 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com> Co-authored-by: ras-odoo <ras@odoo.com> Co-authored-by: bbh-odoo <bbh@odoo.com>
This commit is contained in:
committed by
qsm-odoo
co-authored by
ras-odoo
bbh-odoo
parent
1de5dc4b96
commit
2dd772dab8
@@ -15,6 +15,7 @@ from base64 import b64decode, b64encode
|
||||
from odoo.http import request
|
||||
from odoo import http, tools, _, SUPERUSER_ID
|
||||
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
||||
from odoo.addons.web_editor.tools import get_video_url_data
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.modules.module import get_resource_path
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
@@ -174,6 +175,19 @@ class Web_Editor(http.Controller):
|
||||
|
||||
return True
|
||||
|
||||
@http.route('/web_editor/video_url/data', type='json', auth='user', website=True)
|
||||
def video_url_data(self, video_url, autoplay=False, loop=False,
|
||||
hide_controls=False, hide_fullscreen=False, hide_yt_logo=False,
|
||||
hide_dm_logo=False, hide_dm_share=False):
|
||||
if not request.env.user.has_group('base.group_user'):
|
||||
raise werkzeug.exceptions.Forbidden()
|
||||
return get_video_url_data(
|
||||
video_url, autoplay=autoplay, loop=loop,
|
||||
hide_controls=hide_controls, hide_fullscreen=hide_fullscreen,
|
||||
hide_yt_logo=hide_yt_logo, hide_dm_logo=hide_dm_logo,
|
||||
hide_dm_share=hide_dm_share
|
||||
)
|
||||
|
||||
@http.route('/web_editor/attachment/add_data', type='json', auth='user', methods=['POST'], website=True)
|
||||
def add_data(self, name, data, is_image, quality=0, width=0, height=0, res_id=False, res_model='ir.ui.view', **kwargs):
|
||||
if is_image:
|
||||
|
||||
@@ -1228,7 +1228,8 @@ var VideoWidget = MediaWidget.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
start: async function () {
|
||||
const _super = this._super.bind(this);
|
||||
this.$content = this.$('.o_video_dialog_iframe');
|
||||
|
||||
if (this.media) {
|
||||
@@ -1244,7 +1245,7 @@ var VideoWidget = MediaWidget.extend({
|
||||
this.$('input#o_video_hide_dm_logo').prop('checked', src.indexOf('ui-logo=0') >= 0);
|
||||
this.$('input#o_video_hide_dm_share').prop('checked', src.indexOf('sharing-enable=0') >= 0);
|
||||
|
||||
this._updateVideo();
|
||||
await this._updateVideo();
|
||||
}
|
||||
|
||||
// loads the thumbnail of vimeo video previews.
|
||||
@@ -1264,7 +1265,7 @@ var VideoWidget = MediaWidget.extend({
|
||||
});
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
return _super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -1274,8 +1275,8 @@ var VideoWidget = MediaWidget.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
save: function () {
|
||||
this._updateVideo();
|
||||
save: async function () {
|
||||
await this._updateVideo();
|
||||
const videoSrc = this.$content.attr('src');
|
||||
if (this.isForBgVideo) {
|
||||
return Promise.resolve({bgVideoSrc: videoSrc});
|
||||
@@ -1337,28 +1338,28 @@ var VideoWidget = MediaWidget.extend({
|
||||
* errorCode -> if defined, either '0' for invalid URL or '1' for
|
||||
* unsupported video provider
|
||||
*/
|
||||
_createVideoNode: function (url, options) {
|
||||
_createVideoNode: async function (url, options) {
|
||||
options = options || {};
|
||||
const videoData = this._getVideoURLData(url, options);
|
||||
const videoData = await this._getVideoURLData(url, options);
|
||||
if (videoData.error) {
|
||||
return {errorCode: 0};
|
||||
}
|
||||
if (!videoData.type) {
|
||||
if (!videoData.platform) {
|
||||
return {errorCode: 1};
|
||||
}
|
||||
const $video = $('<iframe>').width(1280).height(720)
|
||||
.attr('frameborder', 0)
|
||||
.attr('src', videoData.embedURL)
|
||||
.attr('src', videoData.embed_url)
|
||||
.addClass('o_video_dialog_iframe');
|
||||
|
||||
return {$video: $video, type: videoData.type};
|
||||
return {$video: $video, platform: videoData.platform};
|
||||
},
|
||||
/**
|
||||
* Updates the video preview according to video code and enabled options.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_updateVideo: function () {
|
||||
_updateVideo: async function () {
|
||||
// Reset the feedback
|
||||
this.$content.empty();
|
||||
this.$('#o_video_form_group').removeClass('o_has_error o_has_success').find('.form-control, .custom-select').removeClass('is-invalid is-valid');
|
||||
@@ -1378,7 +1379,7 @@ var VideoWidget = MediaWidget.extend({
|
||||
}
|
||||
var url = embedMatch ? embedMatch[1] : code;
|
||||
|
||||
var query = this._createVideoNode(url, {
|
||||
const query = await this._createVideoNode(url, {
|
||||
'autoplay': this.isForBgVideo || this.$('input#o_video_autoplay').is(':checked'),
|
||||
'hide_controls': this.isForBgVideo || this.$('input#o_video_hide_controls').is(':checked'),
|
||||
'loop': this.isForBgVideo || this.$('input#o_video_loop').is(':checked'),
|
||||
@@ -1399,13 +1400,13 @@ var VideoWidget = MediaWidget.extend({
|
||||
.toggleClass('o_has_success', !!query.$video).find('.form-control, .custom-select').toggleClass('is-valid', !!query.$video);
|
||||
|
||||
// Individually show / hide options base on the video provider
|
||||
$optBox.find('div.o_' + query.type + '_option').removeClass('d-none');
|
||||
$optBox.find('div.o_' + query.platform + '_option').removeClass('d-none');
|
||||
|
||||
// Hide the entire options box if no options are available or if the
|
||||
// dialog is opened for a background-video
|
||||
$optBox.toggleClass('d-none', this.isForBgVideo || $optBox.find('div:not(.d-none)').length === 0);
|
||||
|
||||
if (query.type === 'youtube') {
|
||||
if (query.platform === 'youtube') {
|
||||
// Youtube only: If 'hide controls' is checked, hide 'fullscreen'
|
||||
// and 'youtube logo' options too
|
||||
this.$('input#o_video_hide_fullscreen, input#o_video_hide_yt_logo').closest('div').toggleClass('d-none', this.$('input#o_video_hide_controls').is(':checked'));
|
||||
@@ -1481,56 +1482,10 @@ var VideoWidget = MediaWidget.extend({
|
||||
* @private
|
||||
*/
|
||||
_getVideoURLData: function (url, options) {
|
||||
if (!url.match(/^(http:\/\/|https:\/\/|\/\/)[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i)) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'The provided url is invalid',
|
||||
};
|
||||
}
|
||||
const regexes = {
|
||||
youtube: /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube(-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((?:\w|-){11})(?:\S+)?$/,
|
||||
instagram: /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/,
|
||||
vine: /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/,
|
||||
vimeo: /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/,
|
||||
dailymotion: /.+dailymotion.com\/(video|hub|embed)\/([^_?]+)[^#]*(#video=([^_&]+))?/,
|
||||
youku: /(.*).youku\.com\/(v_show\/id_|embed\/)(.+)/,
|
||||
};
|
||||
const matches = _.mapObject(regexes, regex => url.match(regex));
|
||||
const autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';
|
||||
const controls = options.hide_controls ? '&controls=0' : '';
|
||||
const loop = options.loop ? '&loop=1' : '';
|
||||
|
||||
let embedURL;
|
||||
let type;
|
||||
if (matches.youtube && matches.youtube[2].length === 11) {
|
||||
const fullscreen = options.hide_fullscreen ? '&fs=0' : '';
|
||||
const ytLoop = loop ? loop + `&playlist=${matches.youtube[2]}` : '';
|
||||
const logo = options.hide_yt_logo ? '&modestbranding=1' : '';
|
||||
embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${matches.youtube[2]}${autoplay}&rel=0${ytLoop}${controls}${fullscreen}${logo}`;
|
||||
type = 'youtube';
|
||||
} else if (matches.instagram && matches.instagram[2].length) {
|
||||
embedURL = `//www.instagram.com/p/${matches.instagram[2]}/embed/`;
|
||||
type = 'instagram';
|
||||
} else if (matches.vine && matches.vine[0].length) {
|
||||
embedURL = `${matches.vine[0]}/embed/simple`;
|
||||
type = 'vine';
|
||||
} else if (matches.vimeo && matches.vimeo[3].length) {
|
||||
const vimeoAutoplay = autoplay.replace('mute', 'muted');
|
||||
embedURL = `//player.vimeo.com/video/${matches.vimeo[3]}${vimeoAutoplay}${loop}`;
|
||||
type = 'vimeo';
|
||||
} else if (matches.dailymotion && matches.dailymotion[2].length) {
|
||||
const videoId = matches.dailymotion[2].replace('video/', '');
|
||||
const logo = options.hide_dm_logo ? '&ui-logo=0' : '';
|
||||
const share = options.hide_dm_share ? '&sharing-enable=0' : '';
|
||||
embedURL = `//www.dailymotion.com/embed/video/${videoId}${autoplay}${controls}${logo}${share}`;
|
||||
type = 'dailymotion';
|
||||
} else if (matches.youku && matches.youku[3].length) {
|
||||
const videoId = matches.youku[3].indexOf('.html?') >= 0 ? matches.youku[3].substring(0, matches.youku[3].indexOf('.html?')) : matches.youku[3];
|
||||
embedURL = `//player.youku.com/embed/${videoId}`;
|
||||
type = 'youku';
|
||||
}
|
||||
|
||||
return {type: type, embedURL: embedURL};
|
||||
return this._rpc({
|
||||
route: '/web_editor/video_url/data',
|
||||
params: Object.assign({video_url: url}, options),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import test_converter
|
||||
from . import test_odoo_editor
|
||||
from . import test_views
|
||||
from . import test_tools
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
from odoo.tests import common, tagged
|
||||
from odoo.addons.web_editor import tools
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestVideoUtils(common.BaseCase):
|
||||
urls = {
|
||||
'youtube': 'https://www.youtube.com/watch?v=xCvFZrrQq7k',
|
||||
'vimeo': 'https://vimeo.com/395399735',
|
||||
'dailymotion': 'https://www.dailymotion.com/video/x7svr6t',
|
||||
'youku': 'https://v.youku.com/v_show/id_XMzY1MjY4.html?spm=a2hzp.8244740.0.0',
|
||||
'instagram': 'https://www.instagram.com/p/B6dXGTxggTG/'
|
||||
}
|
||||
|
||||
def test_player_regexes(self):
|
||||
#youtube
|
||||
self.assertIsNotNone(re.search(tools.player_regexes['youtube'], TestVideoUtils.urls['youtube']))
|
||||
#vimeo
|
||||
self.assertIsNotNone(re.search(tools.player_regexes['vimeo'], TestVideoUtils.urls['vimeo']))
|
||||
#dailymotion
|
||||
self.assertIsNotNone(re.search(tools.player_regexes['dailymotion'], TestVideoUtils.urls['dailymotion']))
|
||||
#youku
|
||||
self.assertIsNotNone(re.search(tools.player_regexes['youku'], TestVideoUtils.urls['youku']))
|
||||
#instagram
|
||||
self.assertIsNotNone(re.search(tools.player_regexes['instagram'], TestVideoUtils.urls['instagram']))
|
||||
|
||||
def test_get_video_source_data(self):
|
||||
self.assertEqual(3, len(tools.get_video_source_data(TestVideoUtils.urls['youtube'])))
|
||||
#youtube
|
||||
self.assertEqual('youtube', tools.get_video_source_data(TestVideoUtils.urls['youtube'])[0])
|
||||
self.assertEqual('xCvFZrrQq7k', tools.get_video_source_data(TestVideoUtils.urls['youtube'])[1])
|
||||
#vimeo
|
||||
self.assertEqual('vimeo', tools.get_video_source_data(TestVideoUtils.urls['vimeo'])[0])
|
||||
self.assertEqual('395399735', tools.get_video_source_data(TestVideoUtils.urls['vimeo'])[1])
|
||||
#dailymotion
|
||||
self.assertEqual('dailymotion', tools.get_video_source_data(TestVideoUtils.urls['dailymotion'])[0])
|
||||
self.assertEqual('x7svr6t', tools.get_video_source_data(TestVideoUtils.urls['dailymotion'])[1])
|
||||
#youku
|
||||
self.assertEqual('youku', tools.get_video_source_data(TestVideoUtils.urls['youku'])[0])
|
||||
self.assertEqual('XMzY1MjY4', tools.get_video_source_data(TestVideoUtils.urls['youku'])[1])
|
||||
#instagram
|
||||
self.assertEqual('instagram', tools.get_video_source_data(TestVideoUtils.urls['instagram'])[0])
|
||||
self.assertEqual('B6dXGTxggTG', tools.get_video_source_data(TestVideoUtils.urls['instagram'])[1])
|
||||
|
||||
def test_get_video_url_data(self):
|
||||
self.assertEqual(2, len(tools.get_video_url_data(TestVideoUtils.urls['youtube'])))
|
||||
#youtube
|
||||
self.assertEqual('youtube', tools.get_video_url_data(TestVideoUtils.urls['youtube'])['platform'])
|
||||
#vimeo
|
||||
self.assertEqual('vimeo', tools.get_video_url_data(TestVideoUtils.urls['vimeo'])['platform'])
|
||||
#dailymotion
|
||||
self.assertEqual('dailymotion', tools.get_video_url_data(TestVideoUtils.urls['dailymotion'])['platform'])
|
||||
#youku
|
||||
self.assertEqual('youku', tools.get_video_url_data(TestVideoUtils.urls['youku'])['platform'])
|
||||
#instagram
|
||||
self.assertEqual('instagram', tools.get_video_url_data(TestVideoUtils.urls['instagram'])['platform'])
|
||||
|
||||
def test_valid_video_url(self):
|
||||
self.assertIsNotNone(re.search(tools.valid_url_regex, TestVideoUtils.urls['youtube']))
|
||||
|
||||
|
||||
@tagged('-standard', 'external')
|
||||
class TestVideoUtilsExternal(common.BaseCase):
|
||||
def test_get_video_thumbnail(self):
|
||||
#youtube
|
||||
self.assertIsInstance(tools.get_video_thumbnail(TestVideoUtils.urls['youtube']), bytes)
|
||||
#vimeo
|
||||
self.assertIsInstance(tools.get_video_thumbnail(TestVideoUtils.urls['vimeo']), bytes)
|
||||
#dailymotion
|
||||
self.assertIsInstance(tools.get_video_thumbnail(TestVideoUtils.urls['dailymotion']), bytes)
|
||||
#instagram
|
||||
self.assertIsInstance(tools.get_video_thumbnail(TestVideoUtils.urls['instagram']), bytes)
|
||||
#default
|
||||
self.assertIsInstance(tools.get_video_thumbnail(TestVideoUtils.urls['youku']), bytes)
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- 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'(?:.+dailymotion.com/(video|hub|embed/video|embed)|dai\.ly)/([^_?]+)[^#]*(#video=([^_&]+))?',
|
||||
'instagram': r'(?:(.*)instagram.com|instagr\.am)/p/(.[a-zA-Z0-9-_\.]*)',
|
||||
'youku': r'(.*).youku\.com/(v_show/id_|embed/)(.+)',
|
||||
}
|
||||
|
||||
|
||||
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[2], 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:
|
||||
youku_link = youku_match[3]
|
||||
if '.html?' in youku_link:
|
||||
youku_link = youku_link.split('.html?')[0]
|
||||
return ('youku', youku_link, 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(base64.b64encode(response.content))
|
||||
return None
|
||||
@@ -1,9 +1,7 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import contextlib
|
||||
import re
|
||||
from lxml import etree
|
||||
from psycopg2 import sql
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
|
||||
import werkzeug
|
||||
@@ -12,53 +10,6 @@ import odoo
|
||||
from odoo.tools.misc import DotDict
|
||||
|
||||
|
||||
def get_video_embed_code(video_url):
|
||||
''' Computes the valid iframe from given URL that can be embedded
|
||||
(or False in case of invalid URL).
|
||||
'''
|
||||
|
||||
if not video_url:
|
||||
return False
|
||||
|
||||
# To detect if we have a valid URL or not
|
||||
validURLRegex = 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
|
||||
ytRegex = r'^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube(-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((?:\w|-){11})(?:\S+)?$'
|
||||
vimeoRegex = r'\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*'
|
||||
dmRegex = r'.+dailymotion.com\/(video|hub|embed)\/([^_?]+)[^#]*(#video=([^_&]+))?'
|
||||
igRegex = r'(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)'
|
||||
ykuRegex = r'(.*).youku\.com\/(v_show\/id_|embed\/)(.+)'
|
||||
|
||||
if not re.search(validURLRegex, video_url):
|
||||
return False
|
||||
else:
|
||||
embedUrl = False
|
||||
ytMatch = re.search(ytRegex, video_url)
|
||||
vimeoMatch = re.search(vimeoRegex, video_url)
|
||||
dmMatch = re.search(dmRegex, video_url)
|
||||
igMatch = re.search(igRegex, video_url)
|
||||
ykuMatch = re.search(ykuRegex, video_url)
|
||||
|
||||
if ytMatch and len(ytMatch.groups()[1]) == 11:
|
||||
embedUrl = '//www.youtube%s.com/embed/%s?rel=0' % (ytMatch.groups()[0] or '', ytMatch.groups()[1])
|
||||
elif vimeoMatch:
|
||||
embedUrl = '//player.vimeo.com/video/%s' % (vimeoMatch.groups()[2])
|
||||
elif dmMatch:
|
||||
embedUrl = '//www.dailymotion.com/embed/video/%s' % (dmMatch.groups()[1])
|
||||
elif igMatch:
|
||||
embedUrl = '//www.instagram.com/p/%s/embed/' % (igMatch.groups()[1])
|
||||
elif ykuMatch:
|
||||
ykuLink = ykuMatch.groups()[2]
|
||||
if '.html?' in ykuLink:
|
||||
ykuLink = ykuLink.split('.html?')[0]
|
||||
embedUrl = '//player.youku.com/embed/%s' % (ykuLink)
|
||||
else:
|
||||
# We directly use the provided URL as it is
|
||||
embedUrl = video_url
|
||||
return '<iframe class="embed-responsive-item" src="%s" allowFullScreen="true" frameborder="0"></iframe>' % embedUrl
|
||||
|
||||
|
||||
def werkzeugRaiseNotFound(*args, **kwargs):
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.website.tools import get_video_embed_code
|
||||
from odoo.addons.web_editor.tools import get_video_embed_code, get_video_thumbnail
|
||||
|
||||
|
||||
class ProductImage(models.Model):
|
||||
@@ -16,7 +16,7 @@ class ProductImage(models.Model):
|
||||
name = fields.Char("Name", required=True)
|
||||
sequence = fields.Integer(default=10, index=True)
|
||||
|
||||
image_1920 = fields.Image(required=True)
|
||||
image_1920 = fields.Image()
|
||||
|
||||
product_tmpl_id = fields.Many2one('product.template', "Product Template", index=True, ondelete='cascade')
|
||||
product_variant_id = fields.Many2one('product.product', "Product Variant", index=True, ondelete='cascade')
|
||||
@@ -31,10 +31,15 @@ class ProductImage(models.Model):
|
||||
for image in self:
|
||||
image.can_image_1024_be_zoomed = image.image_1920 and tools.is_image_size_above(image.image_1920, image.image_1024)
|
||||
|
||||
@api.onchange('video_url')
|
||||
def _onchange_video_url(self):
|
||||
if not self.image_1920:
|
||||
self.image_1920 = get_video_thumbnail(self.video_url) or False
|
||||
|
||||
@api.depends('video_url')
|
||||
def _compute_embed_code(self):
|
||||
for image in self:
|
||||
image.embed_code = get_video_embed_code(image.video_url)
|
||||
image.embed_code = get_video_embed_code(image.video_url) or False
|
||||
|
||||
@api.constrains('video_url')
|
||||
def _check_valid_video_url(self):
|
||||
|
||||
Reference in New Issue
Block a user