Files
flowdeck/app/services/embeds.py
T
bruno ba363eaee9
FlowDeck CI / lint (push) Successful in 43s
FlowDeck CI / test (push) Successful in 4m2s
FlowDeck CI / lint (pull_request) Successful in 42s
FlowDeck CI / test (pull_request) Successful in 4m3s
FlowDeck CI / docker (push) Successful in 1m2s
FlowDeck CI / docker (pull_request) Successful in 35s
feat(v5.2.0): finalize Infrastructure & Polish (tests isolation, xdist, lint, CI)
tests/conftest.py: mutate the settings singleton (instead of rebinding) so DB + backup dir are isolated per test -> pytest-xdist safe.
Real backup tests (snapshot/prune/admin API) and OAuth mock tests (Gitea/GitHub/link) replace the previous skips.
init_db() now also creates webhook_subscriptions (full schema without the FastAPI lifespan).
ruff check is clean; .eslintrc.json migrated to eslint.config.mjs (flat config).
CI: lint job (ruff + eslint), parallel tests (-n auto), run on every branch push.
VERSION 5.11.1.
2026-09-11 23:36:53 -04:00

195 lines
6.4 KiB
Python

"""FlowDeck — Media embeds (v5.5.0): provider detection + iframe rewriting.
Maps a raw http(s) URL to a provider-specific embed URL so that one generic
``embed`` block can render YouTube, Vimeo, Figma, Google Maps, Loom,
CodePen, Miro, Spotify, SoundCloud, Twitch, X/Twitter, Pinterest, Office
docs… exactly like Notion's universal embed.
Unknown/showable URLs (PDF, images, direct video/audio files, plain http)
fall back to a plain iframe so the link is still visible inline.
"""
from __future__ import annotations
import re
from urllib.parse import parse_qs, urlparse
_PROVIDERS = (
# (host pattern, extra path check, to_embed(url, path, query) -> str | None)
)
def _q(params, key):
vals = params.get(key)
return vals[0] if vals else ""
def _embed_youtube(url: str, path: str, params) -> str | None:
m = re.search(r"((?:v|shorts|embed)/|be/)([A-Za-z0-9_-]{6,20})", path)
vid = m.group(2) if m else _q(params, "v")
if not vid:
return None
start = _q(params, "t") or _q(params, "start")
frag = f"?start={start}" if start else ""
return f"https://www.youtube.com/embed/{vid}{frag}"
def _embed_vimeo(url: str, path: str, params) -> str | None:
m = re.search(r"/(\d{6,12})", path)
if not m:
return None
return f"https://player.vimeo.com/video/{m.group(1)}"
def _embed_loom(url: str, path: str, params) -> str | None:
m = re.search(r"/(embed/)?([0-9a-f]{32})", path)
if not m:
return None
return f"https://www.loom.com/embed/{m.group(2)}"
def _embed_figma(url: str, path: str, params) -> str | None:
if "figma.com/file/" not in url and "figma.com/proto/" not in url:
return None
m = re.search(r"(/[^/]+/[^?]+)", url.split("?", 1)[0])
if not m:
return None
return "https://www.figma.com/embed?embed_host=flowdeck&url=" + url.split("?", 1)[0]
def _embed_map(url: str, path: str, params) -> str | None:
if "google.com/maps" not in url and "maps.app.goo.gl" not in url:
return None
return f"https://www.google.com/maps?output=embed&q={url}"
def _embed_codepen(url: str, path: str, params) -> str | None:
m = re.search(r"codepen\.io/([^/]+)/pen/([^/?#]+)", url)
if not m:
return None
return f"https://codepen.io/{m.group(1)}/embed/{m.group(2)}?default-tab=result"
def _embed_miro(url: str, path: str, params) -> str | None:
m = re.search(r"miro\.com/app/board/([^/?#]+)", url)
if not m:
return None
return f"https://miro.com/app/live-embed/{m.group(1)}"
def _embed_spotify(url: str, path: str, params) -> str | None:
m = re.search(r"/(track|playlist|album|episode|show|artist)/([A-Za-z0-9]+)", url)
if not m:
return None
return f"https://open.spotify.com/embed/{m.group(1)}/{m.group(2)}"
def _embed_soundcloud(url: str, path: str, params) -> str | None:
m = re.search(r"(?:soundcloud\.com/[^/]+/[^/?#]+)", url)
if not m:
return None
return f"https://w.soundcloud.com/player/?url={url}&color=%2300aaff"
def _embed_twitch(url: str, path: str, params) -> str | None:
if "twitch.tv" not in url:
return None
m = re.search(r"twitch\.tv/([^/?#]+)", url)
if not m:
return None
return f"https://player.twitch.tv/?channel={m.group(1)}&parent="
def _embed_twitter(url: str, path: str, params) -> str | None:
if "twitter.com" not in url and "x.com" not in url:
return None
m = re.search(r"/([^/?#]+)/status/(\d+)", url)
if not m:
return f"https://platform.twitter.com/embed/Tweet.html?url={url}"
return f"https://platform.twitter.com/embed/Tweet.html?id={m.group(2)}"
def _embed_pinterest(url: str, path: str, params) -> str | None:
if "pinterest" not in url:
return None
return f"https://pinterest.com/pin/embed?url={url}"
def _embed_files(url: str, path: str, params) -> str | None:
"""Direct media: PDF/images/videos/audio can live in a plain iframe."""
return url
_HANDLERS = (
("youtube.com", _embed_youtube),
("youtu.be", _embed_youtube),
("vimeo.com", _embed_vimeo),
("loom.com", _embed_loom),
("figma.com", _embed_figma),
("google.com/maps", _embed_map),
("maps.app.goo.gl", _embed_map),
("codepen.io", _embed_codepen),
("miro.com", _embed_miro),
("open.spotify.com", _embed_spotify),
("spotify.com", _embed_spotify),
("soundcloud.com", _embed_soundcloud),
("twitch.tv", _embed_twitch),
("twitter.com", _embed_twitter),
("x.com", _embed_twitter),
("pinterest", _embed_pinterest),
)
def embed_src(url: str) -> str | None:
"""Return the embeddable iframe src for a URL, or None if it can't embed."""
if not url:
return None
url = url.strip()
u = urlparse(url if url.startswith("http") else "https://" + url)
if u.scheme not in ("http", "https"):
return None
(u.hostname or "").lower().replace("www.", "")
netloc = (u.netloc or "").lower()
params = parse_qs(u.query)
# Google Maps share links encode the query in the path (…&q=/maps/…)
encoded = url
for needle, handler in _HANDLERS:
if needle in netloc:
return handler(encoded, u.path, params)
return _embed_files(encoded, u.path, params)
_IMAGE_EXT = re.compile(r"\.(png|jpe?g|gif|webp|svg|bmp|ico|avif)$", re.I)
_PDF_EXT = re.compile(r"\.pdf$", re.I)
_VIDEO_EXT = re.compile(r"\.(mp4|webm|ogg|ogv|mov|m4v)$", re.I)
_AUDIO_EXT = re.compile(r"\.(mp3|wav|ogg|oga|m4a|flac|aac)$", re.I)
def inline_kind(url: str) -> str | None:
"""Best inline renderer for a URL: 'iframe' | 'image' | 'pdf' | 'video' |
| 'audio'. Returns None when the URL should open in a new tab."""
if not url:
return None
url = url.strip()
path = urlparse(url).path
if _IMAGE_EXT.search(path):
return "image"
if _PDF_EXT.search(path):
return "pdf"
if _VIDEO_EXT.search(path):
return "video"
if _AUDIO_EXT.search(path):
return "audio"
if url.startswith(("http://", "https://")):
return "iframe"
return None
def embed_html(src: str, *, height: int = 520) -> str:
"""A responsive, borderless iframe for a provider embed URL."""
return (
f'<iframe src="{src}" loading="lazy" '
f'style="width:100%;height:{height}px;border:none;border-radius:8px;background:#000;" '
f'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; '
f'picture-in-picture" allowfullscreen></iframe>'
)