mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: autosearch for musics
This commit is contained in:
parent
9426b73940
commit
6d8fe814b0
12 changed files with 380 additions and 12 deletions
|
|
@ -42,4 +42,11 @@ module.exports = {
|
|||
process.env.POST_LOGOUT_REDIRECT_URI ||
|
||||
(process.env.OIDC_REDIRECT_URI ? new URL('/', process.env.OIDC_REDIRECT_URI).href : undefined),
|
||||
adminGroupName: process.env.ADMIN_GROUP_NAME || 'octane-admins',
|
||||
// All optional: the "add a song" autocomplete works with none of these set
|
||||
// (title/artist suggestions come from Apple's free, key-less iTunes Search
|
||||
// API). Without Spotify/YouTube credentials, the matching links are just
|
||||
// left blank for manual entry instead of being auto-filled.
|
||||
spotifyClientId: process.env.SPOTIFY_CLIENT_ID || null,
|
||||
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET || null,
|
||||
youtubeApiKey: process.env.YOUTUBE_API_KEY || null,
|
||||
};
|
||||
|
|
|
|||
93
src/lib/musicSearch.js
Normal file
93
src/lib/musicSearch.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
const config = require('../config');
|
||||
|
||||
let spotifyToken = null; // { accessToken, expiresAt }
|
||||
|
||||
async function searchCandidates(query) {
|
||||
const url = new URL('https://itunes.apple.com/search');
|
||||
url.searchParams.set('term', query);
|
||||
url.searchParams.set('entity', 'song');
|
||||
url.searchParams.set('limit', '8');
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`iTunes search failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
return (data.results || []).map((r) => ({
|
||||
title: r.trackName,
|
||||
artist: r.artistName,
|
||||
artworkUrl: r.artworkUrl100 || r.artworkUrl60 || null,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getSpotifyToken() {
|
||||
if (spotifyToken && spotifyToken.expiresAt > Date.now()) {
|
||||
return spotifyToken.accessToken;
|
||||
}
|
||||
const basic = Buffer.from(`${config.spotifyClientId}:${config.spotifyClientSecret}`).toString('base64');
|
||||
const res = await fetch('https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Spotify token request failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
spotifyToken = {
|
||||
accessToken: data.access_token,
|
||||
expiresAt: Date.now() + (data.expires_in - 60) * 1000,
|
||||
};
|
||||
return spotifyToken.accessToken;
|
||||
}
|
||||
|
||||
async function findSpotifyUrl(title, artist) {
|
||||
if (!config.spotifyClientId || !config.spotifyClientSecret) return null;
|
||||
try {
|
||||
const token = await getSpotifyToken();
|
||||
const url = new URL('https://api.spotify.com/v1/search');
|
||||
url.searchParams.set('q', `track:${title} artist:${artist}`);
|
||||
url.searchParams.set('type', 'track');
|
||||
url.searchParams.set('limit', '1');
|
||||
|
||||
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
||||
if (!res.ok) throw new Error(`Spotify search failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const track = data.tracks?.items?.[0];
|
||||
return track?.external_urls?.spotify || null;
|
||||
} catch (err) {
|
||||
console.warn('[musicSearch] Spotify lookup failed:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function findYoutubeUrl(title, artist) {
|
||||
if (!config.youtubeApiKey) return null;
|
||||
try {
|
||||
const url = new URL('https://www.googleapis.com/youtube/v3/search');
|
||||
url.searchParams.set('part', 'snippet');
|
||||
url.searchParams.set('q', `${title} ${artist}`);
|
||||
url.searchParams.set('type', 'video');
|
||||
url.searchParams.set('maxResults', '1');
|
||||
url.searchParams.set('key', config.youtubeApiKey);
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`YouTube search failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const videoId = data.items?.[0]?.id?.videoId;
|
||||
return videoId ? `https://www.youtube.com/watch?v=${videoId}` : null;
|
||||
} catch (err) {
|
||||
console.warn('[musicSearch] YouTube lookup failed:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function findLinks(title, artist) {
|
||||
const [spotifyUrl, youtubeUrl] = await Promise.all([
|
||||
findSpotifyUrl(title, artist),
|
||||
findYoutubeUrl(title, artist),
|
||||
]);
|
||||
return { spotifyUrl, youtubeUrl };
|
||||
}
|
||||
|
||||
module.exports = { searchCandidates, findLinks };
|
||||
|
|
@ -4,6 +4,7 @@ const instrumentsRoutes = require('./instruments');
|
|||
const songsRoutes = require('./songs');
|
||||
const suggestionsRoutes = require('./suggestions');
|
||||
const setlistsRoutes = require('./setlists');
|
||||
const musicSearchRoutes = require('./musicSearch');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -12,5 +13,6 @@ router.use('/instruments', instrumentsRoutes);
|
|||
router.use('/songs', songsRoutes);
|
||||
router.use('/suggestions', suggestionsRoutes);
|
||||
router.use('/setlists', setlistsRoutes);
|
||||
router.use('/music-search', musicSearchRoutes);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
33
src/routes/musicSearch.js
Normal file
33
src/routes/musicSearch.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const express = require('express');
|
||||
const musicSearch = require('../lib/musicSearch');
|
||||
const asyncHandler = require('../lib/asyncHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const q = (req.query.q || '').trim();
|
||||
if (q.length < 2) return res.json([]);
|
||||
try {
|
||||
const candidates = await musicSearch.searchCandidates(q);
|
||||
res.json(candidates);
|
||||
} catch (err) {
|
||||
console.warn('[musicSearch] candidate search failed:', err.message);
|
||||
res.json([]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/links',
|
||||
asyncHandler(async (req, res) => {
|
||||
const title = (req.query.title || '').trim();
|
||||
const artist = (req.query.artist || '').trim();
|
||||
if (!title || !artist) return res.status(400).json({ error: 'title_and_artist_required' });
|
||||
const links = await musicSearch.findLinks(title, artist);
|
||||
res.json(links);
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Loading…
Add table
Add a link
Reference in a new issue