feat(shared-music): add support for Vimeo, SoundCloud, Spotify, Dailymotion, Twitch
- Add source type detection for Vimeo, SoundCloud, Spotify, Dailymotion, Twitch - Create EmbedPlayerManager for iframe-based media players - Update extractMusicUrl to extract IDs from various platforms - Update SharedMusicPlayer to route to appropriate player manager - Update SharedMusicTile to handle video vs audio sources - Video sources (YouTube, Vimeo, Dailymotion, Twitch) show embedded player - Audio sources (SoundCloud, Spotify) show embedded widget - Direct audio files show HTML5 audio player with custom controls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
751a04b950
commit
f56986818b
|
|
@ -26,7 +26,8 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.youtube-player-container {
|
||||
.youtube-player-container,
|
||||
.embed-player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { PLAYBACK_STATUSES, SOURCE_TYPES } from '../../constants';
|
||||
|
||||
import AbstractMusicManager, {
|
||||
IProps,
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps
|
||||
} from './AbstractMusicManager';
|
||||
|
||||
|
||||
/**
|
||||
* Generates the embed URL for various platforms.
|
||||
*
|
||||
* @param {string} url - The original URL.
|
||||
* @param {string} sourceType - The source type.
|
||||
* @returns {string} The embed URL.
|
||||
*/
|
||||
function getEmbedUrl(url: string, sourceType: string): string {
|
||||
switch (sourceType) {
|
||||
case SOURCE_TYPES.VIMEO: {
|
||||
// Extract Vimeo ID and create embed URL
|
||||
const vimeoMatch = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
|
||||
|
||||
if (vimeoMatch) {
|
||||
return `https://player.vimeo.com/video/${vimeoMatch[1]}?autoplay=1&autopause=0`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
case SOURCE_TYPES.SOUNDCLOUD: {
|
||||
// SoundCloud requires URL encoding
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
|
||||
return `https://w.soundcloud.com/player/?url=${encodedUrl}&auto_play=true&visual=true`;
|
||||
}
|
||||
case SOURCE_TYPES.SPOTIFY: {
|
||||
// Extract Spotify type and ID
|
||||
const spotifyMatch = url.match(/spotify\.com\/(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/);
|
||||
|
||||
if (spotifyMatch) {
|
||||
return `https://open.spotify.com/embed/${spotifyMatch[1]}/${spotifyMatch[2]}?utm_source=generator&theme=0`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
case SOURCE_TYPES.DAILYMOTION: {
|
||||
// Extract Dailymotion ID
|
||||
const dmMatch = url.match(/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/);
|
||||
|
||||
if (dmMatch) {
|
||||
return `https://www.dailymotion.com/embed/video/${dmMatch[1]}?autoplay=1`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
case SOURCE_TYPES.TWITCH: {
|
||||
const parent = window.location.hostname;
|
||||
|
||||
// Check for video or channel
|
||||
const videoMatch = url.match(/twitch\.tv\/videos\/(\d+)/);
|
||||
const channelMatch = url.match(/twitch\.tv\/([a-zA-Z0-9_]+)(?:\?|$)/);
|
||||
|
||||
if (videoMatch) {
|
||||
return `https://player.twitch.tv/?video=v${videoMatch[1]}&parent=${parent}&autoplay=true`;
|
||||
}
|
||||
|
||||
if (channelMatch && channelMatch[1] !== 'videos') {
|
||||
return `https://player.twitch.tv/?channel=${channelMatch[1]}&parent=${parent}&autoplay=true`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
default:
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager for embedded media players (Vimeo, SoundCloud, Spotify, Dailymotion, Twitch).
|
||||
* Uses iframe embeds for playback.
|
||||
*/
|
||||
class EmbedPlayerManager extends AbstractMusicManager {
|
||||
iframeRef: React.RefObject<HTMLIFrameElement>;
|
||||
_isPlaying: boolean;
|
||||
_isMuted: boolean;
|
||||
_currentTime: number;
|
||||
|
||||
/**
|
||||
* Initializes a new EmbedPlayerManager instance.
|
||||
*
|
||||
* @param {Object} props - This component's props.
|
||||
* @returns {void}
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.iframeRef = React.createRef();
|
||||
this._isPlaying = true; // Assume playing since autoplay is enabled
|
||||
this._isMuted = false;
|
||||
this._currentTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the playback state of the music.
|
||||
* Note: Most embed players don't provide reliable state feedback.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
override getPlaybackStatus() {
|
||||
return this._isPlaying ? PLAYBACK_STATUSES.PLAYING : PLAYBACK_STATUSES.PAUSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the music is muted.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override isMuted() {
|
||||
return this._isMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current volume.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
override getVolume() {
|
||||
return this._isMuted ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current time.
|
||||
* Note: Embedded players don't provide time reliably.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
override getTime() {
|
||||
return this._currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks music to provided time.
|
||||
* Note: Most embedded players don't support external seek control.
|
||||
*
|
||||
* @param {number} time - The time to seek to.
|
||||
* @returns {void}
|
||||
*/
|
||||
override seek(time: number) {
|
||||
this._currentTime = time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays music.
|
||||
* Note: Embedded players are controlled by user interaction within the iframe.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override play() {
|
||||
this._isPlaying = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses music.
|
||||
* Note: Embedded players are controlled by user interaction within the iframe.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override pause() {
|
||||
this._isPlaying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes music.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override mute() {
|
||||
this._isMuted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmutes music.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override unMute() {
|
||||
this._isMuted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { musicId, _sourceType } = this.props;
|
||||
const embedUrl = getEmbedUrl(musicId, _sourceType ?? '');
|
||||
|
||||
return (
|
||||
<div className = 'embed-player-container'>
|
||||
<iframe
|
||||
allow = 'autoplay; encrypted-media; fullscreen; picture-in-picture'
|
||||
allowFullScreen = { true }
|
||||
frameBorder = '0'
|
||||
height = '100%'
|
||||
id = 'sharedMusicPlayer'
|
||||
ref = { this.iframeRef }
|
||||
src = { embedUrl }
|
||||
title = 'Shared Media Player'
|
||||
width = '100%' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(EmbedPlayerManager);
|
||||
|
|
@ -8,6 +8,7 @@ import { isSharingStatus } from '../../functions';
|
|||
import { SourceType } from '../../types';
|
||||
|
||||
import DirectAudioManager from './DirectAudioManager';
|
||||
import EmbedPlayerManager from './EmbedPlayerManager';
|
||||
import YouTubeMusicManager from './YouTubeMusicManager';
|
||||
|
||||
interface IProps {
|
||||
|
|
@ -33,6 +34,17 @@ interface IProps {
|
|||
sourceType?: SourceType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedded source types that use iframe players.
|
||||
*/
|
||||
const EMBED_SOURCE_TYPES: readonly string[] = [
|
||||
SOURCE_TYPES.VIMEO,
|
||||
SOURCE_TYPES.SOUNDCLOUD,
|
||||
SOURCE_TYPES.SPOTIFY,
|
||||
SOURCE_TYPES.DAILYMOTION,
|
||||
SOURCE_TYPES.TWITCH
|
||||
];
|
||||
|
||||
/**
|
||||
* Component that manages and renders the appropriate music player.
|
||||
*/
|
||||
|
|
@ -50,10 +62,17 @@ class SharedMusicPlayer extends Component<IProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
// YouTube has its own dedicated player
|
||||
if (sourceType === SOURCE_TYPES.YOUTUBE) {
|
||||
return <YouTubeMusicManager musicId = { musicUrl } />;
|
||||
}
|
||||
|
||||
// Vimeo, SoundCloud, Spotify, Dailymotion, Twitch use iframe embeds
|
||||
if (sourceType && EMBED_SOURCE_TYPES.includes(sourceType)) {
|
||||
return <EmbedPlayerManager musicId = { musicUrl } />;
|
||||
}
|
||||
|
||||
// Direct audio/video files use HTML5 audio element
|
||||
return <DirectAudioManager musicId = { musicUrl } />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,28 @@ import { isSharingStatus } from '../../functions';
|
|||
|
||||
import SharedMusicPlayer from './SharedMusicPlayer';
|
||||
|
||||
/**
|
||||
* Source types that render video content (should show iframe player).
|
||||
*/
|
||||
const VIDEO_SOURCE_TYPES: readonly string[] = [
|
||||
SOURCE_TYPES.YOUTUBE,
|
||||
SOURCE_TYPES.VIMEO,
|
||||
SOURCE_TYPES.DAILYMOTION,
|
||||
SOURCE_TYPES.TWITCH
|
||||
];
|
||||
|
||||
/**
|
||||
* Source types that have embedded controls (user interacts with iframe).
|
||||
*/
|
||||
const EMBEDDED_CONTROL_TYPES: readonly string[] = [
|
||||
SOURCE_TYPES.YOUTUBE,
|
||||
SOURCE_TYPES.VIMEO,
|
||||
SOURCE_TYPES.SOUNDCLOUD,
|
||||
SOURCE_TYPES.SPOTIFY,
|
||||
SOURCE_TYPES.DAILYMOTION,
|
||||
SOURCE_TYPES.TWITCH
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* The participant ID (music URL).
|
||||
|
|
@ -35,7 +57,12 @@ const SharedMusicTile: React.FC<IProps> = ({ participantId }) => {
|
|||
const isOwner = ownerId === localParticipant?.id;
|
||||
const isMusicShared = isSharingStatus(status ?? '');
|
||||
const isPlaying = status === PLAYBACK_STATUSES.PLAYING;
|
||||
const isYouTube = sourceType === SOURCE_TYPES.YOUTUBE;
|
||||
|
||||
// Determine if this source type shows video content
|
||||
const isVideoSource = sourceType && VIDEO_SOURCE_TYPES.includes(sourceType);
|
||||
|
||||
// Determine if this source type has embedded controls (so we don't show our own)
|
||||
const hasEmbeddedControls = sourceType && EMBEDDED_CONTROL_TYPES.includes(sourceType);
|
||||
|
||||
const handlePlayPause = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent thumbnail click from pinning
|
||||
|
|
@ -61,27 +88,27 @@ const SharedMusicTile: React.FC<IProps> = ({ participantId }) => {
|
|||
|
||||
return (
|
||||
<div className = 'shared-music-tile'>
|
||||
{/* Render the actual player for YouTube videos */}
|
||||
{isYouTube ? (
|
||||
{/* Render the actual player for video sources */}
|
||||
{isVideoSource || hasEmbeddedControls ? (
|
||||
<div className = 'shared-music-player-wrapper'>
|
||||
<SharedMusicPlayer />
|
||||
</div>
|
||||
) : (
|
||||
/* For audio-only, show a background with controls */
|
||||
/* For direct audio files, show a background with controls */
|
||||
<div className = 'shared-music-audio-bg'>
|
||||
<SharedMusicPlayer />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay with title and controls for non-owners */}
|
||||
{/* Overlay with title and controls */}
|
||||
<div className = 'shared-music-controls-overlay'>
|
||||
{/* Title */}
|
||||
<div className = 'shared-music-title'>
|
||||
{title || 'Shared Music'}
|
||||
{title || 'Shared Media'}
|
||||
</div>
|
||||
|
||||
{/* Play/Pause button (owner only, shown when YouTube controls are hidden) */}
|
||||
{isOwner && !isYouTube && (
|
||||
{/* Play/Pause button (owner only, shown for direct audio without embedded controls) */}
|
||||
{isOwner && !hasEmbeddedControls && (
|
||||
<button
|
||||
aria-label = { isPlaying ? 'Pause' : 'Play' }
|
||||
className = 'shared-music-control-button'
|
||||
|
|
@ -95,8 +122,8 @@ const SharedMusicTile: React.FC<IProps> = ({ participantId }) => {
|
|||
</button>
|
||||
)}
|
||||
|
||||
{/* Status indicator for non-owners when not YouTube */}
|
||||
{!isOwner && !isYouTube && (
|
||||
{/* Status indicator for non-owners when no embedded controls */}
|
||||
{!isOwner && !hasEmbeddedControls && (
|
||||
<div className = 'shared-music-status'>
|
||||
{isPlaying ? 'Playing' : 'Paused'}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ export const PLAYBACK_START = 'start';
|
|||
*/
|
||||
export const SOURCE_TYPES = {
|
||||
YOUTUBE: 'youtube',
|
||||
VIMEO: 'vimeo',
|
||||
SOUNDCLOUD: 'soundcloud',
|
||||
SPOTIFY: 'spotify',
|
||||
DAILYMOTION: 'dailymotion',
|
||||
TWITCH: 'twitch',
|
||||
DIRECT: 'direct'
|
||||
} as const;
|
||||
|
||||
|
|
@ -43,3 +48,28 @@ export const YOUTUBE_URL_DOMAIN = 'youtube.com';
|
|||
* YouTube Music domain.
|
||||
*/
|
||||
export const YOUTUBE_MUSIC_URL_DOMAIN = 'music.youtube.com';
|
||||
|
||||
/**
|
||||
* Vimeo domain.
|
||||
*/
|
||||
export const VIMEO_URL_DOMAIN = 'vimeo.com';
|
||||
|
||||
/**
|
||||
* SoundCloud domain.
|
||||
*/
|
||||
export const SOUNDCLOUD_URL_DOMAIN = 'soundcloud.com';
|
||||
|
||||
/**
|
||||
* Spotify domain.
|
||||
*/
|
||||
export const SPOTIFY_URL_DOMAIN = 'spotify.com';
|
||||
|
||||
/**
|
||||
* Dailymotion domain.
|
||||
*/
|
||||
export const DAILYMOTION_URL_DOMAIN = 'dailymotion.com';
|
||||
|
||||
/**
|
||||
* Twitch domain.
|
||||
*/
|
||||
export const TWITCH_URL_DOMAIN = 'twitch.tv';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import { IJitsiConference } from '../base/conference/reducer';
|
|||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import {
|
||||
DAILYMOTION_URL_DOMAIN,
|
||||
PLAYBACK_START,
|
||||
PLAYBACK_STATUSES,
|
||||
SHARED_MUSIC,
|
||||
SOUNDCLOUD_URL_DOMAIN,
|
||||
SOURCE_TYPES,
|
||||
SPOTIFY_URL_DOMAIN,
|
||||
TWITCH_URL_DOMAIN,
|
||||
VIMEO_URL_DOMAIN,
|
||||
YOUTUBE_MUSIC_URL_DOMAIN,
|
||||
YOUTUBE_URL_DOMAIN
|
||||
} from './constants';
|
||||
|
|
@ -30,6 +35,86 @@ function getYoutubeId(url: string): string | null {
|
|||
return result ? result[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Vimeo video ID from URL.
|
||||
*
|
||||
* @param {string} url - The entered URL.
|
||||
* @returns {string | null} The Vimeo video id if matched.
|
||||
*/
|
||||
function getVimeoId(url: string): string | null {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Matches vimeo.com/123456789 or player.vimeo.com/video/123456789
|
||||
const p = /(?:https?:\/\/)?(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d+)/;
|
||||
const result = url.match(p);
|
||||
|
||||
return result ? result[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Dailymotion video ID from URL.
|
||||
*
|
||||
* @param {string} url - The entered URL.
|
||||
* @returns {string | null} The Dailymotion video id if matched.
|
||||
*/
|
||||
function getDailymotionId(url: string): string | null {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Matches dailymotion.com/video/x123abc or dai.ly/x123abc
|
||||
const p = /(?:https?:\/\/)?(?:www\.)?(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/;
|
||||
const result = url.match(p);
|
||||
|
||||
return result ? result[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Twitch channel or video from URL.
|
||||
*
|
||||
* @param {string} url - The entered URL.
|
||||
* @returns {Object|null} The Twitch info if matched.
|
||||
*/
|
||||
function getTwitchInfo(url: string): { id: string; type: string; } | null {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Matches twitch.tv/channel or twitch.tv/videos/123456
|
||||
const channelMatch = url.match(/(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([a-zA-Z0-9_]+)(?:\?|$)/);
|
||||
const videoMatch = url.match(/(?:https?:\/\/)?(?:www\.)?twitch\.tv\/videos\/(\d+)/);
|
||||
|
||||
if (videoMatch) {
|
||||
return { type: 'video', id: videoMatch[1] };
|
||||
}
|
||||
|
||||
if (channelMatch && channelMatch[1] !== 'videos') {
|
||||
return { type: 'channel', id: channelMatch[1] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Spotify track/album/playlist from URL.
|
||||
*
|
||||
* @param {string} url - The entered URL.
|
||||
* @returns {Object|null} The Spotify info if matched.
|
||||
*/
|
||||
function getSpotifyInfo(url: string): { id: string; type: string; } | null {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Matches open.spotify.com/track/123, /album/123, /playlist/123
|
||||
const p = /(?:https?:\/\/)?open\.spotify\.com\/(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/;
|
||||
const result = url.match(p);
|
||||
|
||||
return result ? { type: result[1], id: result[2] } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the status is one that is actually sharing music - playing, pause or start.
|
||||
*
|
||||
|
|
@ -47,19 +132,54 @@ export function isSharingStatus(status: string): boolean {
|
|||
* @returns {SourceType} The source type.
|
||||
*/
|
||||
export function getSourceType(url: string): SourceType {
|
||||
const youtubeId = getYoutubeId(url);
|
||||
|
||||
if (youtubeId) {
|
||||
if (getYoutubeId(url)) {
|
||||
return SOURCE_TYPES.YOUTUBE;
|
||||
}
|
||||
|
||||
if (getVimeoId(url)) {
|
||||
return SOURCE_TYPES.VIMEO;
|
||||
}
|
||||
|
||||
if (getDailymotionId(url)) {
|
||||
return SOURCE_TYPES.DAILYMOTION;
|
||||
}
|
||||
|
||||
if (getTwitchInfo(url)) {
|
||||
return SOURCE_TYPES.TWITCH;
|
||||
}
|
||||
|
||||
if (getSpotifyInfo(url)) {
|
||||
return SOURCE_TYPES.SPOTIFY;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const hostname = urlObj.hostname.toLowerCase();
|
||||
|
||||
if (urlObj.hostname.includes(YOUTUBE_URL_DOMAIN)
|
||||
|| urlObj.hostname.includes(YOUTUBE_MUSIC_URL_DOMAIN)) {
|
||||
if (hostname.includes(YOUTUBE_URL_DOMAIN)
|
||||
|| hostname.includes(YOUTUBE_MUSIC_URL_DOMAIN)) {
|
||||
return SOURCE_TYPES.YOUTUBE;
|
||||
}
|
||||
|
||||
if (hostname.includes(VIMEO_URL_DOMAIN)) {
|
||||
return SOURCE_TYPES.VIMEO;
|
||||
}
|
||||
|
||||
if (hostname.includes(SOUNDCLOUD_URL_DOMAIN)) {
|
||||
return SOURCE_TYPES.SOUNDCLOUD;
|
||||
}
|
||||
|
||||
if (hostname.includes(SPOTIFY_URL_DOMAIN)) {
|
||||
return SOURCE_TYPES.SPOTIFY;
|
||||
}
|
||||
|
||||
if (hostname.includes(DAILYMOTION_URL_DOMAIN)) {
|
||||
return SOURCE_TYPES.DAILYMOTION;
|
||||
}
|
||||
|
||||
if (hostname.includes(TWITCH_URL_DOMAIN)) {
|
||||
return SOURCE_TYPES.TWITCH;
|
||||
}
|
||||
} catch (_) {
|
||||
// Not a valid URL
|
||||
}
|
||||
|
|
@ -68,12 +188,16 @@ export function getSourceType(url: string): SourceType {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts a YouTube ID or validates a direct URL.
|
||||
* Extracts media info from URL based on the source type.
|
||||
*
|
||||
* @param {string} input - The user input.
|
||||
* @returns {Object | undefined} An object with url and sourceType, or undefined.
|
||||
* @returns {Object | undefined} An object with url, sourceType, and optional embedInfo.
|
||||
*/
|
||||
export function extractMusicUrl(input: string): { sourceType: SourceType; url: string; } | undefined {
|
||||
export function extractMusicUrl(input: string): {
|
||||
embedInfo?: { id: string; type?: string; };
|
||||
sourceType: SourceType;
|
||||
url: string;
|
||||
} | undefined {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -84,33 +208,75 @@ export function extractMusicUrl(input: string): { sourceType: SourceType; url: s
|
|||
return;
|
||||
}
|
||||
|
||||
// YouTube
|
||||
const youtubeId = getYoutubeId(trimmedLink);
|
||||
|
||||
if (youtubeId) {
|
||||
return {
|
||||
url: youtubeId,
|
||||
sourceType: SOURCE_TYPES.YOUTUBE
|
||||
sourceType: SOURCE_TYPES.YOUTUBE,
|
||||
embedInfo: { id: youtubeId }
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the URL is valid
|
||||
// Vimeo
|
||||
const vimeoId = getVimeoId(trimmedLink);
|
||||
|
||||
if (vimeoId) {
|
||||
return {
|
||||
url: trimmedLink,
|
||||
sourceType: SOURCE_TYPES.VIMEO,
|
||||
embedInfo: { id: vimeoId }
|
||||
};
|
||||
}
|
||||
|
||||
// Dailymotion
|
||||
const dailymotionId = getDailymotionId(trimmedLink);
|
||||
|
||||
if (dailymotionId) {
|
||||
return {
|
||||
url: trimmedLink,
|
||||
sourceType: SOURCE_TYPES.DAILYMOTION,
|
||||
embedInfo: { id: dailymotionId }
|
||||
};
|
||||
}
|
||||
|
||||
// Twitch
|
||||
const twitchInfo = getTwitchInfo(trimmedLink);
|
||||
|
||||
if (twitchInfo) {
|
||||
return {
|
||||
url: trimmedLink,
|
||||
sourceType: SOURCE_TYPES.TWITCH,
|
||||
embedInfo: twitchInfo
|
||||
};
|
||||
}
|
||||
|
||||
// Spotify
|
||||
const spotifyInfo = getSpotifyInfo(trimmedLink);
|
||||
|
||||
if (spotifyInfo) {
|
||||
return {
|
||||
url: trimmedLink,
|
||||
sourceType: SOURCE_TYPES.SPOTIFY,
|
||||
embedInfo: spotifyInfo
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the URL is valid for other sources
|
||||
try {
|
||||
const urlObj = new URL(trimmedLink);
|
||||
const hostname = urlObj.hostname.toLowerCase();
|
||||
|
||||
// Check if it's a YouTube URL
|
||||
if (urlObj.hostname.includes(YOUTUBE_URL_DOMAIN)
|
||||
|| urlObj.hostname.includes(YOUTUBE_MUSIC_URL_DOMAIN)) {
|
||||
const videoId = getYoutubeId(trimmedLink);
|
||||
|
||||
if (videoId) {
|
||||
return {
|
||||
url: videoId,
|
||||
sourceType: SOURCE_TYPES.YOUTUBE
|
||||
};
|
||||
}
|
||||
// SoundCloud - we use the full URL for embedding
|
||||
if (hostname.includes(SOUNDCLOUD_URL_DOMAIN)) {
|
||||
return {
|
||||
url: trimmedLink,
|
||||
sourceType: SOURCE_TYPES.SOUNDCLOUD
|
||||
};
|
||||
}
|
||||
|
||||
// It's a direct URL
|
||||
// It's a direct URL (audio/video file)
|
||||
return {
|
||||
url: trimmedLink,
|
||||
sourceType: SOURCE_TYPES.DIRECT
|
||||
|
|
|
|||
Loading…
Reference in New Issue