refactor(track-backend): refactor track backend code
This commit is contained in:
@@ -5,12 +5,12 @@ import {PlaylistEntity} from "./playlist.entity";
|
|||||||
import {TrackService} from "../track/track.service";
|
import {TrackService} from "../track/track.service";
|
||||||
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
||||||
import {Server} from "socket.io";
|
import {Server} from "socket.io";
|
||||||
import {TrackStatusEnum} from "../track/track.model";
|
|
||||||
import {resolve} from "path";
|
import {resolve} from "path";
|
||||||
import {ConfigService} from "@nestjs/config";
|
import {ConfigService} from "@nestjs/config";
|
||||||
import {EnviromentEnum} from "../enviroment.enum";
|
import {EnviromentEnum} from "../enviroment.enum";
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {Interval} from "@nestjs/schedule";
|
import {Interval} from "@nestjs/schedule";
|
||||||
|
import {TrackStatusEnum} from "../track/track.entity";
|
||||||
const fetch = require('isomorphic-unfetch');
|
const fetch = require('isomorphic-unfetch');
|
||||||
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
|
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export class PlaylistService {
|
|||||||
for(let track of details?.tracks ?? []) {
|
for(let track of details?.tracks ?? []) {
|
||||||
await this.trackService.create({
|
await this.trackService.create({
|
||||||
artist: track.artist,
|
artist: track.artist,
|
||||||
song: track.name,
|
name: track.name,
|
||||||
spotifyUrl: track.previewUrl,
|
spotifyUrl: track.previewUrl,
|
||||||
}, savedPlaylist);
|
}, savedPlaylist);
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ export class PlaylistService {
|
|||||||
for(let track of details?.tracks ?? []) {
|
for(let track of details?.tracks ?? []) {
|
||||||
const track2Save = {
|
const track2Save = {
|
||||||
artist: track.artist,
|
artist: track.artist,
|
||||||
song: track.name,
|
name: track.name,
|
||||||
spotifyUrl: track.previewUrl,
|
spotifyUrl: track.previewUrl,
|
||||||
}
|
}
|
||||||
const isExist = !!(await this.trackService.findAll({...track2Save, playlist: {id: playlist.id}})).length;
|
const isExist = !!(await this.trackService.findAll({...track2Save, playlist: {id: playlist.id}})).length;
|
||||||
|
|||||||
@@ -1,45 +1,50 @@
|
|||||||
import {Controller, Delete, Get, Param, Res, StreamableFile} from '@nestjs/common';
|
import {
|
||||||
import {TrackService} from "./track.service";
|
Controller,
|
||||||
import {TrackModel} from "./track.model";
|
Delete,
|
||||||
import {createReadStream} from "fs";
|
Get,
|
||||||
import { resolve } from 'path';
|
Param,
|
||||||
|
Res,
|
||||||
|
StreamableFile,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TrackService } from './track.service';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import {ConfigService} from "@nestjs/config";
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {EnviromentEnum} from "../enviroment.enum";
|
import { TrackEntity } from './track.entity';
|
||||||
|
|
||||||
@Controller('track')
|
@Controller('track')
|
||||||
export class TrackController {
|
export class TrackController {
|
||||||
|
constructor(
|
||||||
|
private readonly service: TrackService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
constructor(private readonly service: TrackService,
|
@Get('playlist/:id')
|
||||||
private readonly configService: ConfigService) {
|
getAllByPlaylist(@Param('id') playlistId: number): Promise<TrackEntity[]> {
|
||||||
}
|
return this.service.getAllByPlaylist(playlistId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get('download/:id')
|
||||||
getAll(): Promise<TrackModel[]> {
|
async getFile(
|
||||||
return this.service.findAll();
|
@Res({ passthrough: true }) res: Response,
|
||||||
}
|
@Param('id') id: number,
|
||||||
|
): Promise<StreamableFile> {
|
||||||
|
const track = await this.service.findOne(id);
|
||||||
|
const fileName = this.service.getTrackFileName(track);
|
||||||
|
const readStream = createReadStream(
|
||||||
|
this.service.getFolderName(track, track.playlist),
|
||||||
|
);
|
||||||
|
res.set({ 'Content-Disposition': `attachment; filename="${fileName}` });
|
||||||
|
return new StreamableFile(readStream);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('playlist/:id')
|
@Delete(':id')
|
||||||
getAllByPlaylist(@Param('id') playlistId: number): Promise<TrackModel[]> {
|
remove(@Param('id') id: number): Promise<void> {
|
||||||
return this.service.getAllByPlaylist(playlistId);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('download/:id')
|
@Get('retry/:id')
|
||||||
async getFile(@Res({ passthrough: true }) res: Response, @Param('id') id: number): Promise<StreamableFile> {
|
retry(@Param('id') id: number): Promise<void> {
|
||||||
const track = await this.service.findOne(id);
|
return this.service.retry(id);
|
||||||
const fileName = `${track.artist} - ${track.song}.${this.configService.get<string>(EnviromentEnum.FORMAT)}`;
|
}
|
||||||
const filePath = createReadStream(resolve(__dirname, '..', this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH), track.playlist.name, fileName));
|
}
|
||||||
res.set({'Content-Disposition': `attachment; filename="${fileName}`});
|
|
||||||
return new StreamableFile(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
remove(@Param('id') id: number): Promise<void> {
|
|
||||||
return this.service.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('retry/:id')
|
|
||||||
retry(@Param('id') id: number): Promise<void> {
|
|
||||||
return this.service.retry(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,34 +1,43 @@
|
|||||||
import {Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn} from 'typeorm';
|
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||||
import {PlaylistEntity} from "../playlist/playlist.entity";
|
import { PlaylistEntity } from '../playlist/playlist.entity';
|
||||||
import {TrackStatusEnum} from "./track.model";
|
|
||||||
|
export enum TrackStatusEnum {
|
||||||
|
New,
|
||||||
|
Searching,
|
||||||
|
Queued,
|
||||||
|
Downloading,
|
||||||
|
Completed,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class TrackEntity {
|
export class TrackEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id?: number;
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@Column()
|
||||||
id?: number;
|
artist: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
artist: string;
|
name: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
song: string;
|
spotifyUrl: string;
|
||||||
|
|
||||||
@Column()
|
@Column({ nullable: true })
|
||||||
spotifyUrl: string;
|
youtubeUrl?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: TrackStatusEnum.New })
|
||||||
youtubeUrl?: string;
|
status?: TrackStatusEnum;
|
||||||
|
|
||||||
@Column({default: TrackStatusEnum.New})
|
@Column({ nullable: true })
|
||||||
status?: TrackStatusEnum;
|
error?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: Date.now() })
|
||||||
error?: string;
|
createdAt?: number;
|
||||||
|
|
||||||
@Column({default: Date.now()})
|
@ManyToOne(() => PlaylistEntity, (playlist) => playlist.tracks, {
|
||||||
createdAt?: number;
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
@ManyToOne(() => PlaylistEntity, playlist => playlist.tracks, {onDelete: "CASCADE"})
|
playlist?: PlaylistEntity;
|
||||||
playlist?: PlaylistEntity;
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import {PlaylistModel} from "../playlist/playlist.model";
|
|
||||||
|
|
||||||
export interface TrackModel {
|
|
||||||
id?: number;
|
|
||||||
artist: string;
|
|
||||||
song: string;
|
|
||||||
spotifyUrl: string;
|
|
||||||
youtubeUrl?: string;
|
|
||||||
status?: TrackStatusEnum,
|
|
||||||
playlist?: PlaylistModel;
|
|
||||||
createdAt?: number;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TrackStatusEnum {
|
|
||||||
New,
|
|
||||||
Searching,
|
|
||||||
Queued,
|
|
||||||
Downloading,
|
|
||||||
Completed,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import {TrackEntity} from "./track.entity";
|
import { TrackEntity } from './track.entity';
|
||||||
import {TrackService} from "./track.service";
|
import { TrackService } from './track.service';
|
||||||
import {TrackController} from "./track.controller";
|
import { TrackController } from './track.controller';
|
||||||
import {ConfigModule} from "@nestjs/config";
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [TypeOrmModule.forFeature([TrackEntity]), ConfigModule],
|
||||||
TypeOrmModule.forFeature([TrackEntity]),
|
providers: [TrackService],
|
||||||
ConfigModule,
|
controllers: [TrackController],
|
||||||
],
|
exports: [TrackService],
|
||||||
providers: [TrackService],
|
|
||||||
controllers: [TrackController],
|
|
||||||
exports: [TrackService],
|
|
||||||
})
|
})
|
||||||
export class TrackModule {}
|
export class TrackModule {}
|
||||||
|
|||||||
@@ -1,124 +1,172 @@
|
|||||||
import {Injectable} from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import {InjectRepository} from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import {Repository} from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import {TrackEntity} from "./track.entity";
|
import { TrackEntity, TrackStatusEnum } from './track.entity';
|
||||||
import {PlaylistEntity} from "../playlist/playlist.entity";
|
import { PlaylistEntity } from '../playlist/playlist.entity';
|
||||||
import {Interval} from "@nestjs/schedule";
|
import { Interval } from '@nestjs/schedule';
|
||||||
import {TrackStatusEnum} from "./track.model";
|
|
||||||
import * as yts from 'yt-search';
|
import * as yts from 'yt-search';
|
||||||
import {SearchResult} from 'yt-search';
|
|
||||||
import * as ytdl from '@distube/ytdl-core';
|
import * as ytdl from '@distube/ytdl-core';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {ConfigService} from "@nestjs/config";
|
import { ConfigService } from '@nestjs/config';
|
||||||
import {resolve} from "path";
|
import { resolve } from 'path';
|
||||||
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||||
import {Server} from "socket.io";
|
import { Server } from 'socket.io';
|
||||||
import * as ffmpeg from 'fluent-ffmpeg';
|
import * as ffmpeg from 'fluent-ffmpeg';
|
||||||
import {EnviromentEnum} from "../enviroment.enum";
|
import { EnviromentEnum } from '../enviroment.enum';
|
||||||
|
|
||||||
|
enum WsTrackOperation {
|
||||||
|
New = 'trackNew',
|
||||||
|
Update = 'trackUpdate',
|
||||||
|
Delete = 'trackDelete',
|
||||||
|
}
|
||||||
|
|
||||||
@WebSocketGateway()
|
@WebSocketGateway()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrackService {
|
export class TrackService {
|
||||||
|
@WebSocketServer() io: Server;
|
||||||
|
private readonly logger = new Logger(TrackService.name);
|
||||||
|
|
||||||
@WebSocketServer() io: Server;
|
constructor(
|
||||||
|
@InjectRepository(TrackEntity)
|
||||||
|
private repository: Repository<TrackEntity>,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
constructor(
|
findAll(
|
||||||
@InjectRepository(TrackEntity)
|
where?: { [key: string]: any },
|
||||||
private repository: Repository<TrackEntity>,
|
relations: Record<string, boolean> = {},
|
||||||
private readonly configService: ConfigService,
|
): Promise<TrackEntity[]> {
|
||||||
) {}
|
return this.repository.find({ where, relations });
|
||||||
|
}
|
||||||
|
|
||||||
findAll(where?: {[key: string]: any}, relations: Record<string, boolean> = {}): Promise<TrackEntity[]> {
|
getAllByPlaylist(id: number): Promise<TrackEntity[]> {
|
||||||
return this.repository.find({where, relations});
|
return this.repository.find({ where: { playlist: { id } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number): Promise<TrackEntity | null> {
|
||||||
|
return this.repository.findOne({ where: { id }, relations: ['playlist'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
await this.repository.delete(id);
|
||||||
|
this.io.emit(WsTrackOperation.Delete, { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(track: TrackEntity, playlist?: PlaylistEntity): Promise<void> {
|
||||||
|
const savedTrack = await this.repository.save({ ...track, playlist });
|
||||||
|
this.io.emit(WsTrackOperation.New, {
|
||||||
|
track: savedTrack,
|
||||||
|
playlistId: playlist.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, track: TrackEntity): Promise<void> {
|
||||||
|
await this.repository.update(id, track);
|
||||||
|
this.io.emit(WsTrackOperation.Update, track);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retry(id: number): Promise<void> {
|
||||||
|
const track = await this.findOne(id);
|
||||||
|
await this.update(id, { ...track, status: TrackStatusEnum.New });
|
||||||
|
}
|
||||||
|
|
||||||
|
@Interval(1_000)
|
||||||
|
async findOnYoutube(): Promise<void> {
|
||||||
|
const newTracks = await this.findAll({ status: TrackStatusEnum.New });
|
||||||
|
await this.changeTracksStatuses(newTracks, TrackStatusEnum.Searching);
|
||||||
|
for (const track of newTracks) {
|
||||||
|
let updatedTrack: TrackEntity;
|
||||||
|
try {
|
||||||
|
const youtubeResult = await yts(`${track.artist} - ${track.name}`);
|
||||||
|
updatedTrack = {
|
||||||
|
...track,
|
||||||
|
youtubeUrl: youtubeResult.videos[0].url,
|
||||||
|
status: TrackStatusEnum.Queued,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
updatedTrack = {
|
||||||
|
...track,
|
||||||
|
error: String(err),
|
||||||
|
status: TrackStatusEnum.Error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await this.update(track.id, updatedTrack);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAllByPlaylist(id: number): Promise<TrackEntity[]> {
|
@Interval(1_000)
|
||||||
return this.repository.find({where: {playlist: {id}}});
|
async downloadFromYoutube(): Promise<void> {
|
||||||
|
const queuedTracks = await this.findAll(
|
||||||
|
{ status: TrackStatusEnum.Queued },
|
||||||
|
{ playlist: true },
|
||||||
|
);
|
||||||
|
await this.changeTracksStatuses(queuedTracks, TrackStatusEnum.Downloading);
|
||||||
|
for (const track of queuedTracks) {
|
||||||
|
let error: string;
|
||||||
|
try {
|
||||||
|
await this.downloadAndFormat(track, track.playlist);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err);
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
const updatedTrack = {
|
||||||
|
...track,
|
||||||
|
status: error ? TrackStatusEnum.Error : TrackStatusEnum.Completed,
|
||||||
|
...(error ? { error } : {}),
|
||||||
|
};
|
||||||
|
await this.update(track.id, updatedTrack);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findOne(id: number): Promise<TrackEntity | null> {
|
private downloadAndFormat(
|
||||||
return this.repository.findOne({where: {id}, relations: ['playlist']});
|
track: TrackEntity,
|
||||||
}
|
playlist: PlaylistEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
const ffmpegOptions = [
|
||||||
|
'-metadata',
|
||||||
|
`title=${track.name}`,
|
||||||
|
'-metadata',
|
||||||
|
`artist=${track.artist}`,
|
||||||
|
];
|
||||||
|
return new Promise((res, reject) => {
|
||||||
|
const audio = ytdl(track.youtubeUrl, {
|
||||||
|
quality: 'highestaudio',
|
||||||
|
filter: 'audioonly',
|
||||||
|
}).on('error', (err) => reject(err));
|
||||||
|
ffmpeg(audio)
|
||||||
|
.outputOptions(ffmpegOptions)
|
||||||
|
.format(this.configService.get<string>(EnviromentEnum.FORMAT))
|
||||||
|
.on('error', (err) => reject(err))
|
||||||
|
.pipe(
|
||||||
|
fs
|
||||||
|
.createWriteStream(this.getFolderName(track, playlist))
|
||||||
|
.on('finish', () => res())
|
||||||
|
.on('error', (err) => reject(err)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async remove(id: number): Promise<void> {
|
getTrackFileName(track: TrackEntity): string {
|
||||||
await this.repository.delete(id);
|
return `${track.artist} - ${track.name.replace('/', '')}.${this.configService.get<string>(EnviromentEnum.FORMAT)}`;
|
||||||
this.io.emit('trackDelete', {id});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async create(track: TrackEntity, playlist?: PlaylistEntity): Promise<void> {
|
getFolderName(track: TrackEntity, playlist: PlaylistEntity): string {
|
||||||
const savedTrack = await this.repository.save({...track, playlist});
|
return resolve(
|
||||||
this.io.emit('trackNew', {track: savedTrack, playlistId: playlist.id});
|
__dirname,
|
||||||
}
|
'..',
|
||||||
|
this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH),
|
||||||
|
playlist.name,
|
||||||
|
this.getTrackFileName(track),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async update(id: number, track: TrackEntity): Promise<void> {
|
private async changeTracksStatuses(
|
||||||
await this.repository.update(id, track);
|
tracks: TrackEntity[],
|
||||||
this.io.emit('trackUpdate', track);
|
status: TrackStatusEnum,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const track of tracks) {
|
||||||
|
await this.update(track.id, { ...track, status });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async retry(id: number): Promise<void> {
|
}
|
||||||
const track = await this.findOne(id);
|
|
||||||
await this.update(id, {...track, status: TrackStatusEnum.New});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Interval(1000)
|
|
||||||
async findOnYoutube() {
|
|
||||||
const newTracks = await this.findAll({status: TrackStatusEnum.New});
|
|
||||||
for (let track of newTracks) {
|
|
||||||
await this.update(track.id, {...track, status: TrackStatusEnum.Searching});
|
|
||||||
}
|
|
||||||
for(let track of newTracks) {
|
|
||||||
let youtubeResult: SearchResult;
|
|
||||||
let updatedTrack: TrackEntity;
|
|
||||||
try {
|
|
||||||
youtubeResult = await yts(`${track.artist} - ${track.song}`);
|
|
||||||
updatedTrack = {...track, youtubeUrl: youtubeResult.videos[0].url, status: TrackStatusEnum.Queued};
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
updatedTrack = {...track, error: String(err), status: TrackStatusEnum.Error};
|
|
||||||
}
|
|
||||||
await this.update(track.id, updatedTrack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Interval(1000)
|
|
||||||
async download() {
|
|
||||||
const queuedTracks = await this.findAll({status: TrackStatusEnum.Queued}, {playlist: true});
|
|
||||||
for (let track of queuedTracks) {
|
|
||||||
await this.update(track.id, {...track, status: TrackStatusEnum.Downloading});
|
|
||||||
}
|
|
||||||
for(let track of queuedTracks) {
|
|
||||||
let error: string;
|
|
||||||
try {
|
|
||||||
await this.youtubeDownload(track, track.playlist.name);
|
|
||||||
} catch(err) {
|
|
||||||
console.log(err);
|
|
||||||
error = String(err);
|
|
||||||
}
|
|
||||||
const updatedTrack = {
|
|
||||||
...track,
|
|
||||||
status: error ? TrackStatusEnum.Error : TrackStatusEnum.Completed,
|
|
||||||
...(error ? {error} : {}),
|
|
||||||
};
|
|
||||||
await this.update(track.id, updatedTrack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private youtubeDownload(track: TrackEntity, playlistName: string): Promise<void> {
|
|
||||||
return new Promise((res, reject) => {
|
|
||||||
const audio = ytdl(track.youtubeUrl, {quality: "highestaudio", filter: "audioonly"})
|
|
||||||
.on('error', (err) => reject(err));
|
|
||||||
ffmpeg(audio)
|
|
||||||
.outputOptions('-metadata', `title=${track.song}`, '-metadata', `artist=${track.artist}`)
|
|
||||||
.format(this.configService.get<string>(EnviromentEnum.FORMAT))
|
|
||||||
.on('error', (err) => reject(err))
|
|
||||||
.pipe(
|
|
||||||
fs.createWriteStream(
|
|
||||||
resolve(
|
|
||||||
__dirname, '..', this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH), playlistName,
|
|
||||||
`${track.artist} - ${track.song.replace('/', '')}.${this.configService.get<string>(EnviromentEnum.FORMAT)}`
|
|
||||||
)
|
|
||||||
).on('finish', () => res()).on('error', (err) => reject(err))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of tracks$ | async">
|
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of tracks$ | async">
|
||||||
<div>
|
<div>
|
||||||
<span>{{ track.artist }} - {{ track.song }}</span>
|
<span>{{ track.artist }} - {{ track.name }}</span>
|
||||||
<a [href]="track.spotifyUrl"
|
<a [href]="track.spotifyUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="is-color-primary margin-left" title="Spotify preview of track that will be downloaded">
|
class="is-color-primary margin-left" title="Spotify preview of track that will be downloaded">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const ENDPOINT = '/api/track';
|
|||||||
export interface Track {
|
export interface Track {
|
||||||
id: number;
|
id: number;
|
||||||
artist: string;
|
artist: string;
|
||||||
song: string;
|
name: string;
|
||||||
spotifyUrl: string;
|
spotifyUrl: string;
|
||||||
youtubeUrl: string;
|
youtubeUrl: string;
|
||||||
status: TrackStatusEnum;
|
status: TrackStatusEnum;
|
||||||
|
|||||||
Reference in New Issue
Block a user