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 {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
||||
import {Server} from "socket.io";
|
||||
import {TrackStatusEnum} from "../track/track.model";
|
||||
import {resolve} from "path";
|
||||
import {ConfigService} from "@nestjs/config";
|
||||
import {EnviromentEnum} from "../enviroment.enum";
|
||||
import * as fs from 'fs';
|
||||
import {Interval} from "@nestjs/schedule";
|
||||
import {TrackStatusEnum} from "../track/track.entity";
|
||||
const fetch = require('isomorphic-unfetch');
|
||||
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
|
||||
|
||||
@@ -54,7 +54,7 @@ export class PlaylistService {
|
||||
for(let track of details?.tracks ?? []) {
|
||||
await this.trackService.create({
|
||||
artist: track.artist,
|
||||
song: track.name,
|
||||
name: track.name,
|
||||
spotifyUrl: track.previewUrl,
|
||||
}, savedPlaylist);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export class PlaylistService {
|
||||
for(let track of details?.tracks ?? []) {
|
||||
const track2Save = {
|
||||
artist: track.artist,
|
||||
song: track.name,
|
||||
name: track.name,
|
||||
spotifyUrl: track.previewUrl,
|
||||
}
|
||||
const isExist = !!(await this.trackService.findAll({...track2Save, playlist: {id: playlist.id}})).length;
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
import {Controller, Delete, Get, Param, Res, StreamableFile} from '@nestjs/common';
|
||||
import {TrackService} from "./track.service";
|
||||
import {TrackModel} from "./track.model";
|
||||
import {createReadStream} from "fs";
|
||||
import { resolve } from 'path';
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Res,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { TrackService } from './track.service';
|
||||
import { createReadStream } from 'fs';
|
||||
import type { Response } from 'express';
|
||||
import {ConfigService} from "@nestjs/config";
|
||||
import {EnviromentEnum} from "../enviroment.enum";
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { TrackEntity } from './track.entity';
|
||||
|
||||
@Controller('track')
|
||||
export class TrackController {
|
||||
|
||||
constructor(private readonly service: TrackService,
|
||||
private readonly configService: ConfigService) {
|
||||
}
|
||||
|
||||
@Get()
|
||||
getAll(): Promise<TrackModel[]> {
|
||||
return this.service.findAll();
|
||||
}
|
||||
constructor(
|
||||
private readonly service: TrackService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('playlist/:id')
|
||||
getAllByPlaylist(@Param('id') playlistId: number): Promise<TrackModel[]> {
|
||||
getAllByPlaylist(@Param('id') playlistId: number): Promise<TrackEntity[]> {
|
||||
return this.service.getAllByPlaylist(playlistId);
|
||||
}
|
||||
|
||||
@Get('download/:id')
|
||||
async getFile(@Res({ passthrough: true }) res: Response, @Param('id') id: number): Promise<StreamableFile> {
|
||||
async getFile(
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Param('id') id: number,
|
||||
): Promise<StreamableFile> {
|
||||
const track = await this.service.findOne(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));
|
||||
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(filePath);
|
||||
return new StreamableFile(readStream);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn} from 'typeorm';
|
||||
import {PlaylistEntity} from "../playlist/playlist.entity";
|
||||
import {TrackStatusEnum} from "./track.model";
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||
import { PlaylistEntity } from '../playlist/playlist.entity';
|
||||
|
||||
export enum TrackStatusEnum {
|
||||
New,
|
||||
Searching,
|
||||
Queued,
|
||||
Downloading,
|
||||
Completed,
|
||||
Error,
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class TrackEntity {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id?: number;
|
||||
|
||||
@@ -12,7 +19,7 @@ export class TrackEntity {
|
||||
artist: string;
|
||||
|
||||
@Column()
|
||||
song: string;
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
spotifyUrl: string;
|
||||
@@ -29,6 +36,8 @@ export class TrackEntity {
|
||||
@Column({ default: Date.now() })
|
||||
createdAt?: number;
|
||||
|
||||
@ManyToOne(() => PlaylistEntity, playlist => playlist.tracks, {onDelete: "CASCADE"})
|
||||
@ManyToOne(() => PlaylistEntity, (playlist) => playlist.tracks, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
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,15 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import {TrackEntity} from "./track.entity";
|
||||
import {TrackService} from "./track.service";
|
||||
import {TrackController} from "./track.controller";
|
||||
import {ConfigModule} from "@nestjs/config";
|
||||
import { TrackEntity } from './track.entity';
|
||||
import { TrackService } from './track.service';
|
||||
import { TrackController } from './track.controller';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([TrackEntity]),
|
||||
ConfigModule,
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([TrackEntity]), ConfigModule],
|
||||
providers: [TrackService],
|
||||
controllers: [TrackController],
|
||||
exports: [TrackService],
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import {Injectable} from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {TrackEntity} from "./track.entity";
|
||||
import {PlaylistEntity} from "../playlist/playlist.entity";
|
||||
import {Interval} from "@nestjs/schedule";
|
||||
import {TrackStatusEnum} from "./track.model";
|
||||
import { TrackEntity, TrackStatusEnum } from './track.entity';
|
||||
import { PlaylistEntity } from '../playlist/playlist.entity';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import * as yts from 'yt-search';
|
||||
import {SearchResult} from 'yt-search';
|
||||
import * as ytdl from '@distube/ytdl-core';
|
||||
import * as fs from 'fs';
|
||||
import {ConfigService} from "@nestjs/config";
|
||||
import {resolve} from "path";
|
||||
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
||||
import {Server} from "socket.io";
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { resolve } from 'path';
|
||||
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||
import { Server } from 'socket.io';
|
||||
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()
|
||||
@Injectable()
|
||||
export class TrackService {
|
||||
|
||||
@WebSocketServer() io: Server;
|
||||
private readonly logger = new Logger(TrackService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TrackEntity)
|
||||
@@ -28,7 +32,10 @@ export class TrackService {
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
findAll(where?: {[key: string]: any}, relations: Record<string, boolean> = {}): Promise<TrackEntity[]> {
|
||||
findAll(
|
||||
where?: { [key: string]: any },
|
||||
relations: Record<string, boolean> = {},
|
||||
): Promise<TrackEntity[]> {
|
||||
return this.repository.find({ where, relations });
|
||||
}
|
||||
|
||||
@@ -42,17 +49,20 @@ export class TrackService {
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
this.io.emit('trackDelete', {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('trackNew', {track: savedTrack, playlistId: playlist.id});
|
||||
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('trackUpdate', track);
|
||||
this.io.emit(WsTrackOperation.Update, track);
|
||||
}
|
||||
|
||||
async retry(id: number): Promise<void> {
|
||||
@@ -60,38 +70,44 @@ export class TrackService {
|
||||
await this.update(id, { ...track, status: TrackStatusEnum.New });
|
||||
}
|
||||
|
||||
@Interval(1000)
|
||||
async findOnYoutube() {
|
||||
@Interval(1_000)
|
||||
async findOnYoutube(): Promise<void> {
|
||||
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;
|
||||
await this.changeTracksStatuses(newTracks, TrackStatusEnum.Searching);
|
||||
for (const track of newTracks) {
|
||||
let updatedTrack: TrackEntity;
|
||||
try {
|
||||
youtubeResult = await yts(`${track.artist} - ${track.song}`);
|
||||
updatedTrack = {...track, youtubeUrl: youtubeResult.videos[0].url, status: TrackStatusEnum.Queued};
|
||||
const youtubeResult = await yts(`${track.artist} - ${track.name}`);
|
||||
updatedTrack = {
|
||||
...track,
|
||||
youtubeUrl: youtubeResult.videos[0].url,
|
||||
status: TrackStatusEnum.Queued,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
updatedTrack = {...track, error: String(err), status: TrackStatusEnum.Error};
|
||||
this.logger.error(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) {
|
||||
@Interval(1_000)
|
||||
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.youtubeDownload(track, track.playlist.name);
|
||||
await this.downloadAndFormat(track, track.playlist);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
this.logger.error(err);
|
||||
error = String(err);
|
||||
}
|
||||
const updatedTrack = {
|
||||
@@ -103,22 +119,54 @@ export class TrackService {
|
||||
}
|
||||
}
|
||||
|
||||
private youtubeDownload(track: TrackEntity, playlistName: string): Promise<void> {
|
||||
private downloadAndFormat(
|
||||
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));
|
||||
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}`)
|
||||
.outputOptions(ffmpegOptions)
|
||||
.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))
|
||||
fs
|
||||
.createWriteStream(this.getFolderName(track, playlist))
|
||||
.on('finish', () => res())
|
||||
.on('error', (err) => reject(err)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getTrackFileName(track: TrackEntity): string {
|
||||
return `${track.artist} - ${track.name.replace('/', '')}.${this.configService.get<string>(EnviromentEnum.FORMAT)}`;
|
||||
}
|
||||
|
||||
getFolderName(track: TrackEntity, playlist: PlaylistEntity): string {
|
||||
return resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH),
|
||||
playlist.name,
|
||||
this.getTrackFileName(track),
|
||||
);
|
||||
}
|
||||
|
||||
private async changeTracksStatuses(
|
||||
tracks: TrackEntity[],
|
||||
status: TrackStatusEnum,
|
||||
): Promise<void> {
|
||||
for (const track of tracks) {
|
||||
await this.update(track.id, { ...track, status });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of tracks$ | async">
|
||||
<div>
|
||||
<span>{{ track.artist }} - {{ track.song }}</span>
|
||||
<span>{{ track.artist }} - {{ track.name }}</span>
|
||||
<a [href]="track.spotifyUrl"
|
||||
target="_blank"
|
||||
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 {
|
||||
id: number;
|
||||
artist: string;
|
||||
song: string;
|
||||
name: string;
|
||||
spotifyUrl: string;
|
||||
youtubeUrl: string;
|
||||
status: TrackStatusEnum;
|
||||
|
||||
Reference in New Issue
Block a user