Add all remaining Subsonic API endpoints for full Amperfy compatibility

This commit is contained in:
2025-12-07 23:41:42 +00:00
parent 75e4675110
commit 4c5db557b2

View File

@@ -726,27 +726,211 @@ router.get('/getSong.view', authenticateSubsonic, async (req, res) => {
// Search // Search
router.get('/search3.view', authenticateSubsonic, async (req, res) => { router.get('/search3.view', authenticateSubsonic, async (req, res) => {
try { try {
const { query } = req.query; const { query, artistCount, albumCount, songCount } = req.query;
const searchTerm = `%${query}%`; const searchTerm = `%${query}%`;
const maxArtists = parseInt(artistCount) || 20;
const maxAlbums = parseInt(albumCount) || 20;
const maxSongs = parseInt(songCount) || 20;
// Search artists
const artists = await all(`
SELECT DISTINCT artist,
(SELECT artwork_path FROM tracks t2 WHERE t2.artist = tracks.artist AND artwork_path IS NOT NULL LIMIT 1) as artwork_path
FROM tracks
WHERE artist LIKE ?
LIMIT ?
`, [searchTerm, maxArtists]);
// Search albums
const albums = await all(`
SELECT DISTINCT album, album_artist, year,
(SELECT artwork_path FROM tracks t2 WHERE t2.album = tracks.album LIMIT 1) as artwork_path
FROM tracks
WHERE album LIKE ? OR album_artist LIKE ?
LIMIT ?
`, [searchTerm, searchTerm, maxAlbums]);
// Search songs
const tracks = await all(` const tracks = await all(`
SELECT * FROM tracks SELECT * FROM tracks
WHERE title LIKE ? OR artist LIKE ? OR album LIKE ? WHERE title LIKE ? OR artist LIKE ? OR album LIKE ?
LIMIT 20 LIMIT ?
`, [searchTerm, searchTerm, searchTerm]); `, [searchTerm, searchTerm, searchTerm, maxSongs]);
let searchXml = '<searchResult3>\n'; let searchXml = '<searchResult3>\n';
// Add artists
artists.forEach(artist => {
const artistId = crypto.createHash('md5').update(`artist-${artist.artist}`).digest('hex');
const coverArt = artist.artwork_path ? artistId : '';
searchXml += ` <artist id="${artistId}" name="${escapeXml(artist.artist)}" `;
if (coverArt) searchXml += `coverArt="${coverArt}" `;
searchXml += `/>\n`;
});
// Add albums
albums.forEach(album => {
const albumId = crypto.createHash('md5').update(`album-${album.album}-${album.album_artist}`).digest('hex');
const artistId = crypto.createHash('md5').update(`artist-${album.album_artist}`).digest('hex');
const coverArt = album.artwork_path ? albumId : '';
searchXml += ` <album id="${albumId}" name="${escapeXml(album.album)}" `;
searchXml += `artist="${escapeXml(album.album_artist)}" artistId="${artistId}" `;
if (coverArt) searchXml += `coverArt="${coverArt}" `;
searchXml += `year="${album.year || ''}" />\n`;
});
// Add songs
tracks.forEach(track => { tracks.forEach(track => {
searchXml += buildSongXml(track, req); searchXml += buildSongXml(track, req);
}); });
searchXml += '</searchResult3>'; searchXml += '</searchResult3>';
sendSubsonicResponse(res, searchXml); sendSubsonicResponse(res, searchXml);
} catch (error) { } catch (error) {
console.error('Error in search3:', error);
sendSubsonicError(res, 0, error.message); sendSubsonicError(res, 0, error.message);
} }
}); });
// Scrobble (mark as played)
router.get('/scrobble.view', authenticateSubsonic, async (req, res) => {
// Just acknowledge - we're not tracking play counts yet
sendSubsonicResponse(res, '');
});
// Star/unstar items
router.get('/star.view', authenticateSubsonic, async (req, res) => {
// Acknowledge - favorites not implemented yet
sendSubsonicResponse(res, '');
});
router.get('/unstar.view', authenticateSubsonic, async (req, res) => {
// Acknowledge - favorites not implemented yet
sendSubsonicResponse(res, '');
});
// Get starred items
router.get('/getStarred.view', authenticateSubsonic, async (req, res) => {
sendSubsonicResponse(res, '<starred/>');
});
router.get('/getStarred2.view', authenticateSubsonic, async (req, res) => {
sendSubsonicResponse(res, '<starred2/>');
});
// Get user
router.get('/getUser.view', authenticateSubsonic, async (req, res) => {
const { username } = req.query;
sendSubsonicResponse(res, `
<user username="${username}" email="user@example.com"
scrobblingEnabled="false" adminRole="true"
settingsRole="true" downloadRole="true"
uploadRole="true" playlistRole="true"
coverArtRole="true" commentRole="true"
podcastRole="true" streamRole="true"
jukeboxRole="false" shareRole="true"/>
`);
});
// Get now playing
router.get('/getNowPlaying.view', authenticateSubsonic, async (req, res) => {
sendSubsonicResponse(res, '<nowPlaying/>');
});
// Get similar songs
router.get('/getSimilarSongs.view', authenticateSubsonic, async (req, res) => {
try {
const { id, count } = req.query;
const limit = parseInt(count) || 50;
// Get the track to find similar songs
const track = await get('SELECT * FROM tracks WHERE id = ?', [id]);
if (!track) {
return sendSubsonicResponse(res, '<similarSongs/>');
}
// Find similar songs by same artist or genre
const similar = await all(`
SELECT * FROM tracks
WHERE (artist = ? OR genre = ?) AND id != ?
ORDER BY RANDOM()
LIMIT ?
`, [track.artist, track.genre, id, limit]);
let similarXml = '<similarSongs>\n';
similar.forEach(t => {
similarXml += buildSongXml(t, req);
});
similarXml += '</similarSongs>';
sendSubsonicResponse(res, similarXml);
} catch (error) {
console.error('Error in getSimilarSongs:', error);
sendSubsonicResponse(res, '<similarSongs/>');
}
});
router.get('/getSimilarSongs2.view', authenticateSubsonic, async (req, res) => {
try {
const { id, count } = req.query;
const limit = parseInt(count) || 50;
// Get the track to find similar songs
const track = await get('SELECT * FROM tracks WHERE id = ?', [id]);
if (!track) {
return sendSubsonicResponse(res, '<similarSongs2/>');
}
// Find similar songs by same artist or genre
const similar = await all(`
SELECT * FROM tracks
WHERE (artist = ? OR genre = ?) AND id != ?
ORDER BY RANDOM()
LIMIT ?
`, [track.artist, track.genre, id, limit]);
let similarXml = '<similarSongs2>\n';
similar.forEach(t => {
similarXml += buildSongXml(t, req);
});
similarXml += '</similarSongs2>';
sendSubsonicResponse(res, similarXml);
} catch (error) {
console.error('Error in getSimilarSongs2:', error);
sendSubsonicResponse(res, '<similarSongs2/>');
}
});
// Get top songs
router.get('/getTopSongs.view', authenticateSubsonic, async (req, res) => {
try {
const { artist, count } = req.query;
const limit = parseInt(count) || 50;
const tracks = await all(`
SELECT * FROM tracks
WHERE artist = ?
ORDER BY RANDOM()
LIMIT ?
`, [artist, limit]);
let topSongsXml = '<topSongs>\n';
tracks.forEach(track => {
topSongsXml += buildSongXml(track, req);
});
topSongsXml += '</topSongs>';
sendSubsonicResponse(res, topSongsXml);
} catch (error) {
console.error('Error in getTopSongs:', error);
sendSubsonicResponse(res, '<topSongs/>');
}
});
// Helper to build song XML // Helper to build song XML
function buildSongXml(track, req) { function buildSongXml(track, req) {
const albumId = crypto.createHash('md5').update(`album-${track.album}-${track.album_artist}`).digest('hex'); const albumId = crypto.createHash('md5').update(`album-${track.album}-${track.album_artist}`).digest('hex');