From 117110fae9cf15f6ef8076699d50018176b98b1f Mon Sep 17 00:00:00 2001 From: raiper34 Date: Mon, 30 Sep 2024 13:55:08 +0200 Subject: [PATCH] refactor(playlist-backend): refactor playlist backend code --- src/backend/src/enviroment.enum.ts | 10 +- .../src/playlist/playlist.controller.ts | 65 +++--- src/backend/src/playlist/playlist.entity.ts | 33 ++- src/backend/src/playlist/playlist.model.ts | 9 - src/backend/src/playlist/playlist.module.ts | 30 +-- src/backend/src/playlist/playlist.service.ts | 208 ++++++++++-------- src/backend/src/shared/shared.module.ts | 11 + src/backend/src/shared/utils.service.ts | 18 ++ src/backend/src/track/track.module.ts | 7 +- src/backend/src/track/track.service.ts | 7 +- 10 files changed, 222 insertions(+), 176 deletions(-) delete mode 100644 src/backend/src/playlist/playlist.model.ts create mode 100644 src/backend/src/shared/shared.module.ts create mode 100644 src/backend/src/shared/utils.service.ts diff --git a/src/backend/src/enviroment.enum.ts b/src/backend/src/enviroment.enum.ts index 48c0656..c66a005 100644 --- a/src/backend/src/enviroment.enum.ts +++ b/src/backend/src/enviroment.enum.ts @@ -1,6 +1,6 @@ export enum EnviromentEnum { - DB_PATH = 'DB_PATH', - FE_PATH = 'FE_PATH', - DOWNLOADS_PATH = 'DOWNLOADS_PATH', - FORMAT = 'FORMAT', -} \ No newline at end of file + DB_PATH = 'DB_PATH', + FE_PATH = 'FE_PATH', + DOWNLOADS_PATH = 'DOWNLOADS_PATH', + FORMAT = 'FORMAT', +} diff --git a/src/backend/src/playlist/playlist.controller.ts b/src/backend/src/playlist/playlist.controller.ts index e7556bf..5e5d784 100644 --- a/src/backend/src/playlist/playlist.controller.ts +++ b/src/backend/src/playlist/playlist.controller.ts @@ -1,37 +1,44 @@ -import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common'; -import {PlaylistService} from "./playlist.service"; -import {PlaylistModel} from "./playlist.model"; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, +} from '@nestjs/common'; +import { PlaylistService } from './playlist.service'; +import { PlaylistEntity } from './playlist.entity'; @Controller('playlist') export class PlaylistController { + constructor(private readonly service: PlaylistService) {} - constructor( - private readonly service: PlaylistService, - ) { - } + @Get() + getAll(): Promise { + return this.service.findAll(); + } - @Get() - getAll(): Promise { - return this.service.findAll(); - } + @Post() + async create(@Body() playlist: PlaylistEntity): Promise { + await this.service.create(playlist); + } - @Post() - async create(@Body() playlist: PlaylistModel): Promise { - await this.service.create(playlist); - } + @Put(':id') + update( + @Param('id') id: number, + @Body() playlist: Partial, + ): Promise { + return this.service.update(id, playlist); + } - @Put(':id') - update(@Param('id') id: number, @Body() playlist: Partial): Promise { - return this.service.update(id, playlist); - } + @Delete(':id') + remove(@Param('id') id: number): Promise { + return this.service.remove(id); + } - @Delete(':id') - remove(@Param('id') id: number): Promise { - return this.service.remove(id); - } - - @Get('retry/:id') - retryFailedOfPlaylist(@Param('id') id: number): Promise { - return this.service.retryFailedOfPlaylist(id); - } -} \ No newline at end of file + @Get('retry/:id') + retryFailedOfPlaylist(@Param('id') id: number): Promise { + return this.service.retryFailedOfPlaylist(id); + } +} diff --git a/src/backend/src/playlist/playlist.entity.ts b/src/backend/src/playlist/playlist.entity.ts index 79f3eb1..03702cb 100644 --- a/src/backend/src/playlist/playlist.entity.ts +++ b/src/backend/src/playlist/playlist.entity.ts @@ -1,27 +1,26 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; -import {TrackEntity} from "../track/track.entity"; +import { TrackEntity } from '../track/track.entity'; @Entity() export class PlaylistEntity { + @PrimaryGeneratedColumn() + id: number; - @PrimaryGeneratedColumn() - id: number; + @Column({ nullable: true }) + name?: string; - @Column({ nullable: true }) - name?: string; + @Column() + spotifyUrl: string; - @Column() - spotifyUrl: string; + @Column({ nullable: true }) + error?: string; - @Column({ nullable: true }) - error?: string; + @Column({ default: false }) + active?: boolean; - @Column({default: false}) - active?: boolean; + @Column({ default: () => Date.now() }) + createdAt?: number; - @Column({default: () => Date.now()}) - createdAt?: number; - - @OneToMany(() => TrackEntity, track => track.playlist) - tracks?: TrackEntity[]; -} \ No newline at end of file + @OneToMany(() => TrackEntity, (track) => track.playlist) + tracks?: TrackEntity[]; +} diff --git a/src/backend/src/playlist/playlist.model.ts b/src/backend/src/playlist/playlist.model.ts deleted file mode 100644 index 6fbd956..0000000 --- a/src/backend/src/playlist/playlist.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface PlaylistModel { - id: number; - name?: string; - spotifyUrl: string; - tracks?: any[]; //todo fix it - error?: string; - createdAt?: number; - active?: boolean; -} \ No newline at end of file diff --git a/src/backend/src/playlist/playlist.module.ts b/src/backend/src/playlist/playlist.module.ts index 4095f71..8080409 100644 --- a/src/backend/src/playlist/playlist.module.ts +++ b/src/backend/src/playlist/playlist.module.ts @@ -1,19 +1,21 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import {PlaylistEntity} from "./playlist.entity"; -import {PlaylistService} from "./playlist.service"; -import {PlaylistController} from "./playlist.controller"; -import {TrackModule} from "../track/track.module"; -import {ConfigModule} from "@nestjs/config"; +import { PlaylistEntity } from './playlist.entity'; +import { PlaylistService } from './playlist.service'; +import { PlaylistController } from './playlist.controller'; +import { TrackModule } from '../track/track.module'; +import { ConfigModule } from '@nestjs/config'; +import { SharedModule } from '../shared/shared.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([PlaylistEntity]), - ConfigModule, - TrackModule, - ], - providers: [PlaylistService], - controllers: [PlaylistController], - exports: [PlaylistService] + imports: [ + TypeOrmModule.forFeature([PlaylistEntity]), + ConfigModule, + TrackModule, + SharedModule, + ], + providers: [PlaylistService], + controllers: [PlaylistController], + exports: [PlaylistService], }) -export class PlaylistModule {} \ No newline at end of file +export class PlaylistModule {} diff --git a/src/backend/src/playlist/playlist.service.ts b/src/backend/src/playlist/playlist.service.ts index 2c9f199..141f720 100644 --- a/src/backend/src/playlist/playlist.service.ts +++ b/src/backend/src/playlist/playlist.service.ts @@ -1,113 +1,127 @@ -import {Injectable} from '@nestjs/common'; -import {InjectRepository} from '@nestjs/typeorm'; -import {Repository} from 'typeorm'; -import {PlaylistEntity} from "./playlist.entity"; -import {TrackService} from "../track/track.service"; -import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets"; -import {Server} from "socket.io"; -import {resolve} from "path"; -import {ConfigService} from "@nestjs/config"; -import {EnviromentEnum} from "../enviroment.enum"; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PlaylistEntity } from './playlist.entity'; +import { TrackService } from '../track/track.service'; +import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; +import { Server } from 'socket.io'; import * as fs from 'fs'; -import {Interval} from "@nestjs/schedule"; -import {TrackStatusEnum} from "../track/track.entity"; +import { Interval } from '@nestjs/schedule'; +import { TrackStatusEnum } from '../track/track.entity'; +import { UtilsService } from '../shared/utils.service'; 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() @Injectable() export class PlaylistService { + @WebSocketServer() io: Server; - @WebSocketServer() io: Server; + constructor( + @InjectRepository(PlaylistEntity) + private repository: Repository, + private readonly trackService: TrackService, + private readonly utilsService: UtilsService, + ) {} - constructor( - @InjectRepository(PlaylistEntity) - private repository: Repository, - private readonly trackService: TrackService, - private readonly configService: ConfigService, - ) {} + findAll( + relations: Record = { tracks: true }, + where?: Partial, + ): Promise { + return this.repository.find({ where, relations }); + } - findAll(relations: Record = {tracks: true}, where?: Partial): Promise { - return this.repository.find({where, relations}); + findOne(id: number): Promise { + return this.repository.findOneBy({ id }); + } + + async remove(id: number): Promise { + await this.repository.delete(id); + this.io.emit(WsPlaylistOperation.Delete, { id }); + } + + async create(playlist: PlaylistEntity): Promise { + let details; + let playlist2Save: PlaylistEntity; + try { + details = await getDetails(playlist.spotifyUrl); + playlist2Save = { ...playlist, name: details.preview.title }; + } catch (err) { + playlist2Save = { ...playlist, error: String(err) }; } - - findOne(id: number): Promise { - return this.repository.findOneBy({ id }); + const savedPlaylist = await this.save(playlist2Save); + this.createPlaylistFolderStructure(savedPlaylist.name); + 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 { - await this.repository.delete(id); - this.io.emit('playlistDelete', {id}); + async save(playlist: PlaylistEntity): Promise { + const savedPlaylist = await this.repository.save(playlist); + this.io.emit(WsPlaylistOperation.New, savedPlaylist); + return savedPlaylist; + } + + async update(id: number, playlist: Partial): Promise { + await this.repository.update(id, playlist); + const dbPlaylist = await this.findOne(id); + this.io.emit(WsPlaylistOperation.Update, dbPlaylist); + } + + async retryFailedOfPlaylist(id: number): Promise { + 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 { - 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); - this.createPlaylistFolderStructure(savedPlaylist.name); - for(let track of details?.tracks ?? []) { - await this.trackService.create({ - artist: track.artist, - name: track.name, - spotifyUrl: track.previewUrl, - }, savedPlaylist); + private createPlaylistFolderStructure(playlistName: string): void { + const playlistPath = this.utilsService.getPlaylistFolderPath(playlistName); + !fs.existsSync(playlistPath) && fs.mkdirSync(playlistPath); + } + + @Interval(3_600_000) + async checkActivePlaylists(): Promise { + const activePlaylists = await this.findAll({}, { active: true }); + for (const 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 (const 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); } + } } - - async save(playlist: PlaylistEntity): Promise { - const savedPlaylist = await this.repository.save(playlist); - this.io.emit('playlistNew', savedPlaylist); - return savedPlaylist; - } - - async update(id: number, playlist: Partial): Promise { - await this.repository.update(id, playlist); - const dbPlaylist = await this.findOne(id); - this.io.emit('playlistUpdate', dbPlaylist); - } - - async retryFailedOfPlaylist(id: number): Promise { - const tracks = await this.trackService.getAllByPlaylist(id); - for(let track of tracks) { - if (track.status === TrackStatusEnum.Error) { - await this.trackService.retry(track.id); - } - } - } - - private createPlaylistFolderStructure(playlistName): void { - const folderName = resolve(__dirname, '..',this.configService.get(EnviromentEnum.DOWNLOADS_PATH), playlistName); - !fs.existsSync(folderName) && fs.mkdirSync(folderName); - } - - @Interval(3600000) - async checkActivePlaylists(): Promise { - 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); - } - } - } - } -} \ No newline at end of file + } +} diff --git a/src/backend/src/shared/shared.module.ts b/src/backend/src/shared/shared.module.ts new file mode 100644 index 0000000..9570821 --- /dev/null +++ b/src/backend/src/shared/shared.module.ts @@ -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 {} diff --git a/src/backend/src/shared/utils.service.ts b/src/backend/src/shared/utils.service.ts new file mode 100644 index 0000000..dc9b50c --- /dev/null +++ b/src/backend/src/shared/utils.service.ts @@ -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(EnviromentEnum.DOWNLOADS_PATH), + name, + ); + } +} diff --git a/src/backend/src/track/track.module.ts b/src/backend/src/track/track.module.ts index 5c5544a..cefcd0b 100644 --- a/src/backend/src/track/track.module.ts +++ b/src/backend/src/track/track.module.ts @@ -4,9 +4,14 @@ import { TrackEntity } from './track.entity'; import { TrackService } from './track.service'; import { TrackController } from './track.controller'; import { ConfigModule } from '@nestjs/config'; +import { SharedModule } from '../shared/shared.module'; @Module({ - imports: [TypeOrmModule.forFeature([TrackEntity]), ConfigModule], + imports: [ + TypeOrmModule.forFeature([TrackEntity]), + ConfigModule, + SharedModule, + ], providers: [TrackService], controllers: [TrackController], exports: [TrackService], diff --git a/src/backend/src/track/track.service.ts b/src/backend/src/track/track.service.ts index 103f8e3..66bd689 100644 --- a/src/backend/src/track/track.service.ts +++ b/src/backend/src/track/track.service.ts @@ -13,6 +13,7 @@ import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server } from 'socket.io'; import * as ffmpeg from 'fluent-ffmpeg'; import { EnviromentEnum } from '../enviroment.enum'; +import {UtilsService} from "../shared/utils.service"; enum WsTrackOperation { New = 'trackNew', @@ -30,6 +31,7 @@ export class TrackService { @InjectRepository(TrackEntity) private repository: Repository, private readonly configService: ConfigService, + private readonly utilsService: UtilsService, ) {} findAll( @@ -153,10 +155,7 @@ export class TrackService { getFolderName(track: TrackEntity, playlist: PlaylistEntity): string { return resolve( - __dirname, - '..', - this.configService.get(EnviromentEnum.DOWNLOADS_PATH), - playlist.name, + this.utilsService.getPlaylistFolderPath(playlist.name), this.getTrackFileName(track), ); }