Initial commit: StreamFlow IPTV platform

This commit is contained in:
aiulian25 2025-12-17 00:42:43 +00:00
commit 73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions

218
backend/routes/metadata.js Normal file
View file

@ -0,0 +1,218 @@
const express = require('express');
const router = express.Router();
const axios = require('axios');
const { authenticate } = require('../middleware/auth');
const { readLimiter } = require('../middleware/rateLimiter');
const logger = require('../utils/logger');
const { getRadioStationMetadata } = require('../utils/radioMetadata');
/**
* Fetch metadata from radio stream (ICY/Shoutcast metadata)
* This attempts to extract "now playing" information from radio streams
*/
router.get('/radio/:channelId', readLimiter, authenticate, async (req, res) => {
const { channelId } = req.params;
const { db } = require('../database/db');
try {
// Validate channel ID
const id = parseInt(channelId, 10);
if (isNaN(id) || id < 1) {
return res.status(400).json({ error: 'Invalid channel ID' });
}
// Get channel URL from database
db.get(
'SELECT url, name FROM channels WHERE id = ? AND is_radio = 1',
[id],
async (err, channel) => {
if (err || !channel) {
return res.status(404).json({ error: 'Channel not found' });
}
try {
// Request stream with ICY metadata headers
const response = await axios.get(channel.url, {
headers: {
'Icy-MetaData': '1',
'User-Agent': 'StreamFlow/1.0'
},
responseType: 'stream',
timeout: 5000,
maxRedirects: 5
});
let metadata = {
channelId: parseInt(channelId),
channelName: channel.name,
title: null,
artist: null,
song: null,
streamTitle: null,
bitrate: null,
genre: null,
url: null
};
// Extract ICY headers
const icyName = response.headers['icy-name'];
const icyGenre = response.headers['icy-genre'];
const icyBr = response.headers['icy-br'];
const icyUrl = response.headers['icy-url'];
const icyDescription = response.headers['icy-description'];
if (icyName) {
// Filter out stream quality info (e.g., "europafm_aacp_48k")
const cleanName = icyName.replace(/_aacp?_\d+k?/gi, '').replace(/_mp3_\d+k?/gi, '').replace(/_\d+k/gi, '');
if (cleanName && !cleanName.match(/^\w+_\w+$/)) {
metadata.streamTitle = cleanName;
} else {
metadata.streamTitle = icyName;
}
}
if (icyGenre) metadata.genre = icyGenre;
if (icyBr) metadata.bitrate = icyBr + ' kbps';
if (icyUrl) metadata.url = icyUrl;
// Try to get current track from ICY-MetaInt
const metaInt = parseInt(response.headers['icy-metaint']);
if (metaInt && metaInt > 0) {
// Read metadata from stream
const chunks = [];
let bytesRead = 0;
let metadataFound = false;
response.data.on('data', (chunk) => {
if (metadataFound) return;
chunks.push(chunk);
bytesRead += chunk.length;
// Once we have enough data, parse metadata
if (bytesRead >= metaInt + 255) {
const buffer = Buffer.concat(chunks);
const metadataLength = buffer[metaInt] * 16;
if (metadataLength > 0) {
const metadataBuffer = buffer.slice(metaInt + 1, metaInt + 1 + metadataLength);
const metadataString = metadataBuffer.toString('utf8').replace(/\0/g, '');
logger.info(`[Metadata] Raw metadata string for ${channel.name}: ${metadataString}`);
// Parse StreamTitle='Artist - Song'
const titleMatch = metadataString.match(/StreamTitle='([^']*)'/);
if (titleMatch && titleMatch[1]) {
const rawTitle = titleMatch[1];
logger.info(`[Metadata] Raw title: ${rawTitle}`);
// Skip if it's just stream quality info
if (!rawTitle.match(/^\w+_aacp?_\d+k?$/i) && !rawTitle.match(/^\w+_mp3_\d+k?$/i)) {
metadata.song = rawTitle;
// Try to split into artist and title (various separators)
let parts = rawTitle.split(' - ');
if (parts.length === 1) {
parts = rawTitle.split(' '); // em dash
}
if (parts.length === 1) {
parts = rawTitle.split(' | ');
}
if (parts.length >= 2) {
metadata.artist = parts[0].trim();
metadata.title = parts.slice(1).join(' - ').trim();
} else if (parts.length === 1 && rawTitle.length > 0) {
// If no separator, use the whole thing as title
metadata.title = rawTitle.trim();
}
}
}
}
metadataFound = true;
response.data.destroy();
}
});
// Wait a bit for metadata
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Close the stream
if (response.data && !response.data.destroyed) {
response.data.destroy();
}
// If no metadata found from ICY, try external APIs
if (!metadata.title && !metadata.artist && !metadata.song) {
logger.info(`[Metadata] No ICY metadata found, trying external sources for ${channel.name}`);
try {
const externalMetadata = await getRadioStationMetadata(channel.name, channel.url);
if (externalMetadata) {
metadata.title = externalMetadata.title;
metadata.artist = externalMetadata.artist;
if (externalMetadata.title && externalMetadata.artist) {
metadata.song = `${externalMetadata.artist} - ${externalMetadata.title}`;
} else if (externalMetadata.title) {
metadata.song = externalMetadata.title;
}
logger.info(`[Metadata] External metadata found: ${metadata.song}`);
}
} catch (externalError) {
logger.error(`[Metadata] External metadata fetch failed: ${externalError.message}`);
}
}
res.json(metadata);
} catch (streamError) {
logger.error('Error fetching stream metadata:', streamError.message);
// Try external metadata as fallback
try {
const externalMetadata = await getRadioStationMetadata(channel.name, channel.url);
if (externalMetadata) {
return res.json({
channelId: parseInt(channelId),
channelName: channel.name,
title: externalMetadata.title,
artist: externalMetadata.artist,
song: externalMetadata.artist && externalMetadata.title
? `${externalMetadata.artist} - ${externalMetadata.title}`
: externalMetadata.title,
streamTitle: channel.name,
bitrate: null,
genre: null,
url: null,
source: externalMetadata.source
});
}
} catch (externalError) {
logger.error(`[Metadata] External metadata fallback failed: ${externalError.message}`);
}
// Return basic info if everything fails
res.json({
channelId: parseInt(channelId),
channelName: channel.name,
title: null,
artist: null,
song: null,
streamTitle: channel.name,
bitrate: null,
genre: null,
url: null,
error: 'Unable to fetch stream metadata'
});
}
}
);
} catch (error) {
logger.error('Metadata fetch error:', error);
res.status(500).json({ error: 'Failed to fetch metadata' });
}
});
module.exports = router;