add a little api
This commit is contained in:
parent
49dcfb28de
commit
569b5a5846
@ -1,2 +1,3 @@
|
|||||||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||||
VITE_SUPABASE_ANON_KEY=your-anon-key
|
VITE_SUPABASE_ANON_KEY=your-anon-key
|
||||||
|
VITE_API_URL=http://localhost:3001
|
||||||
|
|||||||
4
api/.env.example
Normal file
4
api/.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
SPOTIFY_CLIENT_ID=
|
||||||
|
SPOTIFY_CLIENT_SECRET=
|
||||||
|
YOUTUBE_API_KEY=
|
||||||
|
PORT=3001
|
||||||
15
api/bun.lock
Normal file
15
api/bun.lock
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "wetalk-api",
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.7.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
12
api/package.json
Normal file
12
api/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "wetalk-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --hot src/index.ts",
|
||||||
|
"start": "bun run src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
234
api/src/index.ts
Normal file
234
api/src/index.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { Hono } from 'hono'
|
||||||
|
import { cors } from 'hono/cors'
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.use('*', cors({
|
||||||
|
origin: '*',
|
||||||
|
allowMethods: ['GET', 'POST'],
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ── Spotify token cache ──
|
||||||
|
let spotifyToken: string | null = null
|
||||||
|
let spotifyTokenExpiry = 0
|
||||||
|
|
||||||
|
const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID || ''
|
||||||
|
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET || ''
|
||||||
|
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY || ''
|
||||||
|
|
||||||
|
async function getSpotifyToken(): Promise<string | null> {
|
||||||
|
if (spotifyToken && Date.now() < spotifyTokenExpiry) return spotifyToken
|
||||||
|
if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET) return null
|
||||||
|
|
||||||
|
const res = await fetch('https://accounts.spotify.com/api/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Authorization: `Basic ${btoa(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`)}`,
|
||||||
|
},
|
||||||
|
body: 'grant_type=client_credentials',
|
||||||
|
})
|
||||||
|
if (!res.ok) return null
|
||||||
|
const data = await res.json()
|
||||||
|
spotifyToken = data.access_token
|
||||||
|
spotifyTokenExpiry = Date.now() + (data.expires_in - 60) * 1000
|
||||||
|
return spotifyToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── URL parsing helpers ──
|
||||||
|
function parseDuration(iso: string): number {
|
||||||
|
const match = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/)
|
||||||
|
if (!match) return 0
|
||||||
|
return parseInt(match[1] || '0') * 3600 + parseInt(match[2] || '0') * 60 + parseInt(match[3] || '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractInfo(url: string): { platform: string; id: string; type?: string } | null {
|
||||||
|
// YouTube
|
||||||
|
const ytMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/)
|
||||||
|
if (ytMatch) return { platform: 'youtube', id: ytMatch[1] }
|
||||||
|
|
||||||
|
// Dailymotion
|
||||||
|
const dmMatch = url.match(/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/)
|
||||||
|
if (dmMatch) return { platform: 'dailymotion', id: dmMatch[1] }
|
||||||
|
|
||||||
|
// SoundCloud
|
||||||
|
if (url.includes('soundcloud.com/')) return { platform: 'soundcloud', id: url }
|
||||||
|
|
||||||
|
// Spotify
|
||||||
|
const spotifyMatch = url.match(/open\.spotify\.com\/(episode|show|track)\/([a-zA-Z0-9]+)/)
|
||||||
|
if (spotifyMatch) return { platform: 'spotify', id: spotifyMatch[2], type: spotifyMatch[1] }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main endpoint ──
|
||||||
|
app.get('/api/metadata', async (c) => {
|
||||||
|
const url = c.req.query('url')
|
||||||
|
if (!url) return c.json({ error: 'Missing url parameter' }, 400)
|
||||||
|
|
||||||
|
const info = extractInfo(url)
|
||||||
|
if (!info) return c.json({ error: 'Unsupported URL' }, 400)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ── YouTube ──
|
||||||
|
if (info.platform === 'youtube') {
|
||||||
|
if (YOUTUBE_API_KEY) {
|
||||||
|
const apiUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=${info.id}&key=${YOUTUBE_API_KEY}`
|
||||||
|
const res = await fetch(apiUrl)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
const video = data.items?.[0]
|
||||||
|
if (video) {
|
||||||
|
const s = video.snippet || {}
|
||||||
|
const thumbs = s.thumbnails || {}
|
||||||
|
const thumb = thumbs.maxres?.url || thumbs.standard?.url || thumbs.high?.url || thumbs.medium?.url || thumbs.default?.url || ''
|
||||||
|
return c.json({
|
||||||
|
title: s.title || '',
|
||||||
|
description: (s.description || '').slice(0, 500),
|
||||||
|
thumbnail: thumb,
|
||||||
|
duration: parseDuration(video.contentDetails?.duration || ''),
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'YouTube',
|
||||||
|
author: s.channelTitle || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback oEmbed
|
||||||
|
const res = await fetch(`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${info.id}&format=json`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
return c.json({
|
||||||
|
title: data.title || '',
|
||||||
|
description: data.author_name ? `Par ${data.author_name}` : '',
|
||||||
|
thumbnail: `https://img.youtube.com/vi/${info.id}/maxresdefault.jpg`,
|
||||||
|
duration: 0,
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'YouTube',
|
||||||
|
author: data.author_name || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Spotify ──
|
||||||
|
if (info.platform === 'spotify') {
|
||||||
|
const token = await getSpotifyToken()
|
||||||
|
if (token) {
|
||||||
|
const contentType = info.type || 'episode'
|
||||||
|
const endpoint = contentType === 'show'
|
||||||
|
? `https://api.spotify.com/v1/shows/${info.id}?market=FR`
|
||||||
|
: contentType === 'track'
|
||||||
|
? `https://api.spotify.com/v1/tracks/${info.id}?market=FR`
|
||||||
|
: `https://api.spotify.com/v1/episodes/${info.id}?market=FR`
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (contentType === 'episode') {
|
||||||
|
const images = data.images || data.show?.images || []
|
||||||
|
return c.json({
|
||||||
|
title: data.name || '',
|
||||||
|
description: data.description || '',
|
||||||
|
thumbnail: images[0]?.url || '',
|
||||||
|
duration: Math.floor((data.duration_ms || 0) / 1000),
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'Spotify',
|
||||||
|
author: data.show?.name || data.show?.publisher || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (contentType === 'show') {
|
||||||
|
return c.json({
|
||||||
|
title: data.name || '',
|
||||||
|
description: data.description || '',
|
||||||
|
thumbnail: (data.images || [])[0]?.url || '',
|
||||||
|
duration: 0,
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'Spotify',
|
||||||
|
author: data.publisher || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (contentType === 'track') {
|
||||||
|
const artist = data.artists?.map((a: any) => a.name).join(', ') || ''
|
||||||
|
return c.json({
|
||||||
|
title: data.name || '',
|
||||||
|
description: artist ? `Par ${artist}` : '',
|
||||||
|
thumbnail: (data.album?.images || [])[0]?.url || '',
|
||||||
|
duration: Math.floor((data.duration_ms || 0) / 1000),
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'Spotify',
|
||||||
|
author: artist,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback oEmbed
|
||||||
|
const res = await fetch(`https://open.spotify.com/oembed?url=${encodeURIComponent(url)}`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
const parts = (data.title || '').split(' - ')
|
||||||
|
return c.json({
|
||||||
|
title: parts[0]?.trim() || data.title || '',
|
||||||
|
description: '',
|
||||||
|
thumbnail: data.thumbnail_url || '',
|
||||||
|
duration: 0,
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'Spotify',
|
||||||
|
author: parts.length > 1 ? parts[parts.length - 1]?.trim() : '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dailymotion ──
|
||||||
|
if (info.platform === 'dailymotion') {
|
||||||
|
const apiUrl = `https://api.dailymotion.com/video/${info.id}?fields=title,description,duration,thumbnail_720_url,owner.screenname`
|
||||||
|
const res = await fetch(apiUrl)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
const owner = data.owner?.screenname || ''
|
||||||
|
return c.json({
|
||||||
|
title: data.title || '',
|
||||||
|
description: data.description || '',
|
||||||
|
thumbnail: data.thumbnail_720_url || '',
|
||||||
|
duration: data.duration || 0,
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'Dailymotion',
|
||||||
|
author: owner,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SoundCloud ──
|
||||||
|
if (info.platform === 'soundcloud') {
|
||||||
|
const res = await fetch(`https://soundcloud.com/oembed?url=${encodeURIComponent(url)}&format=json`)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
return c.json({
|
||||||
|
title: data.title || '',
|
||||||
|
description: data.description || '',
|
||||||
|
thumbnail: data.thumbnail_url || '',
|
||||||
|
duration: 0,
|
||||||
|
audioUrl: url,
|
||||||
|
platform: 'SoundCloud',
|
||||||
|
author: data.author_name || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: 'Could not fetch metadata' }, 404)
|
||||||
|
} catch (e) {
|
||||||
|
return c.json({ error: 'Internal error' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/health', (c) => c.json({ ok: true }))
|
||||||
|
|
||||||
|
const port = parseInt(process.env.PORT || '3001')
|
||||||
|
console.log(`[wetalk-api] listening on :${port}`)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
port,
|
||||||
|
fetch: app.fetch,
|
||||||
|
}
|
||||||
@ -12,3 +12,15 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- CHOKIDAR_USEPOLLING=true
|
- CHOKIDAR_USEPOLLING=true
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: oven/bun:1
|
||||||
|
working_dir: /app
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
volumes:
|
||||||
|
- ./api:/app
|
||||||
|
- /app/node_modules
|
||||||
|
env_file:
|
||||||
|
- ./api/.env
|
||||||
|
command: bun run src/index.ts
|
||||||
|
|||||||
@ -21,7 +21,9 @@ export function PlayerBar() {
|
|||||||
|
|
||||||
if (!current) return null
|
if (!current) return null
|
||||||
|
|
||||||
const isYouTube = isExternal && current && isExternalUrl(current.audio_url) && getEmbedInfo(current.audio_url)?.platform === 'youtube'
|
const embedInfo = isExternal && current ? getEmbedInfo(current.audio_url) : null
|
||||||
|
const isYouTube = embedInfo?.platform === 'youtube'
|
||||||
|
const isSpotify = embedInfo?.platform === 'spotify'
|
||||||
const pct = duration > 0 ? (progress / duration) * 100 : 0
|
const pct = duration > 0 ? (progress / duration) * 100 : 0
|
||||||
const displayPct = isDragging ? dragPct : pct
|
const displayPct = isDragging ? dragPct : pct
|
||||||
// Allow seeking for native audio and YouTube (via IFrame API)
|
// Allow seeking for native audio and YouTube (via IFrame API)
|
||||||
@ -97,6 +99,19 @@ export function PlayerBar() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Spotify player - mini floating */}
|
||||||
|
{isSpotify && embedInfo && (
|
||||||
|
<div className="fixed bottom-[106px] right-4 z-[60] rounded-xl overflow-hidden shadow-[0_4px_20px_rgba(0,0,0,0.3)]">
|
||||||
|
<iframe
|
||||||
|
src={embedInfo.embedUrl}
|
||||||
|
width="300"
|
||||||
|
height="152"
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
loading="lazy"
|
||||||
|
style={{ border: 0, borderRadius: 12 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#1E1B33] text-white shadow-[0_-2px_20px_rgba(0,0,0,0.3)]">
|
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#1E1B33] text-white shadow-[0_-2px_20px_rgba(0,0,0,0.3)]">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
||||||
{/* Main row */}
|
{/* Main row */}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export type EmbedInfo = {
|
export type EmbedInfo = {
|
||||||
platform: 'youtube' | 'dailymotion' | 'soundcloud'
|
platform: 'youtube' | 'dailymotion' | 'soundcloud' | 'spotify'
|
||||||
id: string
|
id: string
|
||||||
embedUrl: string
|
embedUrl: string
|
||||||
}
|
}
|
||||||
@ -38,6 +38,20 @@ export function getEmbedInfo(url: string): EmbedInfo | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spotify (episodes, shows, tracks)
|
||||||
|
const spotifyMatch = url.match(
|
||||||
|
/open\.spotify\.com\/(episode|show|track)\/([a-zA-Z0-9]+)/,
|
||||||
|
)
|
||||||
|
if (spotifyMatch) {
|
||||||
|
const type = spotifyMatch[1]
|
||||||
|
const id = spotifyMatch[2]
|
||||||
|
return {
|
||||||
|
platform: 'spotify',
|
||||||
|
id,
|
||||||
|
embedUrl: `https://open.spotify.com/embed/${type}/${id}?utm_source=generator&theme=0`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,148 +18,16 @@ interface OEmbedData {
|
|||||||
author: string
|
author: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractVideoId(url: string): { platform: string; id: string } | null {
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'
|
||||||
// YouTube
|
|
||||||
const ytMatch = url.match(
|
|
||||||
/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
|
|
||||||
)
|
|
||||||
if (ytMatch) return { platform: 'youtube', id: ytMatch[1] }
|
|
||||||
|
|
||||||
// Dailymotion
|
|
||||||
const dmMatch = url.match(
|
|
||||||
/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/,
|
|
||||||
)
|
|
||||||
if (dmMatch) return { platform: 'dailymotion', id: dmMatch[1] }
|
|
||||||
|
|
||||||
// SoundCloud (handled via oEmbed, no ID extraction needed)
|
|
||||||
if (url.includes('soundcloud.com/')) return { platform: 'soundcloud', id: url }
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse ISO 8601 duration (PT1H2M30S) to seconds
|
|
||||||
function parseDuration(iso: string): number {
|
|
||||||
const match = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/)
|
|
||||||
if (!match) return 0
|
|
||||||
const h = parseInt(match[1] || '0')
|
|
||||||
const m = parseInt(match[2] || '0')
|
|
||||||
const s = parseInt(match[3] || '0')
|
|
||||||
return h * 3600 + m * 60 + s
|
|
||||||
}
|
|
||||||
|
|
||||||
const YT_API_KEY = import.meta.env.VITE_YOUTUBE_API_KEY || ''
|
|
||||||
|
|
||||||
async function fetchVideoMeta(url: string): Promise<OEmbedData | null> {
|
async function fetchVideoMeta(url: string): Promise<OEmbedData | null> {
|
||||||
const info = extractVideoId(url)
|
|
||||||
if (!info) return null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (info.platform === 'youtube') {
|
const res = await fetch(`${API_URL}/api/metadata?url=${encodeURIComponent(url)}`)
|
||||||
// Use YouTube Data API v3 if key is available
|
if (!res.ok) return null
|
||||||
if (YT_API_KEY) {
|
return await res.json()
|
||||||
const apiUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=${info.id}&key=${YT_API_KEY}`
|
|
||||||
const res = await fetch(apiUrl)
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
const video = data.items?.[0]
|
|
||||||
if (video) {
|
|
||||||
const snippet = video.snippet || {}
|
|
||||||
const contentDetails = video.contentDetails || {}
|
|
||||||
const stats = video.statistics || {}
|
|
||||||
// Best thumbnail: maxres > standard > high > medium > default
|
|
||||||
const thumbs = snippet.thumbnails || {}
|
|
||||||
const thumb = thumbs.maxres?.url || thumbs.standard?.url || thumbs.high?.url || thumbs.medium?.url || thumbs.default?.url || ''
|
|
||||||
const duration = parseDuration(contentDetails.duration || '')
|
|
||||||
const viewCount = parseInt(stats.viewCount || '0')
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: snippet.title || '',
|
|
||||||
description: snippet.description?.slice(0, 500) || '',
|
|
||||||
thumbnail: thumb,
|
|
||||||
duration,
|
|
||||||
audioUrl: url,
|
|
||||||
platform: 'YouTube',
|
|
||||||
author: snippet.channelTitle || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to oEmbed (no duration but basic metadata)
|
|
||||||
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${info.id}&format=json`
|
|
||||||
const res = await fetch(oembedUrl)
|
|
||||||
if (!res.ok) return null
|
|
||||||
const data = await res.json()
|
|
||||||
return {
|
|
||||||
title: data.title || '',
|
|
||||||
description: data.author_name ? `Par ${data.author_name}` : '',
|
|
||||||
thumbnail: `https://img.youtube.com/vi/${info.id}/maxresdefault.jpg`,
|
|
||||||
duration: 0,
|
|
||||||
audioUrl: url,
|
|
||||||
platform: 'YouTube',
|
|
||||||
author: data.author_name || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.platform === 'dailymotion') {
|
|
||||||
// Dailymotion public Data API — returns duration, description, tags, author
|
|
||||||
const apiUrl = `https://api.dailymotion.com/video/${info.id}?fields=title,description,duration,thumbnail_720_url,owner.screenname,tags,created_time`
|
|
||||||
const res = await fetch(apiUrl)
|
|
||||||
if (!res.ok) {
|
|
||||||
// Fallback to oEmbed
|
|
||||||
const oembedUrl = `https://www.dailymotion.com/services/oembed?url=https://www.dailymotion.com/video/${info.id}&format=json`
|
|
||||||
const oeRes = await fetch(oembedUrl)
|
|
||||||
if (!oeRes.ok) return null
|
|
||||||
const oeData = await oeRes.json()
|
|
||||||
return {
|
|
||||||
title: oeData.title || '',
|
|
||||||
description: '',
|
|
||||||
thumbnail: oeData.thumbnail_url || '',
|
|
||||||
duration: 0,
|
|
||||||
audioUrl: url,
|
|
||||||
platform: 'Dailymotion',
|
|
||||||
author: oeData.author_name || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
const owner = data.owner?.screenname || ''
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: data.title || '',
|
|
||||||
description: data.description || (owner ? `Par ${owner}` : ''),
|
|
||||||
thumbnail: data.thumbnail_720_url || `https://www.dailymotion.com/thumbnail/video/${info.id}`,
|
|
||||||
duration: data.duration || 0,
|
|
||||||
audioUrl: url,
|
|
||||||
platform: 'Dailymotion',
|
|
||||||
author: owner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.platform === 'soundcloud') {
|
|
||||||
const oembedUrl = `https://soundcloud.com/oembed?url=${encodeURIComponent(url)}&format=json`
|
|
||||||
const res = await fetch(oembedUrl)
|
|
||||||
if (!res.ok) return null
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
// Extract duration from the iframe embed (SoundCloud embeds sometimes include it)
|
|
||||||
// Parse the HTML to get the widget URL for potential API calls
|
|
||||||
const author = data.author_name || ''
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: data.title || '',
|
|
||||||
description: data.description || (author ? `Par ${author}` : ''),
|
|
||||||
thumbnail: data.thumbnail_url || '',
|
|
||||||
duration: 0,
|
|
||||||
audioUrl: url,
|
|
||||||
platform: 'SoundCloud',
|
|
||||||
author,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Upload() {
|
export function Upload() {
|
||||||
@ -446,6 +314,7 @@ export function Upload() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-1.5 mt-4">
|
<div className="flex gap-1.5 mt-4">
|
||||||
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">YouTube</span>
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">YouTube</span>
|
||||||
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">Spotify</span>
|
||||||
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">Dailymotion</span>
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">Dailymotion</span>
|
||||||
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">SoundCloud</span>
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">SoundCloud</span>
|
||||||
</div>
|
</div>
|
||||||
@ -602,7 +471,7 @@ export function Upload() {
|
|||||||
label="URL de la video ou du podcast"
|
label="URL de la video ou du podcast"
|
||||||
value={externalUrl}
|
value={externalUrl}
|
||||||
onChange={(e) => setExternalUrl(e.target.value)}
|
onChange={(e) => setExternalUrl(e.target.value)}
|
||||||
placeholder="https://youtube.com/watch?v=... ou https://dai.ly/..."
|
placeholder="https://youtube.com/watch?v=... ou https://open.spotify.com/episode/..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
|
|||||||
@ -250,6 +250,12 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Spotify: embed is rendered in PlayerBar, no hidden iframe needed ──
|
||||||
|
if (external && embed?.platform === 'spotify') {
|
||||||
|
set({ audio: null, current: podcast, isPlaying: true, isExternal: true, progress: 0, duration: podcast.duration_seconds || 0 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ── Other external (Dailymotion, SoundCloud): hidden iframe ──
|
// ── Other external (Dailymotion, SoundCloud): hidden iframe ──
|
||||||
if (external && embed) {
|
if (external && embed) {
|
||||||
createHiddenIframe(embed.embedUrl)
|
createHiddenIframe(embed.embedUrl)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user