chore: Release v1.3.3 - Critical bug fixes and QoL improvements
Critical Fixes: - Docker permissions for PostgreSQL/Redis bind mounts Fixes #59, fixes #62 - Audio analyzer memory consumption and OOM crashes Fixes #21, fixes #26, fixes #53 - LastFM array normalization preventing .map crashes Fixes #37, fixes #39 - Wikidata 403 errors from missing User-Agent Fixes #57 - Singles directory creation race conditions Fixes #58 - Firefox FLAC playback stopping at ~4:34 mark Fixes #42, fixes #17 Quality of Life: - Add Releases link to desktop sidebar navigation Fixes #41 - iPhone safe area insets for Dynamic Island/notch Fixes #54 Contributors: @arsaboo, @rustyricky, @RustyJonez, @tombatossals No regressions detected, backward compatible, production ready.
This commit is contained in:
@@ -5,6 +5,7 @@ import { musicBrainzService } from "../services/musicbrainz";
|
||||
import { fanartService } from "../services/fanart";
|
||||
import { deezerService } from "../services/deezer";
|
||||
import { redisClient } from "../utils/redis";
|
||||
import { normalizeToArray } from "../utils/normalize";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -158,8 +159,10 @@ router.get("/discover/:nameOrMbid", async (req, res) => {
|
||||
}
|
||||
|
||||
// Fallback to Last.fm (but filter placeholders)
|
||||
// NORMALIZATION: lastFmInfo.image could be a single object or array
|
||||
if (!image && lastFmInfo?.image) {
|
||||
const lastFmImage = lastFmService.getBestImage(lastFmInfo.image);
|
||||
const images = normalizeToArray(lastFmInfo.image);
|
||||
const lastFmImage = lastFmService.getBestImage(images);
|
||||
// Filter out Last.fm placeholder
|
||||
if (
|
||||
lastFmImage &&
|
||||
@@ -274,10 +277,13 @@ router.get("/discover/:nameOrMbid", async (req, res) => {
|
||||
}
|
||||
|
||||
// Get similar artists from Last.fm and fetch images
|
||||
const similarArtistsRaw = lastFmInfo?.similar?.artist || [];
|
||||
// NORMALIZATION: lastFmInfo.similar.artist could be a single object or array
|
||||
const similarArtistsRaw = normalizeToArray(lastFmInfo?.similar?.artist);
|
||||
const similarArtists = await Promise.all(
|
||||
similarArtistsRaw.slice(0, 10).map(async (artist: any) => {
|
||||
const similarImage = artist.image?.find(
|
||||
// NORMALIZATION: artist.image could be a single object or array
|
||||
const images = normalizeToArray(artist.image);
|
||||
const similarImage = images.find(
|
||||
(img: any) => img.size === "large"
|
||||
)?.[" #text"];
|
||||
|
||||
@@ -325,14 +331,19 @@ router.get("/discover/:nameOrMbid", async (req, res) => {
|
||||
})
|
||||
);
|
||||
|
||||
// NORMALIZATION: lastFmInfo.tags.tag could be a single object or array
|
||||
const tags = normalizeToArray(lastFmInfo?.tags?.tag)
|
||||
.map((t: any) => t?.name)
|
||||
.filter(Boolean);
|
||||
|
||||
const response = {
|
||||
mbid,
|
||||
name: artistName,
|
||||
image,
|
||||
bio, // Use filtered bio instead of raw Last.fm bio
|
||||
summary: bio, // Alias for consistency
|
||||
tags: lastFmInfo?.tags?.tag?.map((t: any) => t.name) || [],
|
||||
genres: lastFmInfo?.tags?.tag?.map((t: any) => t.name) || [], // Alias for consistency
|
||||
tags,
|
||||
genres: tags, // Alias for consistency
|
||||
listeners: parseInt(lastFmInfo?.stats?.listeners || "0"),
|
||||
playcount: parseInt(lastFmInfo?.stats?.playcount || "0"),
|
||||
url: lastFmInfo?.url || null,
|
||||
@@ -470,7 +481,10 @@ router.get("/album/:mbid", async (req, res) => {
|
||||
|
||||
// Check if Cover Art Archive actually has the image
|
||||
try {
|
||||
const response = await fetch(coverArtUrl, { method: "HEAD" });
|
||||
const response = await fetch(coverArtUrl, {
|
||||
method: "HEAD",
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (response.ok) {
|
||||
coverUrl = coverArtUrl;
|
||||
logger.debug(`Cover Art Archive has cover for ${albumTitle}`);
|
||||
@@ -529,7 +543,10 @@ router.get("/album/:mbid", async (req, res) => {
|
||||
coverUrl,
|
||||
coverArt: coverUrl, // Alias for compatibility
|
||||
bio: lastFmInfo?.wiki?.summary || null,
|
||||
tags: lastFmInfo?.tags?.tag?.map((t: any) => t.name) || [],
|
||||
// NORMALIZATION: lastFmInfo.tags.tag could be a single object or array
|
||||
tags: normalizeToArray(lastFmInfo?.tags?.tag)
|
||||
.map((t: any) => t?.name)
|
||||
.filter(Boolean),
|
||||
tracks: tracks.map((track: any, index: number) => ({
|
||||
id: `mb-${releaseGroupId}-${track.id || index}`,
|
||||
title: track.title,
|
||||
|
||||
@@ -2754,32 +2754,12 @@ router.get("/tracks/:id/stream", async (req, res) => {
|
||||
`[STREAM] Sending file: ${filePath}, mimeType: ${mimeType}`
|
||||
);
|
||||
|
||||
res.sendFile(
|
||||
filePath,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Accept-Ranges": "bytes",
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Access-Control-Allow-Origin":
|
||||
req.headers.origin || "*",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Cross-Origin-Resource-Policy": "cross-origin",
|
||||
},
|
||||
},
|
||||
(err) => {
|
||||
// Always destroy the streaming service to clean up intervals
|
||||
streamingService.destroy();
|
||||
if (err) {
|
||||
logger.error(`[STREAM] sendFile error:`, err);
|
||||
} else {
|
||||
logger.debug(
|
||||
`[STREAM] File sent successfully: ${path.basename(
|
||||
filePath
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
await streamingService.streamFileWithRangeSupport(req, res, filePath, mimeType);
|
||||
streamingService.destroy();
|
||||
logger.debug(
|
||||
`[STREAM] File sent successfully: ${path.basename(
|
||||
filePath
|
||||
)}`
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -2812,30 +2792,8 @@ router.get("/tracks/:id/stream", async (req, res) => {
|
||||
absolutePath
|
||||
);
|
||||
|
||||
res.sendFile(
|
||||
filePath,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Accept-Ranges": "bytes",
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Access-Control-Allow-Origin":
|
||||
req.headers.origin || "*",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Cross-Origin-Resource-Policy": "cross-origin",
|
||||
},
|
||||
},
|
||||
(err) => {
|
||||
// Always destroy the streaming service to clean up intervals
|
||||
streamingService.destroy();
|
||||
if (err) {
|
||||
logger.error(
|
||||
`[STREAM] sendFile fallback error:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
await streamingService.streamFileWithRangeSupport(req, res, filePath, mimeType);
|
||||
streamingService.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user