@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user