diff --git a/packages/components/library/src/user-library.ts b/packages/components/library/src/user-library.ts
index 09e8f91..bfbc4e1 100644
--- a/packages/components/library/src/user-library.ts
+++ b/packages/components/library/src/user-library.ts
@@ -24,6 +24,15 @@ export class UserLibrary extends LitElement {
albums,
(album) => html`
+ ${album.base64EmbeddedCover &&
+ html`
+

+ `}
${album.name}
${album.artist.name}
diff --git a/packages/core/types/src/model/album.ts b/packages/core/types/src/model/album.ts
index 6053ed6..874ea45 100644
--- a/packages/core/types/src/model/album.ts
+++ b/packages/core/types/src/model/album.ts
@@ -1,4 +1,4 @@
-import { Brand, Option } from "effect";
+import { Brand } from "effect";
import type { Artist } from "./artist";
import type { Track } from "./track";
@@ -37,8 +37,8 @@ export type Album = {
tracks: Track[];
/**
- * URL to an image of the album. This is typically the album's cover art on a
- * third-party service. If none is available, this field is omitted.
+ * Cover art of the album, encoded in base64. If the cover art is not
+ * available, this field is `undefined`.
*/
- imageUrl: Option.Option
;
+ base64EmbeddedCover: string | undefined;
};
diff --git a/packages/core/types/src/services/metadata-provider.ts b/packages/core/types/src/services/metadata-provider.ts
index 6edc6cd..fea4c3b 100644
--- a/packages/core/types/src/services/metadata-provider.ts
+++ b/packages/core/types/src/services/metadata-provider.ts
@@ -51,13 +51,9 @@ export type TrackMetadata = {
genre?: string[] | undefined;
/**
- * Format and buffer with the embedded cover art.
- * TODO: Re-add without dependency on buffer.
+ * Base64 encoded cover image embedded in the audio file.
*/
- // embeddedCoverArt?: | undefined{
- // format: string;
- // data: Buffer;
- // };
+ base64EmbeddedCover?: string | undefined;
/**
* Keywords to reflect the mood of the audio, e.g. 'Romantic' or 'Sad'
diff --git a/packages/infrastructure/mmb-metadata-provider/index.ts b/packages/infrastructure/mmb-metadata-provider/index.ts
index f5c1c86..5ccb44b 100644
--- a/packages/infrastructure/mmb-metadata-provider/index.ts
+++ b/packages/infrastructure/mmb-metadata-provider/index.ts
@@ -1,12 +1,8 @@
-import {
- MetadataProviderError,
- MetadataProvider,
- type TrackMetadata,
-} from "@echo/core-types";
+import { MetadataProviderError, MetadataProvider } from "@echo/core-types";
import { Effect, Layer } from "effect";
import { Buffer } from "buffer";
import process from "process";
-import { parseWebStream } from "music-metadata";
+import { parseWebStream, type IAudioMetadata } from "music-metadata";
// Polyfills needed for `music-metadata-browser` to work.
globalThis.Buffer = Buffer;
@@ -30,23 +26,48 @@ const mmbMetadataProvider = MetadataProvider.of({
}),
catch: () => MetadataProviderError.MalformedFile,
}).pipe(
- Effect.map(
- (metadata): TrackMetadata => ({
- album: metadata.common.album,
- artists: metadata.common.artists,
- diskNumber: metadata.common.disk.no ?? undefined,
- genre: metadata.common.genre,
- mood: metadata.common.mood,
- title: metadata.common.title,
- totalDisks: metadata.common.disk.of ?? undefined,
- totalTracks: metadata.common.track.of ?? undefined,
- trackNumber: metadata.common.track.no ?? undefined,
- year: metadata.common.year,
+ Effect.flatMap((metadata) =>
+ Effect.gen(function* () {
+ const base64EmbeddedCover = yield* tryExtractToBase64(metadata).pipe(
+ Effect.catchAll(() =>
+ Effect.logError(
+ `Cover extraction failed for ${file.name}, continuing without cover`,
+ ).pipe(Effect.map(() => undefined)),
+ ),
+ );
+
+ return {
+ album: metadata.common.album,
+ artists: metadata.common.artists,
+ diskNumber: metadata.common.disk.no ?? undefined,
+ genre: metadata.common.genre,
+ mood: metadata.common.mood,
+ title: metadata.common.title,
+ totalDisks: metadata.common.disk.of ?? undefined,
+ totalTracks: metadata.common.track.of ?? undefined,
+ trackNumber: metadata.common.track.no ?? undefined,
+ base64EmbeddedCover,
+ year: metadata.common.year,
+ };
}),
),
),
});
+const tryExtractToBase64 = (parsedMetadata: IAudioMetadata) =>
+ Effect.try(() => {
+ const firstPicture = parsedMetadata.common.picture?.find(
+ (picture) => picture.data,
+ );
+
+ if (!firstPicture) {
+ return undefined;
+ }
+
+ const pictureData = new Uint8Array(firstPicture.data as ArrayBufferLike);
+ return Buffer.from(pictureData).toString("base64");
+ });
+
/**
* Layer that provides a metadata provider that uses `music-metadata-browser`.
*/
diff --git a/packages/workers/media-provider/src/sync/file-based-sync.ts b/packages/workers/media-provider/src/sync/file-based-sync.ts
index d8c16c1..b4c8234 100644
--- a/packages/workers/media-provider/src/sync/file-based-sync.ts
+++ b/packages/workers/media-provider/src/sync/file-based-sync.ts
@@ -106,7 +106,7 @@ export const syncFileBasedProvider = ({
const partiallyDownloadFile = (file: FileMetadata) =>
// TODO: Implement retry with a bigger partial range if metadata is undefined.
- partiallyDownloadIntoStream(file, 0, 10000).pipe(
+ partiallyDownloadIntoStream(file, 0, 500000).pipe(
Effect.map((stream) => [stream, file] as const),
Effect.retry({
times: 3,
@@ -222,6 +222,7 @@ const normalizeData = (
{ database, crypto },
artist.id,
metadata.album ?? "Unknown Album",
+ metadata.base64EmbeddedCover,
accumulator.albums,
);
@@ -287,6 +288,7 @@ const tryRetrieveOrCreateAlbum = (
{ database, crypto }: Pick,
artistId: DatabaseArtist["id"],
albumName: string,
+ base64Cover: string | undefined,
processedAlbums: Map,
): Effect.Effect =>
Effect.gen(function* () {
@@ -306,7 +308,7 @@ const tryRetrieveOrCreateAlbum = (
);
return Option.isNone(existingAlbum)
- ? yield* createAlbum({ crypto }, albumName, artistId)
+ ? yield* createAlbum({ crypto }, albumName, artistId, base64Cover)
: existingAlbum.value;
});
@@ -353,6 +355,7 @@ const createAlbum = (
{ crypto }: Pick,
name: string,
artistId: DatabaseArtist["id"],
+ base64Cover: string | undefined,
): Effect.Effect =>
Effect.gen(function* () {
const id = yield* crypto.generateUuid;
@@ -361,7 +364,7 @@ const createAlbum = (
id: AlbumId(id),
name,
artistId,
- imageUrl: Option.some("https://example.com/image.jpg"),
+ base64EmbeddedCover: base64Cover,
};
});