refactor(playlist-backend): refactor playlist backend code

This commit is contained in:
raiper34
2024-09-30 13:55:08 +02:00
parent 0e88627ec7
commit 117110fae9
10 changed files with 222 additions and 176 deletions
+5 -5
View File
@@ -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',
} }
+36 -29
View File
@@ -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);
}
}
+16 -17
View File
@@ -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;
}
+16 -14
View File
@@ -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 {}
+111 -97
View File
@@ -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}; @Interval(3_600_000)
} catch (err) { async checkActivePlaylists(): Promise<void> {
playlist2Save = {...playlist, error: String(err)}; const activePlaylists = await this.findAll({}, { active: true });
} for (const playlist of activePlaylists) {
const savedPlaylist = await this.save(playlist2Save); let details;
this.createPlaylistFolderStructure(savedPlaylist.name); try {
for(let track of details?.tracks ?? []) { details = await getDetails(playlist.spotifyUrl);
await this.trackService.create({ } catch (err) {
artist: track.artist, await this.update(playlist.id, { ...playlist, error: String(err) });
name: track.name, }
spotifyUrl: track.previewUrl, this.createPlaylistFolderStructure(playlist.name);
}, savedPlaylist); 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<PlaylistEntity> { }
const savedPlaylist = await this.repository.save(playlist);
this.io.emit('playlistNew', 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('playlistUpdate', dbPlaylist);
}
async retryFailedOfPlaylist(id: number): Promise<void> {
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<string>(EnviromentEnum.DOWNLOADS_PATH), playlistName);
!fs.existsSync(folderName) && fs.mkdirSync(folderName);
}
@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);
}
}
}
}
}
+11
View File
@@ -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 {}
+18
View File
@@ -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,
);
}
}
+6 -1
View File
@@ -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],
+3 -4
View File
@@ -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),
); );
} }