Generated
+22
-2
@@ -14482,6 +14482,25 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-id3": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-id3/-/node-id3-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-dSxhuxrkkGVRgUhDHFxdY0pilzOREcodO01HcZWfaRkCaPWGmo0dOgD8ygyL6ln4Iv4cmfRxAWn1WD9bIB9Bhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-id3/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -20475,7 +20494,7 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"src/backend": {
|
"src/backend": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.11",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@distube/ytdl-core": "^4.16.12",
|
"@distube/ytdl-core": "^4.16.12",
|
||||||
@@ -20494,6 +20513,7 @@
|
|||||||
"bullmq": "^5.31.2",
|
"bullmq": "^5.31.2",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"isomorphic-unfetch": "^4.0.2",
|
"isomorphic-unfetch": "^4.0.2",
|
||||||
|
"node-id3": "^0.2.9",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"spotify-url-info": "^3.2.18",
|
"spotify-url-info": "^3.2.18",
|
||||||
@@ -20933,7 +20953,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/frontend": {
|
"src/frontend": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.0.6",
|
"@angular/animations": "^19.0.6",
|
||||||
"@angular/common": "^19.0.6",
|
"@angular/common": "^19.0.6",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"bullmq": "^5.31.2",
|
"bullmq": "^5.31.2",
|
||||||
"fluent-ffmpeg": "^2.1.3",
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"isomorphic-unfetch": "^4.0.2",
|
"isomorphic-unfetch": "^4.0.2",
|
||||||
|
"node-id3": "^0.2.9",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"spotify-url-info": "^3.2.18",
|
"spotify-url-info": "^3.2.18",
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export class PlaylistEntity {
|
|||||||
@Column({ default: () => Date.now() })
|
@Column({ default: () => Date.now() })
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
coverUrl?: string;
|
||||||
|
|
||||||
@OneToMany(() => TrackEntity, (track) => track.playlist)
|
@OneToMany(() => TrackEntity, (track) => track.playlist)
|
||||||
tracks?: TrackEntity[];
|
tracks?: TrackEntity[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ export class PlaylistService {
|
|||||||
let playlist2Save: PlaylistEntity;
|
let playlist2Save: PlaylistEntity;
|
||||||
try {
|
try {
|
||||||
detail = await this.spotifyService.getPlaylistDetail(playlist.spotifyUrl);
|
detail = await this.spotifyService.getPlaylistDetail(playlist.spotifyUrl);
|
||||||
playlist2Save = { ...playlist, name: detail.name };
|
playlist2Save = {
|
||||||
|
...playlist,
|
||||||
|
name: detail.name,
|
||||||
|
coverUrl: detail.image,
|
||||||
|
};
|
||||||
this.createPlaylistFolderStructure(playlist2Save.name);
|
this.createPlaylistFolderStructure(playlist2Save.name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
playlist2Save = { ...playlist, error: String(err) };
|
playlist2Save = { ...playlist, error: String(err) };
|
||||||
@@ -102,7 +106,9 @@ export class PlaylistService {
|
|||||||
for (const playlist of activePlaylists) {
|
for (const playlist of activePlaylists) {
|
||||||
let tracks = [];
|
let tracks = [];
|
||||||
try {
|
try {
|
||||||
tracks = await this.spotifyService.getPlaylistTracks(playlist.spotifyUrl);
|
tracks = await this.spotifyService.getPlaylistTracks(
|
||||||
|
playlist.spotifyUrl,
|
||||||
|
);
|
||||||
this.createPlaylistFolderStructure(playlist.name);
|
this.createPlaylistFolderStructure(playlist.name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await this.update(playlist.id, { ...playlist, error: String(err) });
|
await this.update(playlist.id, { ...playlist, error: String(err) });
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ export class SpotifyService {
|
|||||||
|
|
||||||
async getPlaylistDetail(
|
async getPlaylistDetail(
|
||||||
spotifyUrl: string,
|
spotifyUrl: string,
|
||||||
): Promise<{ name: string; tracks: any[] }> {
|
): Promise<{ name: string; tracks: any[]; image: string }> {
|
||||||
this.logger.debug(`Get playlist ${spotifyUrl} on Spotify`);
|
this.logger.debug(`Get playlist ${spotifyUrl} on Spotify`);
|
||||||
const detail = await getDetails(spotifyUrl);
|
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[]> {
|
async getPlaylistTracks(spotifyUrl: string): Promise<any[]> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as ffmpeg from 'fluent-ffmpeg';
|
|||||||
import * as yts from 'yt-search';
|
import * as yts from 'yt-search';
|
||||||
import * as ytdl from '@distube/ytdl-core';
|
import * as ytdl from '@distube/ytdl-core';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
const NodeID3 = require('node-id3');
|
||||||
|
|
||||||
const META_DATA_TITLE = '-metadata';
|
const META_DATA_TITLE = '-metadata';
|
||||||
const YT_SETTINGS: ytdl.downloadOptions = {
|
const YT_SETTINGS: ytdl.downloadOptions = {
|
||||||
@@ -34,7 +35,9 @@ export class YoutubeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadAndFormat(track: TrackEntity, folderName: string): Promise<void> {
|
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) => {
|
return new Promise((res, reject) => {
|
||||||
ffmpeg(this.getYoutubeAudio(track.youtubeUrl, reject))
|
ffmpeg(this.getYoutubeAudio(track.youtubeUrl, reject))
|
||||||
.outputOptions(...this.getFfmpegOptions(track.name, track.artist))
|
.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[] {
|
private getFfmpegOptions(name: string, artist: string): string[] {
|
||||||
return [
|
return [
|
||||||
META_DATA_TITLE,
|
META_DATA_TITLE,
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export class TrackService {
|
|||||||
try {
|
try {
|
||||||
const folderName = this.getFolderName(track, track.playlist);
|
const folderName = this.getFolderName(track, track.playlist);
|
||||||
await this.youtubeService.downloadAndFormat(track, folderName);
|
await this.youtubeService.downloadAndFormat(track, folderName);
|
||||||
|
await this.youtubeService.addImage(folderName, track.playlist.coverUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
error = String(err);
|
error = String(err);
|
||||||
|
|||||||
@@ -98,5 +98,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user