Files
motif/server/src/adapters/BitMidiAdapter.ts
T

182 lines
5.9 KiB
TypeScript

import type { SearchAdapter, MIDICandidate } from '../types.js';
import { ScoreUtils } from '../utils/ScoreUtils.js';
export class BitMidiAdapter implements SearchAdapter {
name = 'bitmidi';
private baseUrl = 'https://bitmidi.com';
async search(query: string): Promise<MIDICandidate[]> {
try {
const searchUrl = `${this.baseUrl}/search?q=${encodeURIComponent(query)}`;
const response = await fetch(searchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; MotifBot/1.0)'
}
});
if (!response.ok) {
throw new Error(`BitMidi search failed: ${response.status}`);
}
const html = await response.text();
return this.parseSearchResults(html, query);
} catch (error) {
console.error('BitMidi search error:', error);
return [];
}
}
private parseSearchResults(html: string, query: string): MIDICandidate[] {
const candidates: MIDICandidate[] = [];
try {
// Look for different possible variable names containing search data
let initStoreStart = html.indexOf('window.initStore = ');
if (initStoreStart === -1) {
initStoreStart = html.indexOf('window.__INITIAL_STATE__ = ');
if (initStoreStart === -1) {
initStoreStart = html.indexOf('window.INITIAL_PROPS = ');
if (initStoreStart === -1) {
// Look for any assignment that contains the data structure we need
// From the context, we know the pattern is: {"data":{"midis":{...
const dataStartPattern = '"data":{"midis":';
const dataPatternIndex = html.indexOf(dataStartPattern);
if (dataPatternIndex !== -1) {
// Go backwards to find the start of the containing object
let bracketStart = dataPatternIndex;
let bracketDepth = 0;
while (bracketStart > 0) {
bracketStart--;
if (html[bracketStart] === '}') bracketDepth++;
if (html[bracketStart] === '{') {
if (bracketDepth === 0) break;
bracketDepth--;
}
}
initStoreStart = bracketStart;
} else {
return [];
}
}
}
}
let jsonStart = initStoreStart;
// If initStoreStart is not already at a brace, find the next one
if (html[initStoreStart] !== '{') {
jsonStart = html.indexOf('{', initStoreStart);
if (jsonStart === -1) {
console.log('BitMidi: Could not find JSON start');
return [];
}
}
// Find the matching closing brace
let braceCount = 0;
let jsonEnd = -1;
for (let i = jsonStart; i < html.length; i++) {
if (html[i] === '{') braceCount++;
else if (html[i] === '}') {
braceCount--;
if (braceCount === 0) {
jsonEnd = i + 1;
break;
}
}
}
if (jsonEnd === -1) {
console.log('BitMidi: Could not find JSON end');
return [];
}
const jsonString = html.slice(jsonStart, jsonEnd);
const jsonData = JSON.parse(jsonString);
// Extract search results from the JSON structure
if (jsonData.data && jsonData.data.midis) {
const midisData = jsonData.data.midis;
const midiKeys = Object.keys(midisData);
for (let i = 0; i < Math.min(midiKeys.length, 10); i++) {
const midiKey = midiKeys[i];
const midiData = midisData[midiKey];
if (midiData && midiData.name) {
const title = midiData.name;
const slug = midiData.slug;
const downloadUrl = midiData.downloadUrl || `/uploads/${midiData.id}.mid`;
const pageUrl = `${this.baseUrl}/${slug}`;
const fullDownloadUrl = downloadUrl.startsWith('http')
? downloadUrl
: `${this.baseUrl}${downloadUrl}`;
const confidence = ScoreUtils.calculateConfidence(title, query, this.name);
candidates.push({
id: `bitmidi_${midiData.id}`,
title: title.trim(),
source: 'bitmidi',
pageUrl: pageUrl,
midiUrl: fullDownloadUrl,
confidence
});
}
}
}
} catch (error) {
console.error('BitMidi: Error parsing JSON data:', error);
return [];
}
return candidates
.filter(c => c.confidence > 0.3) // Filter low confidence matches
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 5); // Top 5 results
}
async getMidiDownloadUrl(pageUrl: string): Promise<string> {
try {
const response = await fetch(pageUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; MotifBot/1.0)'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch MIDI page: ${response.status}`);
}
const html = await response.text();
// Extract the actual download URL from the page
// Pattern: href="/uploads/12345.mid" or similar
const downloadMatch = html.match(/href="(\/uploads\/[^"]*\.mid)"/);
if (downloadMatch) {
return `${this.baseUrl}${downloadMatch[1]}`;
}
// Fallback: look for any .mid download link
const midiMatch = html.match(/href="([^"]*\.mid)"/);
if (midiMatch) {
const url = midiMatch[1];
return url.startsWith('http') ? url : `${this.baseUrl}${url}`;
}
throw new Error('Could not find MIDI download URL');
} catch (error) {
console.error('Error extracting MIDI URL:', error);
return pageUrl; // Fallback to page URL
}
}
private extractMidiUrl(pageUrl: string): string {
// This method is deprecated in favor of getMidiDownloadUrl
// but keeping for backward compatibility
return pageUrl;
}
}