diff --git a/.gitignore b/.gitignore
index ad4ff4d9294..0e726f326b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,7 +38,7 @@
!/plugins/discourse-local-dates
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
-!/plugins/lazy-yt/
+!/plugins/discourse-lazy-videos/
!/plugins/chat/
!/plugins/poll/
!/plugins/styleguide
diff --git a/app/assets/stylesheets/common/printer-friendly.scss b/app/assets/stylesheets/common/printer-friendly.scss
index b8cef469afa..58649c225e3 100644
--- a/app/assets/stylesheets/common/printer-friendly.scss
+++ b/app/assets/stylesheets/common/printer-friendly.scss
@@ -16,7 +16,7 @@
#topic-progress,
.quote-controls,
.topic-timer-info,
- div.lazyYT,
+ div.lazy-video-container,
.post-info.edits,
.post-action,
.saving-text,
diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb
index 530ac26c6d0..f0af956e85b 100644
--- a/app/services/search_indexer.rb
+++ b/app/services/search_indexer.rb
@@ -396,7 +396,7 @@ class SearchIndexer
end
MENTION_CLASSES ||= %w[mention mention-group]
- ATTRIBUTES ||= %w[alt title href data-youtube-title]
+ ATTRIBUTES ||= %w[alt title href data-video-title]
def start_element(_name, attributes = [])
attributes = Hash[*attributes.flatten]
diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb
index ab59144079c..96d836ac51e 100644
--- a/lib/plugin/metadata.rb
+++ b/lib/plugin/metadata.rb
@@ -45,6 +45,7 @@ class Plugin::Metadata
"discourse-graphviz",
"discourse-group-tracker",
"discourse-invite-tokens",
+ "discourse-lazy-videos",
"discourse-local-dates",
"discourse-login-with-amazon",
"discourse-logster-rate-limit-checker",
@@ -93,7 +94,6 @@ class Plugin::Metadata
"discourse-zendesk-plugin",
"docker_manager",
"chat",
- "lazy-yt",
"poll",
"styleguide",
],
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 66d9c96dfae..5d142f9e31a 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -433,13 +433,19 @@ module PrettyText
# extract Youtube links
doc
- .css("div[data-youtube-id]")
+ .css("div[data-video-id]")
.each do |div|
- if div["data-youtube-id"].present?
- links << DetectedLink.new(
- "https://www.youtube.com/watch?v=#{div["data-youtube-id"]}",
- false,
- )
+ if div["data-video-id"].present? && div["data-provider-name"].present?
+ base_url =
+ case div["data-provider-name"]
+ when "youtube"
+ "https://www.youtube.com/watch?v="
+ when "vimeo"
+ "https://vimeo.com/"
+ when "tiktok"
+ "https://m.tiktok.com/v/"
+ end
+ links << DetectedLink.new(base_url + div["data-video-id"], false)
end
end
diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake
index 6f4d67fe12e..9c8d4e601ee 100644
--- a/lib/tasks/plugin.rake
+++ b/lib/tasks/plugin.rake
@@ -4,7 +4,7 @@ directory "plugins"
desc "install all official plugins (use GIT_WRITE=1 to pull with write access)"
task "plugin:install_all_official" do
- skip = Set.new(%w[customer-flair lazy-yt poll])
+ skip = Set.new(%w[customer-flair poll])
map = { "Canned Replies" => "https://github.com/discourse/discourse-canned-replies" }
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.hbs
index 588df4baca7..0ac4035be2f 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.hbs
@@ -13,7 +13,13 @@
{{#each this.cookedBodies as |cooked|}}
{{#if cooked.needsCollapser}}
- {{cooked.body}}
+ {{#if cooked.videoAttributes}}
+
+
+
+ {{else}}
+ {{cooked.body}}
+ {{/if}}
{{else}}
{{cooked.body}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.js
index d62df9e639e..3a6100e6887 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-collapser.js
@@ -1,10 +1,13 @@
import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template";
+import { inject as service } from "@ember/service";
import { escapeExpression } from "discourse/lib/utilities";
import domFromString from "discourse-common/lib/dom-from-string";
import I18n from "I18n";
export default class ChatMessageCollapser extends Component {
+ @service siteSettings;
+
get hasUploads() {
return hasUploads(this.args.uploads);
}
@@ -28,8 +31,8 @@ export default class ChatMessageCollapser extends Component {
domFromString(this.args.cooked)
);
- if (hasYoutube(elements)) {
- return this.youtubeCooked(elements);
+ if (hasLazyVideo(elements)) {
+ return this.lazyVideoCooked(elements);
}
if (hasImageOnebox(elements)) {
@@ -47,20 +50,26 @@ export default class ChatMessageCollapser extends Component {
return [];
}
- youtubeCooked(elements) {
+ lazyVideoCooked(elements) {
return elements.reduce((acc, e) => {
- if (youtubePredicate(e)) {
- const id = e.dataset.youtubeId;
- const link = `https://www.youtube.com/watch?v=${escapeExpression(id)}`;
- const title = escapeExpression(e.dataset.youtubeTitle);
- const header = htmlSafe(
- `${title} `
- );
- const body = document.createElement("div");
- body.className = "chat-message-collapser-youtube";
- body.appendChild(e);
+ if (this.siteSettings.lazy_videos_enabled && lazyVideoPredicate(e)) {
+ const getVideoAttributes = requirejs(
+ "discourse/plugins/discourse-lazy-videos/lib/lazy-video-attributes"
+ ).default;
- acc.push({ header, body, needsCollapser: true });
+ const videoAttributes = getVideoAttributes(e);
+
+ if (this.siteSettings[`lazy_${videoAttributes.providerName}_enabled`]) {
+ const link = escapeExpression(videoAttributes.url);
+ const title = videoAttributes.title;
+ const header = htmlSafe(
+ `${title} `
+ );
+
+ acc.push({ header, body: e, videoAttributes, needsCollapser: true });
+ } else {
+ acc.push({ body: e, needsCollapser: false });
+ }
} else {
acc.push({ body: e, needsCollapser: false });
}
@@ -125,16 +134,12 @@ export default class ChatMessageCollapser extends Component {
}
}
-function youtubePredicate(e) {
- return (
- e.classList.length &&
- e.classList.contains("onebox") &&
- e.classList.contains("lazyYT-container")
- );
+function lazyVideoPredicate(e) {
+ return e.classList.contains("lazy-video-container");
}
-function hasYoutube(elements) {
- return elements.some((e) => youtubePredicate(e));
+function hasLazyVideo(elements) {
+ return elements.some((e) => lazyVideoPredicate(e));
}
function animatedImagePredicate(e) {
@@ -198,7 +203,7 @@ export function isCollapsible(cooked, uploads) {
const elements = Array.prototype.slice.call(domFromString(cooked));
return (
- hasYoutube(elements) ||
+ hasLazyVideo(elements) ||
hasImageOnebox(elements) ||
hasUploads(uploads) ||
hasImage(elements) ||
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-plugin-decorators.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-plugin-decorators.js
index 8439021ba06..877fb9c319b 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-plugin-decorators.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-plugin-decorators.js
@@ -34,20 +34,6 @@ export default {
}
);
}
- if (siteSettings.lazy_yt_enabled) {
- api.decorateChatMessage(
- (element) => {
- element
- .querySelectorAll(".lazyYT:not(.lazyYT-video-loaded)")
- .forEach((iframe) => {
- $(iframe).lazyYT();
- });
- },
- {
- id: "lazy-yt",
- }
- );
- }
},
initialize(container) {
diff --git a/plugins/chat/assets/stylesheets/common/chat-message-images.scss b/plugins/chat/assets/stylesheets/common/chat-message-images.scss
index 300629fe7f2..75958050b40 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message-images.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message-images.scss
@@ -21,9 +21,24 @@ $max_image_height: 150px;
.chat-message-collapser
.chat-message-collapser-header
+ div
- .chat-message-collapser-youtube {
+ .chat-message-collapser-lazy-video {
object-fit: contain;
height: $max_image_height;
width: calc(#{$max_image_height} / 9 * 16);
}
+
+ // Prevent overflow of old lazy-yt images
+ // TODO: remove in December 2023
+ .lazyYT.lazyYT-container {
+ border: none;
+ a {
+ display: flex;
+ }
+ .ytp-thumbnail-image {
+ object-fit: contain;
+ height: $max_image_height;
+ width: calc(#{$max_image_height} / 9 * 16);
+ pointer-events: none;
+ }
+ }
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-message.scss b/plugins/chat/assets/stylesheets/common/chat-message.scss
index 2ee5de37395..9a7d7d1a39a 100644
--- a/plugins/chat/assets/stylesheets/common/chat-message.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-message.scss
@@ -88,16 +88,6 @@
text-decoration: none;
}
- img.ytp-thumbnail-image,
- img.youtube-thumbnail {
- height: 100%;
- max-height: unset;
-
- &:hover {
- border-radius: 0;
- }
- }
-
// Automatic aspect-ratio mapping https://developer.mozilla.org/en-US/docs/Web/Media/images/aspect_ratio_mapping
p img:not(.emoji) {
max-width: 100%;
diff --git a/plugins/chat/test/javascripts/acceptance/chat-live-pane-collapse-test.js b/plugins/chat/test/javascripts/acceptance/chat-live-pane-collapse-test.js
index 4674ac50722..c59e0ece4dd 100644
--- a/plugins/chat/test/javascripts/acceptance/chat-live-pane-collapse-test.js
+++ b/plugins/chat/test/javascripts/acceptance/chat-live-pane-collapse-test.js
@@ -30,7 +30,7 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
id: 1,
message: "https://www.youtube.com/watch?v=aOWkVdU4NH0",
cooked:
- '
',
+ ' ',
excerpt:
'[Picnic with my cat (shaved ice & lemonade… ',
created_at: "2021-07-20T08:14:16.950Z",
@@ -106,8 +106,9 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
);
});
- skip("can collapse and expand youtube chat", async function (assert) {
- const youtubeContainer = ".chat-message-container[data-id='1'] .lazyYT";
+ skip("can collapse and expand videos in chat", async function (assert) {
+ const videoContainer =
+ ".chat-message-container[data-id='1'] .lazy-video-container";
const expandImage =
".chat-message-container[data-id='1'] .chat-message-collapser-closed";
const collapseImage =
@@ -115,19 +116,19 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
await visit("/chat/c/cat/1");
- assert.ok(visible(youtubeContainer));
+ assert.ok(visible(videoContainer));
assert.ok(visible(collapseImage), "the open arrow is shown");
assert.notOk(exists(expandImage), "the close arrow is hidden");
await click(collapseImage);
- assert.notOk(visible(youtubeContainer));
+ assert.notOk(visible(videoContainer));
assert.ok(visible(expandImage), "the close arrow is shown");
assert.notOk(exists(collapseImage), "the open arrow is hidden");
await click(expandImage);
- assert.ok(visible(youtubeContainer));
+ assert.ok(visible(videoContainer));
assert.ok(visible(collapseImage), "the open arrow is shown again");
assert.notOk(exists(expandImage), "the close arrow is hidden again");
});
diff --git a/plugins/chat/test/javascripts/components/chat-message-collapser-test.js b/plugins/chat/test/javascripts/components/chat-message-collapser-test.js
index 56be03b199c..0025a99fdbd 100644
--- a/plugins/chat/test/javascripts/components/chat-message-collapser-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-collapser-test.js
@@ -10,9 +10,9 @@ import { module, test } from "qunit";
const youtubeCooked =
"written text
" +
- 'Vid 1
' +
+ '' +
"more written text
" +
- 'Vid 2
' +
+ '' +
"and even more
";
const animatedImageCooked =
@@ -71,7 +71,13 @@ module(
setupRenderingTest(hooks);
test("escapes youtube header", async function (assert) {
- this.set("cooked", youtubeCooked.replace("ytId1", evilString));
+ this.set(
+ "cooked",
+ youtubeCooked.replace(
+ "https://www.youtube.com/watch?v=ytId1",
+ `https://www.youtube.com/watch?v=${evilString}`
+ )
+ );
await render(hbs` `);
assert.true(
@@ -124,7 +130,7 @@ module(
await render(hbs` `);
- const youtubeDivs = queryAll(".onebox");
+ const youtubeDivs = queryAll(".youtube-onebox");
assert.strictEqual(
youtubeDivs.length,
@@ -138,11 +144,11 @@ module(
);
assert.false(
- visible(".onebox[data-youtube-id='ytId1']"),
+ visible(".youtube-onebox[data-video-id='ytId1']"),
"first youtube preview hidden"
);
assert.true(
- visible(".onebox[data-youtube-id='ytId2']"),
+ visible(".youtube-onebox[data-video-id='ytId2']"),
"second youtube preview still visible"
);
@@ -160,11 +166,11 @@ module(
);
assert.true(
- visible(".onebox[data-youtube-id='ytId1']"),
+ visible(".youtube-onebox[data-video-id='ytId1']"),
"first youtube preview still visible"
);
assert.false(
- visible(".onebox[data-youtube-id='ytId2']"),
+ visible(".youtube-onebox[data-video-id='ytId2']"),
"second youtube preview hidden"
);
diff --git a/plugins/chat/test/javascripts/components/chat-message-text-test.js b/plugins/chat/test/javascripts/components/chat-message-text-test.js
index f0ba6fcd14f..b5f05f80bc3 100644
--- a/plugins/chat/test/javascripts/components/chat-message-text-test.js
+++ b/plugins/chat/test/javascripts/components/chat-message-text-test.js
@@ -22,7 +22,7 @@ module("Discourse Chat | Component | chat-message-text", function (hooks) {
test("shows collapsed", async function (assert) {
this.set(
"cooked",
- '
'
+ '
'
);
await render(
@@ -51,7 +51,10 @@ module("Discourse Chat | Component | chat-message-text", function (hooks) {
});
test("shows edits - collapsible message", async function (assert) {
- this.set("cooked", '
');
+ this.set(
+ "cooked",
+ '
'
+ );
await render(
hbs` `
diff --git a/plugins/discourse-lazy-videos/README.md b/plugins/discourse-lazy-videos/README.md
new file mode 100644
index 00000000000..47d4df67524
--- /dev/null
+++ b/plugins/discourse-lazy-videos/README.md
@@ -0,0 +1,12 @@
+## Discourse Lazy Videos
+
+Adds lazy loading support for embedded videos
+
+### Supported providers
+
+ - YouTube
+ - Vimeo
+
+### Experimental
+
+ - TikTok
\ No newline at end of file
diff --git a/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-iframe.hbs b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-iframe.hbs
new file mode 100644
index 00000000000..d662c4787a5
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-iframe.hbs
@@ -0,0 +1,11 @@
+{{#if @providerName}}
+
+{{/if}}
\ No newline at end of file
diff --git a/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-iframe.js b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-iframe.js
new file mode 100644
index 00000000000..7c81303ea8f
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-iframe.js
@@ -0,0 +1,14 @@
+import Component from "@glimmer/component";
+
+export default class LazyVideo extends Component {
+ get iframeSrc() {
+ switch (this.args.providerName) {
+ case "youtube":
+ return `https://www.youtube.com/embed/${this.args.videoId}?autoplay=1`;
+ case "vimeo":
+ return `https://player.vimeo.com/video/${this.args.videoId}?autoplay=1`;
+ case "tiktok":
+ return `https://www.tiktok.com/embed/v2/${this.args.videoId}`;
+ }
+ }
+}
diff --git a/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-video.hbs b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-video.hbs
new file mode 100644
index 00000000000..b3dba578c7e
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-video.hbs
@@ -0,0 +1,51 @@
+
+ {{#if this.isLoaded}}
+
+ {{else}}
+
+
+
+
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-video.js b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-video.js
new file mode 100644
index 00000000000..6257605aadf
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/javascripts/discourse/components/lazy-video.js
@@ -0,0 +1,23 @@
+import Component from "@glimmer/component";
+import { action } from "@ember/object";
+import { tracked } from "@glimmer/tracking";
+
+export default class LazyVideo extends Component {
+ @tracked isLoaded = false;
+
+ @action
+ loadEmbed() {
+ if (!this.isLoaded) {
+ this.isLoaded = true;
+ this.args.onLoadedVideo?.();
+ }
+ }
+
+ @action
+ onKeyPress(event) {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ this.loadEmbed();
+ }
+ }
+}
diff --git a/plugins/discourse-lazy-videos/assets/javascripts/initializers/lazy-videos.js b/plugins/discourse-lazy-videos/assets/javascripts/initializers/lazy-videos.js
new file mode 100644
index 00000000000..ffee62ef815
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/javascripts/initializers/lazy-videos.js
@@ -0,0 +1,46 @@
+import { withPluginApi } from "discourse/lib/plugin-api";
+import getVideoAttributes from "../lib/lazy-video-attributes";
+import { hbs } from "ember-cli-htmlbars";
+
+function initLazyEmbed(api) {
+ api.decorateCookedElement(
+ (cooked, helper) => {
+ if (cooked.classList.contains("d-editor-preview")) {
+ return;
+ }
+
+ const lazyContainers = cooked.querySelectorAll(".lazy-video-container");
+
+ lazyContainers.forEach((container) => {
+ const siteSettings = api.container.lookup("site-settings:main");
+ const videoAttributes = getVideoAttributes(container);
+
+ if (siteSettings[`lazy_${videoAttributes.providerName}_enabled`]) {
+ const onLoadedVideo = () => {
+ const postId = cooked.closest("article")?.dataset?.postId;
+ if (postId) {
+ api.preventCloak(parseInt(postId, 10));
+ }
+ };
+
+ const lazyVideo = helper.renderGlimmer(
+ "p.lazy-video-wrapper",
+ hbs` `,
+ { param: videoAttributes, onLoadedVideo }
+ );
+
+ container.replaceWith(lazyVideo);
+ }
+ });
+ },
+ { onlyStream: true, id: "discourse-lazy-videos" }
+ );
+}
+
+export default {
+ name: "discourse-lazy-videos",
+
+ initialize() {
+ withPluginApi("1.6.0", initLazyEmbed);
+ },
+};
diff --git a/plugins/discourse-lazy-videos/assets/javascripts/lib/lazy-video-attributes.js b/plugins/discourse-lazy-videos/assets/javascripts/lib/lazy-video-attributes.js
new file mode 100644
index 00000000000..f4dd6626264
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/javascripts/lib/lazy-video-attributes.js
@@ -0,0 +1,13 @@
+export default function getVideoAttributes(cooked) {
+ if (!cooked.classList.contains("lazy-video-container")) {
+ return {};
+ }
+
+ const url = cooked.querySelector("a")?.getAttribute("href");
+ const thumbnail = cooked.querySelector("img")?.getAttribute("src");
+ const title = cooked.dataset.videoTitle;
+ const providerName = cooked.dataset.providerName;
+ const id = cooked.dataset.videoId;
+
+ return { url, thumbnail, title, providerName, id };
+}
diff --git a/plugins/discourse-lazy-videos/assets/stylesheets/lazy-videos.scss b/plugins/discourse-lazy-videos/assets/stylesheets/lazy-videos.scss
new file mode 100644
index 00000000000..1877a9ddaec
--- /dev/null
+++ b/plugins/discourse-lazy-videos/assets/stylesheets/lazy-videos.scss
@@ -0,0 +1,137 @@
+.lazy-video-container {
+ z-index: z("base");
+ position: relative;
+ display: block;
+ height: 0;
+ padding: 0 0 56.25% 0;
+ background-color: #000;
+ margin-bottom: 12px;
+
+ .video-thumbnail {
+ cursor: pointer;
+ overflow: hidden;
+ height: 0;
+ padding: 0 0 56.25% 0;
+
+ img {
+ object-fit: cover;
+ width: 100%;
+ pointer-events: none;
+ }
+
+ &:hover,
+ &:focus {
+ .icon {
+ transform: translate(-50%, -50%) scale(1.1);
+ }
+ }
+
+ &:focus {
+ outline: 5px auto Highlight;
+ outline: 5px auto -webkit-focus-ring-color;
+ }
+
+ &:active {
+ outline: 0px;
+ }
+ }
+
+ .title-container {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ top: 0;
+ width: 100%;
+ height: 60px;
+ overflow: hidden;
+ background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(255, 0, 0, 0));
+
+ .title-wrapper {
+ overflow: hidden;
+ padding-inline: 20px;
+ padding-block: 10px;
+
+ .title-link {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #fff;
+ text-decoration: none;
+ font-size: 18px;
+ font-family: Arial, sans-serif;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+
+ iframe {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ }
+
+ .icon {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ transition: 150ms;
+
+ // Default play button
+ background: svg-uri(
+ " "
+ );
+ width: 60px;
+ height: 60px;
+
+ &.youtube-icon {
+ width: 68px;
+ height: 48px;
+ background: svg-uri(
+ " "
+ );
+ }
+
+ &.vimeo-icon {
+ width: 77px;
+ height: 44px;
+ background: svg-uri(
+ " "
+ );
+ }
+
+ &.tiktok-icon {
+ width: 58px;
+ height: 64px;
+ background: svg-uri(
+ " "
+ );
+ }
+ }
+}
+
+// TikTok iframe isn't fluid
+.lazy-video-container.tiktok-onebox {
+ width: 332px;
+ height: 745px;
+ padding: 0;
+
+ .video-thumbnail.tiktok img {
+ height: 745px;
+ }
+
+ iframe {
+ min-width: 332px;
+ height: 742px;
+ background-color: #fff;
+ border-top: 3px solid #fff;
+ border-radius: 9px;
+ }
+}
diff --git a/plugins/discourse-lazy-videos/config/locales/server.en.yml b/plugins/discourse-lazy-videos/config/locales/server.en.yml
new file mode 100644
index 00000000000..cff25e5b6e4
--- /dev/null
+++ b/plugins/discourse-lazy-videos/config/locales/server.en.yml
@@ -0,0 +1,3 @@
+en:
+ site_settings:
+ lazy_videos_enabled: "Enable the Lazy Videos plugin"
\ No newline at end of file
diff --git a/plugins/discourse-lazy-videos/config/settings.yml b/plugins/discourse-lazy-videos/config/settings.yml
new file mode 100644
index 00000000000..b89902684ca
--- /dev/null
+++ b/plugins/discourse-lazy-videos/config/settings.yml
@@ -0,0 +1,17 @@
+plugins:
+ lazy_videos_enabled:
+ default: true
+ client: true
+ hidden: true
+ lazy_youtube_enabled:
+ default: true
+ client: true
+ hidden: false
+ lazy_vimeo_enabled:
+ default: true
+ client: true
+ hidden: false
+ lazy_tiktok_enabled:
+ default: false
+ client: true
+ hidden: false
\ No newline at end of file
diff --git a/plugins/discourse-lazy-videos/db/post_migrate/20230317194217_rebake_lazy_yt_posts.rb b/plugins/discourse-lazy-videos/db/post_migrate/20230317194217_rebake_lazy_yt_posts.rb
new file mode 100644
index 00000000000..ab99a586b3f
--- /dev/null
+++ b/plugins/discourse-lazy-videos/db/post_migrate/20230317194217_rebake_lazy_yt_posts.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class RebakeLazyYtPosts < ActiveRecord::Migration[7.0]
+ def up
+ execute <<~SQL
+ UPDATE posts SET baked_version = 0
+ WHERE cooked LIKE '%lazyYT-container%'
+ SQL
+ end
+
+ def down
+ # do nothing
+ end
+end
diff --git a/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_tiktok.rb b/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_tiktok.rb
new file mode 100644
index 00000000000..d667cce6948
--- /dev/null
+++ b/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_tiktok.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "onebox"
+
+class Onebox::Engine::TiktokOnebox
+ include Onebox::Engine
+ alias_method :default_onebox_to_html, :to_html
+
+ def to_html
+ if SiteSetting.lazy_videos_enabled && SiteSetting.lazy_tiktok_enabled &&
+ oembed_data.embed_product_id
+ thumbnail_url = oembed_data.thumbnail_url
+ escaped_title = ERB::Util.html_escape(oembed_data.title)
+
+ <<~HTML
+
+ HTML
+ else
+ default_onebox_to_html
+ end
+ end
+end
diff --git a/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_vimeo.rb b/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_vimeo.rb
new file mode 100644
index 00000000000..5e983dd18ee
--- /dev/null
+++ b/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_vimeo.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "onebox"
+
+class Onebox::Engine::VimeoOnebox
+ include Onebox::Engine
+ alias_method :default_onebox_to_html, :to_html
+
+ def to_html
+ if SiteSetting.lazy_videos_enabled && SiteSetting.lazy_vimeo_enabled
+ video_id = oembed_data[:video_id]
+ thumbnail_url = "https://vumbnail.com/#{oembed_data[:video_id]}.jpg"
+ escaped_title = ERB::Util.html_escape(og_data.title)
+
+ <<~HTML
+
+ HTML
+ else
+ default_onebox_to_html
+ end
+ end
+end
diff --git a/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_youtube.rb b/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_youtube.rb
new file mode 100644
index 00000000000..adf3e64e788
--- /dev/null
+++ b/plugins/discourse-lazy-videos/lib/lazy-videos/lazy_youtube.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "onebox"
+
+class Onebox::Engine::YoutubeOnebox
+ include Onebox::Engine
+ alias_method :default_onebox_to_html, :to_html
+
+ def to_html
+ if SiteSetting.lazy_videos_enabled && SiteSetting.lazy_youtube_enabled && video_id &&
+ !params["list"]
+ result = parse_embed_response
+ result ||= get_opengraph.data
+
+ thumbnail_url = "https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg"
+
+ begin
+ Onebox::Helpers.fetch_response(thumbnail_url)
+ rescue StandardError
+ thumbnail_url = result[:image]
+ end
+
+ escaped_title = ERB::Util.html_escape(video_title)
+
+ <<~HTML
+
+ HTML
+ else
+ default_onebox_to_html
+ end
+ end
+end
diff --git a/plugins/discourse-lazy-videos/plugin.rb b/plugins/discourse-lazy-videos/plugin.rb
new file mode 100644
index 00000000000..8875cad76af
--- /dev/null
+++ b/plugins/discourse-lazy-videos/plugin.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# name: discourse-lazy-videos
+# about: Lazy loading for embedded videos
+# version: 0.1
+# authors: Jan Cernik
+# url: https://github.com/discourse/discourse-lazy-videos
+
+hide_plugin if self.respond_to?(:hide_plugin)
+enabled_site_setting :lazy_videos_enabled
+
+register_asset "stylesheets/lazy-videos.scss"
+
+require_relative "lib/lazy-videos/lazy_youtube"
+require_relative "lib/lazy-videos/lazy_vimeo"
+require_relative "lib/lazy-videos/lazy_tiktok"
+
+after_initialize do
+ on(:reduce_cooked) do |fragment|
+ fragment
+ .css(".lazy-video-container")
+ .each do |video|
+ title = video["data-video-title"]
+ href = video.at_css("a")["href"]
+ video.replace("#{title}
")
+ end
+ end
+end
diff --git a/plugins/discourse-lazy-videos/spec/components/pretty_text_spec.rb b/plugins/discourse-lazy-videos/spec/components/pretty_text_spec.rb
new file mode 100644
index 00000000000..dd1c506770a
--- /dev/null
+++ b/plugins/discourse-lazy-videos/spec/components/pretty_text_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+require "pretty_text"
+
+RSpec.describe PrettyText do
+ let(:post) { Fabricate(:post) }
+
+ it "replaces lazy videos in emails" do
+ cooked_html = <<~HTML
+
+
+
+
+ HTML
+
+ email_formated = <<~HTML
+ 15 Sorting Algorithms in 6 Minutes
+ Dear Rich
+ HTML
+
+ expect(PrettyText.format_for_email(cooked_html, post)).to match_html(email_formated)
+ end
+end
diff --git a/plugins/discourse-lazy-videos/test/javascripts/components/lazy-video-test.js b/plugins/discourse-lazy-videos/test/javascripts/components/lazy-video-test.js
new file mode 100644
index 00000000000..a1d2f54b4e3
--- /dev/null
+++ b/plugins/discourse-lazy-videos/test/javascripts/components/lazy-video-test.js
@@ -0,0 +1,49 @@
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import hbs from "htmlbars-inline-precompile";
+import { module, test } from "qunit";
+import { click, render } from "@ember/test-helpers";
+
+module("Discourse Lazy Videos | Component | lazy-video", function (hooks) {
+ setupRenderingTest(hooks);
+
+ this.attributes = {
+ url: "https://www.youtube.com/watch?v=kPRA0W1kECg",
+ thumbnail: "thumbnail.jpeg",
+ title: "15 Sorting Algorithms in 6 Minutes",
+ providerName: "youtube",
+ id: "kPRA0W1kECg",
+ };
+
+ test("displays the correct video title", async function (assert) {
+ await render(hbs` `);
+
+ assert.dom(".title-link").hasText(this.attributes.title);
+ });
+
+ test("displays the correct provider icon", async function (assert) {
+ await render(hbs` `);
+
+ assert.dom(".icon.youtube-icon").exists();
+ });
+
+ test("loads the iframe when clicked", async function (assert) {
+ await render(hbs` `);
+ assert.dom(".lazy-video-container.video-loaded").doesNotExist();
+
+ await click(".video-thumbnail.youtube");
+ assert.dom(".lazy-video-container.video-loaded iframe").exists();
+ });
+
+ test("accepts an optional onLoadedVideo callback function", async function (assert) {
+ this.set("foo", 1);
+ this.set("onLoadedVideo", () => this.set("foo", 2));
+
+ await render(
+ hbs` `
+ );
+ assert.strictEqual(this.foo, 1);
+
+ await click(".video-thumbnail.youtube");
+ assert.strictEqual(this.foo, 2);
+ });
+});
diff --git a/plugins/lazy-yt/README.md b/plugins/lazy-yt/README.md
deleted file mode 100644
index 5f0e7d7e0d1..00000000000
--- a/plugins/lazy-yt/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# lazy-yt
-
-Lazy load YouTube videos plugin for [Discourse](http://discourse.org), highly inspired by the [lazyYT](https://github.com/tylerpearson/lazyYT) jQuery plugin.
diff --git a/plugins/lazy-yt/assets/javascripts/initializers/lazyYT.js b/plugins/lazy-yt/assets/javascripts/initializers/lazyYT.js
deleted file mode 100644
index cbc4df3c684..00000000000
--- a/plugins/lazy-yt/assets/javascripts/initializers/lazyYT.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { withPluginApi } from "discourse/lib/plugin-api";
-import initLazyYt from "../lib/lazyYT";
-
-export default {
- name: "apply-lazyYT",
- initialize() {
- withPluginApi("0.1", (api) => {
- initLazyYt($);
- api.decorateCooked(
- ($elem) => {
- const iframes = $(".lazyYT", $elem);
- if (iframes.length === 0) {
- return;
- }
-
- $(".lazyYT", $elem).lazyYT({
- onPlay(e, $el) {
- // don't cloak posts that have playing videos in them
- const postId = parseInt(
- $el.closest("article").data("post-id"),
- 10
- );
- if (postId) {
- api.preventCloak(postId);
- }
- },
- });
- },
- { id: "discourse-lazyyt" }
- );
- });
- },
-};
diff --git a/plugins/lazy-yt/assets/javascripts/lib/lazyYT.js b/plugins/lazy-yt/assets/javascripts/lib/lazyYT.js
deleted file mode 100644
index 467515e4ee2..00000000000
--- a/plugins/lazy-yt/assets/javascripts/lib/lazyYT.js
+++ /dev/null
@@ -1,179 +0,0 @@
-/*!
- * lazyYT (lazy load YouTube videos)
- * v1.0.1 - 2014-12-30
- * (CC) This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
- * http://creativecommons.org/licenses/by-sa/4.0/
- * Contributors: https://github.com/tylerpearson/lazyYT/graphs/contributors || https://github.com/daugilas/lazyYT/graphs/contributors
- *
- * Usage: loading...
- *
- * Note: Discourse has forked this from the original, beware when updating the file.
- *
- */
-
-import escape from "discourse-common/lib/escape";
-
-export default function initLazyYt($) {
- "use strict";
-
- function setUp($el, settings) {
- let width = $el.data("width"),
- height = $el.data("height"),
- ratio = $el.data("ratio") ? $el.data("ratio") : settings.default_ratio,
- id = $el.data("youtube-id"),
- title = $el.data("youtube-title"),
- padding_bottom,
- innerHtml = [],
- $thumb,
- thumb_img,
- youtube_parameters = $el.data("parameters") || "";
-
- ratio = ratio.split(":");
-
- // width and height might override default_ratio value
- if (typeof width === "number" && typeof height === "number") {
- $el.width(width);
- padding_bottom = height + "px";
- } else if (typeof width === "number") {
- $el.width(width);
- padding_bottom = (width * ratio[1]) / ratio[0] + "px";
- } else {
- width = $el.width();
-
- // no width means that container is fluid and will be the size of its parent
- if (width === 0) {
- width = $el.parent().width();
- }
-
- padding_bottom = (ratio[1] / ratio[0]) * 100 + "%";
- }
-
- //
- // This HTML will be placed inside 'lazyYT' container
-
- innerHtml.push('');
-
- // Play button from YouTube (exactly as it is in YouTube)
- innerHtml.push('
"); // end of .ytp-large-play-button
-
- innerHtml.push("
"); // end of .ytp-thumbnail
-
- // Video title (info bar)
- innerHtml.push('');
- innerHtml.push('
');
- innerHtml.push('
"); // .html5-title
- innerHtml.push("
"); // .html5-title-text-wrapper
- innerHtml.push("
"); // end of Video title .html5-info-bar
-
- let prefetchedThumbnail = $el[0].querySelector(".ytp-thumbnail-image");
-
- $el
- .css({
- "padding-bottom": padding_bottom,
- })
- .html(innerHtml.join(""));
-
- if (width > 640) {
- thumb_img = "maxresdefault.jpg";
- } else if (width > 480) {
- thumb_img = "sddefault.jpg";
- } else if (width > 320) {
- thumb_img = "hqdefault.jpg";
- } else if (width > 120) {
- thumb_img = "mqdefault.jpg";
- } else if (width === 0) {
- // sometimes it fails on fluid layout
- thumb_img = "hqdefault.jpg";
- } else {
- thumb_img = "default.jpg";
- }
-
- if (prefetchedThumbnail) {
- $el.find(".ytp-thumbnail").append(prefetchedThumbnail);
- } else {
- // Fallback for old posts which were baked before the lazy-yt onebox prefetched a thumbnail
- $el
- .find(".ytp-thumbnail")
- .append(
- $(
- [
- ' ',
- ].join("")
- )
- );
- }
-
- $thumb = $el
- .find(".ytp-thumbnail")
- .addClass("lazyYT-image-loaded")
- .on("keypress click", function (e) {
- // Only support Enter for keypress
- if (e.type === "keypress" && e.keyCode !== 13) {
- return;
- }
- e.preventDefault();
-
- if (
- !$el.hasClass("lazyYT-video-loaded") &&
- $thumb.hasClass("lazyYT-image-loaded")
- ) {
- $el
- .html(
- ''
- )
- .addClass("lazyYT-video-loaded");
- }
-
- if (settings.onPlay) {
- settings.onPlay(e, $el);
- }
- });
- }
-
- $.fn.lazyYT = function (newSettings) {
- let defaultSettings = {
- default_ratio: "16:9",
- callback: null, // TODO: execute callback if given
- container_class: "lazyYT-container",
- };
- let settings = Object.assign(defaultSettings, newSettings);
-
- return this.each(function () {
- let $el = $(this).addClass(settings.container_class);
- setUp($el, settings);
- });
- };
-}
diff --git a/plugins/lazy-yt/assets/stylesheets/lazyYT.css b/plugins/lazy-yt/assets/stylesheets/lazyYT.css
deleted file mode 100644
index 848b57a11bd..00000000000
--- a/plugins/lazy-yt/assets/stylesheets/lazyYT.css
+++ /dev/null
@@ -1,127 +0,0 @@
-/*!
-* lazyYT (lazy load YouTube videos)
-* v1.0.1 - 2014-12-30
-* (CC) This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
-* http://creativecommons.org/licenses/by-sa/4.0/
-* Contributors: https://github.com/tylerpearson/lazyYT/graphs/contributors || https://github.com/daugilas/lazyYT/graphs/contributors
-*/
-
-.lazyYT-container {
- position: relative;
- z-index: z("base");
- display: block;
- height: 0;
- padding: 0 0 56.25% 0;
- overflow: hidden;
- background-color: #000000;
- margin-bottom: 12px;
-}
-
-.lazyYT-container iframe {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 100%;
- border: 0;
-}
-
-/*
-* Video Title (YouTube style)
-*/
-
-.lazyYT-container .html5-info-bar {
- position: absolute;
- top: 0;
- z-index: 935;
- width: 100%;
- height: 30px;
- overflow: hidden;
- font-family: Arial, sans-serif;
- font-size: 12px;
- color: #fff;
- background-color: rgba(0, 0, 0, 0.8);
- -webkit-transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
- -moz-transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
- transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
-}
-
-.lazyYT-container .html5-title {
- padding-right: 6px;
- padding-left: 12px;
-}
-
-.lazyYT-container .html5-title-text-wrapper {
- overflow: hidden;
- -o-text-overflow: ellipsis;
- text-overflow: ellipsis;
- word-wrap: normal;
- white-space: nowrap;
-}
-
-.lazyYT-container .html5-title-text {
- width: 100%;
- font-size: 13px;
- line-height: 30px;
- color: #ccc;
- text-decoration: none;
-}
-
-.lazyYT-container .html5-title-text:hover {
- color: #fff;
- text-decoration: underline;
-}
-
-/*
-* Thumbnail
-*/
-
-.ytp-thumbnail {
- padding-bottom: inherit;
- cursor: pointer;
- background-position: 50% 50%;
- background-repeat: no-repeat;
- -webkit-background-size: cover;
- -moz-background-size: cover;
- -o-background-size: cover;
- background-size: cover;
-}
-
-/*
-* Play button (YouTube style)
-*/
-
-.ytp-large-play-button {
- position: absolute;
- top: 50% !important;
- left: 50% !important;
- width: 86px !important;
- height: 60px !important;
- padding: 0 !important;
- margin: -29px 0 0 -42px !important;
- font-size: normal !important;
- font-weight: normal !important;
- line-height: 1 !important;
- opacity: 0.9;
- z-index: 935;
-}
-
-.ytp-large-play-button-svg {
- opacity: 0.9;
- fill: #1f1f1f;
-}
-
-.lazyYT-image-loaded:hover .ytp-large-play-button-svg,
-.lazyYT-image-loaded:focus .ytp-large-play-button-svg,
-.ytp-large-play-button:focus .ytp-large-play-button-svg {
- opacity: 1;
- fill: #cc181e;
-}
-
-.ytp-thumbnail-image {
- position: absolute;
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
diff --git a/plugins/lazy-yt/assets/stylesheets/lazyYT_mobile.scss b/plugins/lazy-yt/assets/stylesheets/lazyYT_mobile.scss
deleted file mode 100644
index 149a8d9b84f..00000000000
--- a/plugins/lazy-yt/assets/stylesheets/lazyYT_mobile.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.lazyYT {
- max-width: 100%;
-}
diff --git a/plugins/lazy-yt/config/locales/server.en.yml b/plugins/lazy-yt/config/locales/server.en.yml
deleted file mode 100644
index 1ac0bee45f9..00000000000
--- a/plugins/lazy-yt/config/locales/server.en.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-en:
- site_settings:
- lazy_yt_enabled: "Enable the LazyYT plugin"
diff --git a/plugins/lazy-yt/config/settings.yml b/plugins/lazy-yt/config/settings.yml
deleted file mode 100644
index cca346d5df1..00000000000
--- a/plugins/lazy-yt/config/settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-plugins:
- lazy_yt_enabled:
- default: true
- client: false
- hidden: true
diff --git a/plugins/lazy-yt/plugin.rb b/plugins/lazy-yt/plugin.rb
deleted file mode 100644
index 5fb481c041a..00000000000
--- a/plugins/lazy-yt/plugin.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-# name: lazy-yt
-# about: Uses the lazyYT plugin to lazy load Youtube videos
-# version: 1.0.1
-# authors: Arpit Jalan
-# url: https://github.com/discourse/discourse/tree/main/plugins/lazy-yt
-
-hide_plugin if self.respond_to?(:hide_plugin)
-enabled_site_setting :lazy_yt_enabled
-
-require "onebox"
-
-# stylesheet
-register_asset "stylesheets/lazyYT.css"
-register_asset "stylesheets/lazyYT_mobile.scss", :mobile
-
-# freedom patch YouTube Onebox
-class Onebox::Engine::YoutubeOnebox
- include Onebox::Engine
- alias_method :yt_onebox_to_html, :to_html
-
- def to_html
- if SiteSetting.lazy_yt_enabled && video_id && !params["list"]
- size_restricted = [params["width"], params["height"]].any?
- video_width = (params["width"] && params["width"].to_i <= 695) ? params["width"] : 690 # embed width
- video_height = (params["height"] && params["height"].to_i <= 500) ? params["height"] : 388 # embed height
- size_tags = ["width=\"#{video_width}\"", "height=\"#{video_height}\""]
-
- result = parse_embed_response
- result ||= get_opengraph.data
-
- thumbnail_url = result[:image] || "https://img.youtube.com/vi/#{video_id}/hqdefault.jpg"
-
- # Put in the LazyYT div instead of the iframe
- escaped_title = ERB::Util.html_escape(video_title)
-
- <<~HTML
-
- HTML
- else
- yt_onebox_to_html
- end
- end
-end
-
-after_initialize do
- on(:reduce_cooked) do |fragment|
- fragment
- .css(".lazyYT")
- .each do |yt|
- begin
- youtube_id = yt["data-youtube-id"]
- parameters = yt["data-parameters"]
- uri = URI("https://www.youtube.com/embed/#{youtube_id}?autoplay=1{parameters}")
- yt.replace %{https://#{uri.host}#{uri.path}
}
- rescue URI::InvalidURIError
- # remove any invalid/weird URIs
- yt.remove
- end
- end
- end
-end
diff --git a/spec/lib/pretty_text_spec.rb b/spec/lib/pretty_text_spec.rb
index ac11ef85e21..cf53fc17f72 100644
--- a/spec/lib/pretty_text_spec.rb
+++ b/spec/lib/pretty_text_spec.rb
@@ -1019,12 +1019,30 @@ RSpec.describe PrettyText do
expect(extract_urls(html)).to eq(["https://example.com"])
end
- it "should lazyYT videos" do
- expect(
- extract_urls(
- "
",
- ),
- ).to eq(["https://www.youtube.com/watch?v=yXEuEUQIP3Q"])
+ context "when lazy-videos" do
+ it "should extract youtube url" do
+ expect(
+ extract_urls(
+ "
",
+ ),
+ ).to eq(["https://www.youtube.com/watch?v=yXEuEUQIP3Q"])
+ end
+
+ it "should extract vimeo url" do
+ expect(
+ extract_urls(
+ "
",
+ ),
+ ).to eq(["https://vimeo.com/786646692"])
+ end
+
+ it "should extract tiktok url" do
+ expect(
+ extract_urls(
+ "
",
+ ),
+ ).to eq(["https://m.tiktok.com/v/6718335390845095173"])
+ end
end
it "should extract links to posts" do
diff --git a/spec/services/search_indexer_spec.rb b/spec/services/search_indexer_spec.rb
index b8c1cc8b9eb..f784fcd83cb 100644
--- a/spec/services/search_indexer_spec.rb
+++ b/spec/services/search_indexer_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe SearchIndexer do
it "extract youtube title" do
html =
- "
"
+ "
"
scrubbed = SearchIndexer::HtmlScrubber.scrub(html)
expect(scrubbed).to eq(
"Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive]",