websockets for tracks and ui improvement

This commit is contained in:
raiper34
2024-07-10 23:08:05 +02:00
parent 3455db8d71
commit 2f0e4626f8
22 changed files with 389 additions and 101 deletions
+2
View File
@@ -24,9 +24,11 @@
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/schedule": "^4.0.2",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.3.10",
"@types/yt-search": "^2.10.3",
"isomorphic-unfetch": "^4.0.2",
"reflect-metadata": "^0.2.0",
@@ -13,6 +13,9 @@ export class PlaylistEntity {
@Column()
spotifyUrl: string;
@Column({default: () => Date.now()})
createdAt?: number;
@OneToMany(() => TrackEntity, track => track.playlist)
tracks?: TrackEntity[];
}
+1
View File
@@ -3,4 +3,5 @@ export interface PlaylistModel {
name: string;
spotifyUrl: string;
tracks?: any[]; //todo fix it
createdAt?: number;
}
@@ -8,6 +8,7 @@ const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info
@Injectable()
export class PlaylistService {
constructor(
@InjectRepository(PlaylistEntity)
private repository: Repository<PlaylistEntity>,
+4 -20
View File
@@ -1,7 +1,6 @@
import {Body, Controller, Delete, Get, Param, Post, Put, Res, StreamableFile} from '@nestjs/common';
import {Controller, Get, Param, Res, StreamableFile} from '@nestjs/common';
import {TrackService} from "./track.service";
import {TrackModel} from "./track.model";
import {TrackEntity} from "./track.entity";
import {createReadStream} from "fs";
import { resolve } from 'path';
import type { Response } from 'express';
@@ -19,24 +18,9 @@ export class TrackController {
return this.service.findAll();
}
@Get(':id')
get(@Param('id') id: number): Promise<TrackModel> {
return this.service.findOne(id);
}
@Post()
create(@Body() track: TrackModel): Promise<TrackModel> {
return this.service.create(track as TrackEntity);
}
@Put(':id')
update(@Param('id') id: number, @Body() track: TrackModel): Promise<void> {
return this.service.update(id, track as TrackEntity);
}
@Delete(':id')
delete(@Param('id') id: number): Promise<void> {
return this.service.remove(id);
@Get('playlist/:id')
getAllByPlaylist(@Param('id') playlistId: number): Promise<TrackModel[]> {
return this.service.getAllByPlaylist(playlistId);
}
@Get('download/:id')
+7 -1
View File
@@ -1,4 +1,4 @@
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import {Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn} from 'typeorm';
import {PlaylistEntity} from "../playlist/playlist.entity";
import {TrackStatusEnum} from "./track.model";
@@ -23,6 +23,12 @@ export class TrackEntity {
@Column({default: TrackStatusEnum.New})
status?: TrackStatusEnum;
@Column({ nullable: true })
error?: string;
@Column({default: Date.now()})
createdAt?: number;
@ManyToOne(() => PlaylistEntity, playlist => playlist.tracks)
playlist?: PlaylistEntity;
}
+5
View File
@@ -8,10 +8,15 @@ export interface TrackModel {
youtubeUrl?: string;
status?: TrackStatusEnum,
playlist?: PlaylistModel;
createdAt?: number;
error?: string;
}
export enum TrackStatusEnum {
New,
Searching,
Queued,
Downloading,
Completed,
Error,
}
+51 -16
View File
@@ -10,9 +10,16 @@ import * as ytdl from '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 {SearchResult} from "yt-search";
@WebSocketGateway()
@Injectable()
export class TrackService {
@WebSocketServer() io: Server;
constructor(
@InjectRepository(TrackEntity)
private repository: Repository<TrackEntity>,
@@ -23,6 +30,10 @@ export class TrackService {
return this.repository.find({where: criteria});
}
getAllByPlaylist(id: number): Promise<TrackEntity[]> {
return this.repository.find({where: {playlist: {id}}});
}
findOne(id: number): Promise<TrackEntity | null> {
return this.repository.findOneBy({ id });
}
@@ -31,41 +42,65 @@ export class TrackService {
await this.repository.delete(id);
}
async create(track: TrackEntity, playlist?: PlaylistEntity): Promise<TrackEntity> {
return this.repository.save({...track, playlist});
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});
}
async update(id: number, track: TrackEntity): Promise<void> {
await this.repository.update(id, track);
this.io.emit('track', track);
}
@Interval(10000)
@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) {
const youtubeResult = await yts(`${track.artist} - ${track.song}`);
await this.update(track.id, {
...track,
youtubeUrl: youtubeResult.videos[0].url,
status: TrackStatusEnum.Queued
});
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) {
updatedTrack = {...track, error: String(err), status: TrackStatusEnum.Error};
}
await this.update(track.id, updatedTrack);
}
}
@Interval(15000)
@Interval(1000)
async download() {
const queuedTracks = await this.findAll({status: TrackStatusEnum.Queued});
for (let track of queuedTracks) {
await this.update(track.id, {...track, status: TrackStatusEnum.Downloading});
}
for(let track of queuedTracks) {
await this.youtubeDownload(track);
await this.update(track.id, {...track, status: TrackStatusEnum.Completed})
let error: string;
try {
await this.youtubeDownload(track);
} 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): Promise<void> {
return new Promise((res, _reject) =>
ytdl(track.youtubeUrl, {quality: "highestaudio", filter: "audioonly"}).pipe(
fs.createWriteStream(resolve(__dirname, '..', this.configService.get<string>('DOWNLOADS'), `${track.artist} - ${track.song.replace('/', '')}.mp3`))
.on('finish', () => res())
return new Promise((res, reject) =>
ytdl(track.youtubeUrl, {quality: "highestaudio", filter: "audioonly"})
.on('error', (err) => reject(err)).pipe(
fs.createWriteStream(resolve(__dirname, '..', this.configService.get<string>('DOWNLOADS'), `${track.artist} - ${track.song.replace('/', '')}.mp3`))
.on('finish', () => res())
.on('error', (err) => reject(err))
)
);
}