From 2dd772dab89cbe5ee9354b7242b83c8b8ab9bb2d Mon Sep 17 00:00:00 2001 From: jpr-odoo Date: Tue, 28 Jan 2020 09:13:47 +0000 Subject: [PATCH] [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) Co-authored-by: ras-odoo Co-authored-by: bbh-odoo --- addons/web_editor/controllers/main.py | 14 ++ .../static/src/js/wysiwyg/widgets/media.js | 83 +++------- addons/web_editor/tests/__init__.py | 1 + addons/web_editor/tests/test_tools.py | 79 ++++++++++ addons/web_editor/tools.py | 148 ++++++++++++++++++ addons/website/tools.py | 49 ------ addons/website_sale/models/product_image.py | 11 +- 7 files changed, 269 insertions(+), 116 deletions(-) create mode 100644 addons/web_editor/tests/test_tools.py create mode 100644 addons/web_editor/tools.py diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 5f46d8f1df2..fce6212b224 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -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: diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index 97bd2abceca..966c1e747bd 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -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 = $('') % 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 diff --git a/addons/website/tools.py b/addons/website/tools.py index 6b47811e386..546757463df 100644 --- a/addons/website/tools.py +++ b/addons/website/tools.py @@ -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 '' % embedUrl - - def werkzeugRaiseNotFound(*args, **kwargs): raise werkzeug.exceptions.NotFound() diff --git a/addons/website_sale/models/product_image.py b/addons/website_sale/models/product_image.py index c8cdb4aaf06..526256e9ca1 100644 --- a/addons/website_sale/models/product_image.py +++ b/addons/website_sale/models/product_image.py @@ -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):