Skip to content

Commit

Permalink
Naive approach to fetch cover art
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepyfran committed Aug 31, 2024
1 parent 7a156ea commit ade4aa2
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 31 deletions.
9 changes: 9 additions & 0 deletions packages/components/library/src/user-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export class UserLibrary extends LitElement {
albums,
(album) => html`
<div key="{album.id}">
${album.base64EmbeddedCover &&
html`
<img
src="data:image/png;base64, ${album.base64EmbeddedCover}"
height="100"
width="100"
alt="Album cover"
/>
`}
<h3>${album.name}</h3>
<p>${album.artist.name}</p>
<button @click=${() => this._playAlbum.run(album)}>Play</button>
Expand Down
8 changes: 4 additions & 4 deletions packages/core/types/src/model/album.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Brand, Option } from "effect";
import { Brand } from "effect";
import type { Artist } from "./artist";
import type { Track } from "./track";

Expand Down Expand Up @@ -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<string>;
base64EmbeddedCover: string | undefined;
};
8 changes: 2 additions & 6 deletions packages/core/types/src/services/metadata-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
57 changes: 39 additions & 18 deletions packages/infrastructure/mmb-metadata-provider/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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`.
*/
Expand Down
9 changes: 6 additions & 3 deletions packages/workers/media-provider/src/sync/file-based-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -222,6 +222,7 @@ const normalizeData = (
{ database, crypto },
artist.id,
metadata.album ?? "Unknown Album",
metadata.base64EmbeddedCover,
accumulator.albums,
);

Expand Down Expand Up @@ -287,6 +288,7 @@ const tryRetrieveOrCreateAlbum = (
{ database, crypto }: Pick<SyncFileBasedProviderInput, "database" | "crypto">,
artistId: DatabaseArtist["id"],
albumName: string,
base64Cover: string | undefined,
processedAlbums: Map<string, DatabaseAlbum>,
): Effect.Effect<DatabaseAlbum> =>
Effect.gen(function* () {
Expand All @@ -306,7 +308,7 @@ const tryRetrieveOrCreateAlbum = (
);

return Option.isNone(existingAlbum)
? yield* createAlbum({ crypto }, albumName, artistId)
? yield* createAlbum({ crypto }, albumName, artistId, base64Cover)
: existingAlbum.value;
});

Expand Down Expand Up @@ -353,6 +355,7 @@ const createAlbum = (
{ crypto }: Pick<SyncFileBasedProviderInput, "crypto">,
name: string,
artistId: DatabaseArtist["id"],
base64Cover: string | undefined,
): Effect.Effect<DatabaseAlbum> =>
Effect.gen(function* () {
const id = yield* crypto.generateUuid;
Expand All @@ -361,7 +364,7 @@ const createAlbum = (
id: AlbumId(id),
name,
artistId,
imageUrl: Option.some("https://example.com/image.jpg"),
base64EmbeddedCover: base64Cover,
};
});

Expand Down

0 comments on commit ade4aa2

Please sign in to comment.