feat(track): add track cover art into downloaded file

close #24
This commit is contained in:
Raiper34
2025-08-12 21:38:06 +00:00
parent 5f169b0fdb
commit b8309f2d01
8 changed files with 68 additions and 7 deletions
+1
View File
@@ -37,6 +37,7 @@
"bullmq": "^5.31.2",
"fluent-ffmpeg": "^2.1.3",
"isomorphic-unfetch": "^4.0.2",
"node-id3": "^0.2.9",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"spotify-url-info": "^3.2.18",
@@ -21,6 +21,9 @@ export class PlaylistEntity {
@Column({ default: () => Date.now() })
createdAt?: number;
@Column({ nullable: true })
coverUrl?: string;
@OneToMany(() => TrackEntity, (track) => track.playlist)
tracks?: TrackEntity[];
}
+8 -2
View File
@@ -52,7 +52,11 @@ export class PlaylistService {
let playlist2Save: PlaylistEntity;
try {
detail = await this.spotifyService.getPlaylistDetail(playlist.spotifyUrl);
playlist2Save = { ...playlist, name: detail.name };
playlist2Save = {
...playlist,
name: detail.name,
coverUrl: detail.image,
};
this.createPlaylistFolderStructure(playlist2Save.name);
} catch (err) {
playlist2Save = { ...playlist, error: String(err) };
@@ -102,7 +106,9 @@ export class PlaylistService {
for (const playlist of activePlaylists) {
let tracks = [];
try {
tracks = await this.spotifyService.getPlaylistTracks(playlist.spotifyUrl);
tracks = await this.spotifyService.getPlaylistTracks(
playlist.spotifyUrl,
);
this.createPlaylistFolderStructure(playlist.name);
} catch (err) {
await this.update(playlist.id, { ...playlist, error: String(err) });
+6 -2
View File
@@ -9,10 +9,14 @@ export class SpotifyService {
async getPlaylistDetail(
spotifyUrl: string,
): Promise<{ name: string; tracks: any[] }> {
): Promise<{ name: string; tracks: any[]; image: string }> {
this.logger.debug(`Get playlist ${spotifyUrl} on Spotify`);
const detail = await getDetails(spotifyUrl);
return { name: detail.preview.title, tracks: detail?.tracks ?? [] };
return {
name: detail.preview.title,
tracks: detail?.tracks ?? [],
image: detail.preview.image,
};
}
async getPlaylistTracks(spotifyUrl: string): Promise<any[]> {
+24 -1
View File
@@ -8,6 +8,7 @@ import * as ffmpeg from 'fluent-ffmpeg';
import * as yts from 'yt-search';
import * as ytdl from '@distube/ytdl-core';
import { Readable } from 'stream';
const NodeID3 = require('node-id3');
const META_DATA_TITLE = '-metadata';
const YT_SETTINGS: ytdl.downloadOptions = {
@@ -34,7 +35,9 @@ export class YoutubeService {
}
downloadAndFormat(track: TrackEntity, folderName: string): Promise<void> {
this.logger.debug(`Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`);
this.logger.debug(
`Downloading ${track.artist} - ${track.name} (${track.youtubeUrl}) from YT`,
);
return new Promise((res, reject) => {
ffmpeg(this.getYoutubeAudio(track.youtubeUrl, reject))
.outputOptions(...this.getFfmpegOptions(track.name, track.artist))
@@ -54,6 +57,26 @@ export class YoutubeService {
});
}
async addImage(folderName: string, coverUrl: string): Promise<void> {
if (coverUrl) {
const res = await fetch(coverUrl);
const arrayBuf = await res.arrayBuffer();
const imageBuffer = Buffer.from(arrayBuf);
NodeID3.write(
{
APIC: {
mime: 'image/jpeg',
type: { id: 3, name: 'front cover' },
description: 'cover',
imageBuffer,
},
},
folderName,
);
}
}
private getFfmpegOptions(name: string, artist: string): string[] {
return [
META_DATA_TITLE,
+1
View File
@@ -118,6 +118,7 @@ export class TrackService {
try {
const folderName = this.getFolderName(track, track.playlist);
await this.youtubeService.downloadAndFormat(track, folderName);
await this.youtubeService.addImage(folderName, track.playlist.coverUrl);
} catch (err) {
this.logger.error(err);
error = String(err);
+3
View File
@@ -98,5 +98,8 @@
}
}
}
},
"cli": {
"analytics": false
}
}