fix: use Node.js http module for reliable ICY parsing
This commit is contained in:
@@ -2,6 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { Interval } from '@nestjs/schedule';
|
import { Interval } from '@nestjs/schedule';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { NowPlayingGateway } from './now-playing.gateway';
|
import { NowPlayingGateway } from './now-playing.gateway';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class IcyNowPlayingService {
|
export class IcyNowPlayingService {
|
||||||
@@ -74,52 +76,98 @@ export class IcyNowPlayingService {
|
|||||||
private async parseIcyMetadata(
|
private async parseIcyMetadata(
|
||||||
url: string,
|
url: string,
|
||||||
): Promise<{ artist: string; song: string } | null> {
|
): Promise<{ artist: string; song: string } | null> {
|
||||||
const response = await fetch(url, {
|
return new Promise((resolve, reject) => {
|
||||||
headers: { 'Icy-MetaData': '1' },
|
const client = url.startsWith('https') ? https : http;
|
||||||
signal: AbortSignal.timeout(5000),
|
const req = client.get(
|
||||||
});
|
url,
|
||||||
|
{ headers: { 'Icy-MetaData': '1' }, timeout: 5000 },
|
||||||
const metaintHeader = response.headers.get('icy-metaint');
|
(res) => {
|
||||||
if (!metaintHeader) return null;
|
const metaint = parseInt(
|
||||||
const metaint = parseInt(metaintHeader, 10);
|
(res.headers['icy-metaint'] as string) || '0',
|
||||||
if (!metaint || isNaN(metaint)) return null;
|
);
|
||||||
|
if (!metaint) {
|
||||||
const body = response.body;
|
req.destroy();
|
||||||
if (!body) return null;
|
resolve(null);
|
||||||
const reader = body.getReader();
|
return;
|
||||||
|
|
||||||
// Skip audio bytes up to metaint
|
|
||||||
let skipped = 0;
|
|
||||||
while (skipped < metaint) {
|
|
||||||
const chunk = await reader.read();
|
|
||||||
if (chunk.done) return null;
|
|
||||||
skipped += chunk.value.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read metadata length byte
|
let audioBytesRead = 0;
|
||||||
const lenChunk = await reader.read();
|
let metaLength = 0;
|
||||||
if (lenChunk.done || !lenChunk.value.length) return null;
|
let metaBytesRead = 0;
|
||||||
const metaLength = lenChunk.value[0] * 16;
|
|
||||||
if (metaLength === 0) return null;
|
|
||||||
|
|
||||||
// Read metadata bytes
|
|
||||||
let metaBuffer = Buffer.alloc(0);
|
let metaBuffer = Buffer.alloc(0);
|
||||||
while (metaBuffer.length < metaLength) {
|
let state: 'audio' | 'meta-length' | 'meta' = 'audio';
|
||||||
const chunk = await reader.read();
|
|
||||||
if (chunk.done) break;
|
res.on('data', (chunk: Buffer) => {
|
||||||
metaBuffer = Buffer.concat([metaBuffer, Buffer.from(chunk.value)]);
|
let offset = 0;
|
||||||
|
while (offset < chunk.length) {
|
||||||
|
if (state === 'audio') {
|
||||||
|
const need = metaint - audioBytesRead;
|
||||||
|
const available = chunk.length - offset;
|
||||||
|
const take = Math.min(need, available);
|
||||||
|
audioBytesRead += take;
|
||||||
|
offset += take;
|
||||||
|
if (audioBytesRead >= metaint) {
|
||||||
|
state = 'meta-length';
|
||||||
}
|
}
|
||||||
|
} else if (state === 'meta-length') {
|
||||||
reader.cancel();
|
metaLength = chunk[offset] * 16;
|
||||||
|
offset++;
|
||||||
const metaStr = metaBuffer.toString('utf-8').replace(/\x00/g, '');
|
if (metaLength === 0) {
|
||||||
|
audioBytesRead = 0;
|
||||||
|
state = 'audio';
|
||||||
|
} else {
|
||||||
|
metaBuffer = Buffer.alloc(0);
|
||||||
|
metaBytesRead = 0;
|
||||||
|
state = 'meta';
|
||||||
|
}
|
||||||
|
} else if (state === 'meta') {
|
||||||
|
const need = metaLength - metaBytesRead;
|
||||||
|
const available = chunk.length - offset;
|
||||||
|
const take = Math.min(need, available);
|
||||||
|
metaBuffer = Buffer.concat([
|
||||||
|
metaBuffer,
|
||||||
|
chunk.slice(offset, offset + take),
|
||||||
|
]);
|
||||||
|
metaBytesRead += take;
|
||||||
|
offset += take;
|
||||||
|
if (metaBytesRead >= metaLength) {
|
||||||
|
const metaStr = metaBuffer
|
||||||
|
.toString('utf-8')
|
||||||
|
.replace(/\x00/g, '');
|
||||||
const match = metaStr.match(/StreamTitle='([^']+)'/);
|
const match = metaStr.match(/StreamTitle='([^']+)'/);
|
||||||
if (!match) return null;
|
req.destroy();
|
||||||
|
if (!match) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parts = match[1].split(' - ', 2);
|
const parts = match[1].split(' - ', 2);
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
return { artist: match[1], song: match[1] };
|
resolve({ artist: match[1], song: match[1] });
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
artist: parts[0].trim(),
|
||||||
|
song: parts[1].trim(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { artist: parts[0].trim(), song: parts[1].trim() };
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('error', (err) => {
|
||||||
|
req.destroy();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
res.on('end', () => resolve(null));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', (err) => reject(err));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Timeout'));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user