refactor(playlist-backend): refactor playlist backend code
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export enum EnviromentEnum {
|
export enum EnviromentEnum {
|
||||||
DB_PATH = 'DB_PATH',
|
DB_PATH = 'DB_PATH',
|
||||||
FE_PATH = 'FE_PATH',
|
FE_PATH = 'FE_PATH',
|
||||||
DOWNLOADS_PATH = 'DOWNLOADS_PATH',
|
DOWNLOADS_PATH = 'DOWNLOADS_PATH',
|
||||||
FORMAT = 'FORMAT',
|
FORMAT = 'FORMAT',
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,44 @@
|
|||||||
import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common';
|
import {
|
||||||
import {PlaylistService} from "./playlist.service";
|
Body,
|
||||||
import {PlaylistModel} from "./playlist.model";
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PlaylistService } from './playlist.service';
|
||||||
|
import { PlaylistEntity } from './playlist.entity';
|
||||||
|
|
||||||
@Controller('playlist')
|
@Controller('playlist')
|
||||||
export class PlaylistController {
|
export class PlaylistController {
|
||||||
|
constructor(private readonly service: PlaylistService) {}
|
||||||
|
|
||||||
constructor(
|
@Get()
|
||||||
private readonly service: PlaylistService,
|
getAll(): Promise<PlaylistEntity[]> {
|
||||||
) {
|
return this.service.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Post()
|
||||||
getAll(): Promise<PlaylistModel[]> {
|
async create(@Body() playlist: PlaylistEntity): Promise<void> {
|
||||||
return this.service.findAll();
|
await this.service.create(playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Put(':id')
|
||||||
async create(@Body() playlist: PlaylistModel): Promise<void> {
|
update(
|
||||||
await this.service.create(playlist);
|
@Param('id') id: number,
|
||||||
}
|
@Body() playlist: Partial<PlaylistEntity>,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.service.update(id, playlist);
|
||||||
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Delete(':id')
|
||||||
update(@Param('id') id: number, @Body() playlist: Partial<PlaylistModel>): Promise<void> {
|
remove(@Param('id') id: number): Promise<void> {
|
||||||
return this.service.update(id, playlist);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Get('retry/:id')
|
||||||
remove(@Param('id') id: number): Promise<void> {
|
retryFailedOfPlaylist(@Param('id') id: number): Promise<void> {
|
||||||
return this.service.remove(id);
|
return this.service.retryFailedOfPlaylist(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('retry/:id')
|
|
||||||
retryFailedOfPlaylist(@Param('id') id: number): Promise<void> {
|
|
||||||
return this.service.retryFailedOfPlaylist(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||||
import {TrackEntity} from "../track/track.entity";
|
import { TrackEntity } from '../track/track.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class PlaylistEntity {
|
export class PlaylistEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@Column({ nullable: true })
|
||||||
id: number;
|
name?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column()
|
||||||
name?: string;
|
spotifyUrl: string;
|
||||||
|
|
||||||
@Column()
|
@Column({ nullable: true })
|
||||||
spotifyUrl: string;
|
error?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ default: false })
|
||||||
error?: string;
|
active?: boolean;
|
||||||
|
|
||||||
@Column({default: false})
|
@Column({ default: () => Date.now() })
|
||||||
active?: boolean;
|
createdAt?: number;
|
||||||
|
|
||||||
@Column({default: () => Date.now()})
|
@OneToMany(() => TrackEntity, (track) => track.playlist)
|
||||||
createdAt?: number;
|
tracks?: TrackEntity[];
|
||||||
|
|
||||||
@OneToMany(() => TrackEntity, track => track.playlist)
|
|
||||||
tracks?: TrackEntity[];
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface PlaylistModel {
|
|
||||||
id: number;
|
|
||||||
name?: string;
|
|
||||||
spotifyUrl: string;
|
|
||||||
tracks?: any[]; //todo fix it
|
|
||||||
error?: string;
|
|
||||||
createdAt?: number;
|
|
||||||
active?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import {PlaylistEntity} from "./playlist.entity";
|
import { PlaylistEntity } from './playlist.entity';
|
||||||
import {PlaylistService} from "./playlist.service";
|
import { PlaylistService } from './playlist.service';
|
||||||
import {PlaylistController} from "./playlist.controller";
|
import { PlaylistController } from './playlist.controller';
|
||||||
import {TrackModule} from "../track/track.module";
|
import { TrackModule } from '../track/track.module';
|
||||||
import {ConfigModule} from "@nestjs/config";
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([PlaylistEntity]),
|
TypeOrmModule.forFeature([PlaylistEntity]),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
TrackModule,
|
TrackModule,
|
||||||
],
|
SharedModule,
|
||||||
providers: [PlaylistService],
|
],
|
||||||
controllers: [PlaylistController],
|
providers: [PlaylistService],
|
||||||
exports: [PlaylistService]
|
controllers: [PlaylistController],
|
||||||
|
exports: [PlaylistService],
|
||||||
})
|
})
|
||||||
export class PlaylistModule {}
|
export class PlaylistModule {}
|
||||||
@@ -1,113 +1,127 @@
|
|||||||
import {Injectable} from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {InjectRepository} from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import {Repository} from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import {PlaylistEntity} from "./playlist.entity";
|
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 {resolve} from "path";
|
|
||||||
import {ConfigService} from "@nestjs/config";
|
|
||||||
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";
|
import { TrackStatusEnum } from '../track/track.entity';
|
||||||
|
import { UtilsService } from '../shared/utils.service';
|
||||||
const fetch = require('isomorphic-unfetch');
|
const fetch = require('isomorphic-unfetch');
|
||||||
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
|
const { getDetails } = require('spotify-url-info')(fetch);
|
||||||
|
|
||||||
|
enum WsPlaylistOperation {
|
||||||
|
New = 'playlistNew',
|
||||||
|
Update = 'playlistUpdate',
|
||||||
|
Delete = 'playlistDelete',
|
||||||
|
}
|
||||||
|
|
||||||
@WebSocketGateway()
|
@WebSocketGateway()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlaylistService {
|
export class PlaylistService {
|
||||||
|
@WebSocketServer() io: Server;
|
||||||
|
|
||||||
@WebSocketServer() io: Server;
|
constructor(
|
||||||
|
@InjectRepository(PlaylistEntity)
|
||||||
|
private repository: Repository<PlaylistEntity>,
|
||||||
|
private readonly trackService: TrackService,
|
||||||
|
private readonly utilsService: UtilsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
constructor(
|
findAll(
|
||||||
@InjectRepository(PlaylistEntity)
|
relations: Record<string, boolean> = { tracks: true },
|
||||||
private repository: Repository<PlaylistEntity>,
|
where?: Partial<PlaylistEntity>,
|
||||||
private readonly trackService: TrackService,
|
): Promise<PlaylistEntity[]> {
|
||||||
private readonly configService: ConfigService,
|
return this.repository.find({ where, relations });
|
||||||
) {}
|
}
|
||||||
|
|
||||||
findAll(relations: Record<string, boolean> = {tracks: true}, where?: Partial<PlaylistEntity>): Promise<PlaylistEntity[]> {
|
findOne(id: number): Promise<PlaylistEntity | null> {
|
||||||
return this.repository.find({where, relations});
|
return this.repository.findOneBy({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
await this.repository.delete(id);
|
||||||
|
this.io.emit(WsPlaylistOperation.Delete, { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(playlist: PlaylistEntity): Promise<void> {
|
||||||
|
let details;
|
||||||
|
let playlist2Save: PlaylistEntity;
|
||||||
|
try {
|
||||||
|
details = await getDetails(playlist.spotifyUrl);
|
||||||
|
playlist2Save = { ...playlist, name: details.preview.title };
|
||||||
|
} catch (err) {
|
||||||
|
playlist2Save = { ...playlist, error: String(err) };
|
||||||
}
|
}
|
||||||
|
const savedPlaylist = await this.save(playlist2Save);
|
||||||
findOne(id: number): Promise<PlaylistEntity | null> {
|
this.createPlaylistFolderStructure(savedPlaylist.name);
|
||||||
return this.repository.findOneBy({ id });
|
for (const track of details?.tracks ?? []) {
|
||||||
|
await this.trackService.create(
|
||||||
|
{
|
||||||
|
artist: track.artist,
|
||||||
|
name: track.name,
|
||||||
|
spotifyUrl: track.previewUrl,
|
||||||
|
},
|
||||||
|
savedPlaylist,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async remove(id: number): Promise<void> {
|
async save(playlist: PlaylistEntity): Promise<PlaylistEntity> {
|
||||||
await this.repository.delete(id);
|
const savedPlaylist = await this.repository.save(playlist);
|
||||||
this.io.emit('playlistDelete', {id});
|
this.io.emit(WsPlaylistOperation.New, savedPlaylist);
|
||||||
|
return savedPlaylist;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, playlist: Partial<PlaylistEntity>): Promise<void> {
|
||||||
|
await this.repository.update(id, playlist);
|
||||||
|
const dbPlaylist = await this.findOne(id);
|
||||||
|
this.io.emit(WsPlaylistOperation.Update, dbPlaylist);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retryFailedOfPlaylist(id: number): Promise<void> {
|
||||||
|
const tracks = await this.trackService.getAllByPlaylist(id);
|
||||||
|
for (const track of tracks) {
|
||||||
|
if (track.status === TrackStatusEnum.Error) {
|
||||||
|
await this.trackService.retry(track.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(playlist: PlaylistEntity): Promise<void> {
|
private createPlaylistFolderStructure(playlistName: string): void {
|
||||||
let details;
|
const playlistPath = this.utilsService.getPlaylistFolderPath(playlistName);
|
||||||
let playlist2Save: PlaylistEntity;
|
!fs.existsSync(playlistPath) && fs.mkdirSync(playlistPath);
|
||||||
try {
|
}
|
||||||
details = await getDetails(playlist.spotifyUrl);
|
|
||||||
playlist2Save = {...playlist, name: details.preview.title};
|
|
||||||
} catch (err) {
|
|
||||||
playlist2Save = {...playlist, error: String(err)};
|
|
||||||
}
|
|
||||||
const savedPlaylist = await this.save(playlist2Save);
|
|
||||||
this.createPlaylistFolderStructure(savedPlaylist.name);
|
|
||||||
for(let track of details?.tracks ?? []) {
|
|
||||||
await this.trackService.create({
|
|
||||||
artist: track.artist,
|
|
||||||
name: track.name,
|
|
||||||
spotifyUrl: track.previewUrl,
|
|
||||||
}, savedPlaylist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(playlist: PlaylistEntity): Promise<PlaylistEntity> {
|
@Interval(3_600_000)
|
||||||
const savedPlaylist = await this.repository.save(playlist);
|
async checkActivePlaylists(): Promise<void> {
|
||||||
this.io.emit('playlistNew', savedPlaylist);
|
const activePlaylists = await this.findAll({}, { active: true });
|
||||||
return savedPlaylist;
|
for (const playlist of activePlaylists) {
|
||||||
}
|
let details;
|
||||||
|
try {
|
||||||
async update(id: number, playlist: Partial<PlaylistEntity>): Promise<void> {
|
details = await getDetails(playlist.spotifyUrl);
|
||||||
await this.repository.update(id, playlist);
|
} catch (err) {
|
||||||
const dbPlaylist = await this.findOne(id);
|
await this.update(playlist.id, { ...playlist, error: String(err) });
|
||||||
this.io.emit('playlistUpdate', dbPlaylist);
|
}
|
||||||
}
|
this.createPlaylistFolderStructure(playlist.name);
|
||||||
|
for (const track of details?.tracks ?? []) {
|
||||||
async retryFailedOfPlaylist(id: number): Promise<void> {
|
const track2Save = {
|
||||||
const tracks = await this.trackService.getAllByPlaylist(id);
|
artist: track.artist,
|
||||||
for(let track of tracks) {
|
name: track.name,
|
||||||
if (track.status === TrackStatusEnum.Error) {
|
spotifyUrl: track.previewUrl,
|
||||||
await this.trackService.retry(track.id);
|
};
|
||||||
}
|
const isExist = !!(
|
||||||
}
|
await this.trackService.findAll({
|
||||||
}
|
...track2Save,
|
||||||
|
playlist: { id: playlist.id },
|
||||||
private createPlaylistFolderStructure(playlistName): void {
|
})
|
||||||
const folderName = resolve(__dirname, '..',this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH), playlistName);
|
).length;
|
||||||
!fs.existsSync(folderName) && fs.mkdirSync(folderName);
|
if (!isExist) {
|
||||||
}
|
await this.trackService.create(track2Save, playlist);
|
||||||
|
|
||||||
@Interval(3600000)
|
|
||||||
async checkActivePlaylists(): Promise<void> {
|
|
||||||
const activePlaylists = await this.findAll({}, {active: true});
|
|
||||||
for (let playlist of activePlaylists) {
|
|
||||||
let details;
|
|
||||||
try {
|
|
||||||
details = await getDetails(playlist.spotifyUrl);
|
|
||||||
} catch (err) {
|
|
||||||
await this.update(playlist.id, {...playlist, error: String(err)});
|
|
||||||
}
|
|
||||||
this.createPlaylistFolderStructure(playlist.name);
|
|
||||||
for(let track of details?.tracks ?? []) {
|
|
||||||
const track2Save = {
|
|
||||||
artist: track.artist,
|
|
||||||
name: track.name,
|
|
||||||
spotifyUrl: track.previewUrl,
|
|
||||||
}
|
|
||||||
const isExist = !!(await this.trackService.findAll({...track2Save, playlist: {id: playlist.id}})).length;
|
|
||||||
if(!isExist) {
|
|
||||||
await this.trackService.create(track2Save, playlist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UtilsService } from './utils.service';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
providers: [UtilsService],
|
||||||
|
controllers: [],
|
||||||
|
exports: [UtilsService],
|
||||||
|
})
|
||||||
|
export class SharedModule {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { EnviromentEnum } from '../enviroment.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UtilsService {
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
getPlaylistFolderPath(name: string): string {
|
||||||
|
return resolve(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH),
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,14 @@ 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';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([TrackEntity]), ConfigModule],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([TrackEntity]),
|
||||||
|
ConfigModule,
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
providers: [TrackService],
|
providers: [TrackService],
|
||||||
controllers: [TrackController],
|
controllers: [TrackController],
|
||||||
exports: [TrackService],
|
exports: [TrackService],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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';
|
||||||
|
import {UtilsService} from "../shared/utils.service";
|
||||||
|
|
||||||
enum WsTrackOperation {
|
enum WsTrackOperation {
|
||||||
New = 'trackNew',
|
New = 'trackNew',
|
||||||
@@ -30,6 +31,7 @@ export class TrackService {
|
|||||||
@InjectRepository(TrackEntity)
|
@InjectRepository(TrackEntity)
|
||||||
private repository: Repository<TrackEntity>,
|
private repository: Repository<TrackEntity>,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly utilsService: UtilsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
findAll(
|
findAll(
|
||||||
@@ -153,10 +155,7 @@ export class TrackService {
|
|||||||
|
|
||||||
getFolderName(track: TrackEntity, playlist: PlaylistEntity): string {
|
getFolderName(track: TrackEntity, playlist: PlaylistEntity): string {
|
||||||
return resolve(
|
return resolve(
|
||||||
__dirname,
|
this.utilsService.getPlaylistFolderPath(playlist.name),
|
||||||
'..',
|
|
||||||
this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH),
|
|
||||||
playlist.name,
|
|
||||||
this.getTrackFileName(track),
|
this.getTrackFileName(track),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user