playlist subscription functionality

This commit is contained in:
raiper34
2024-07-21 16:44:50 +02:00
parent 57da7c7410
commit c365d8be90
8 changed files with 76 additions and 13 deletions
@@ -1,4 +1,4 @@
import {Body, Controller, Delete, Get, Param, Post} from '@nestjs/common';
import {Body, Controller, Delete, Get, Param, Post, Put} from '@nestjs/common';
import {PlaylistService} from "./playlist.service";
import {PlaylistModel} from "./playlist.model";
@@ -20,6 +20,11 @@ export class PlaylistController {
await this.service.create(playlist);
}
@Put(':id')
update(@Param('id') id: number, @Body() playlist: Partial<PlaylistModel>): Promise<void> {
return this.service.update(id, playlist);
}
@Delete(':id')
remove(@Param('id') id: number): Promise<void> {
return this.service.remove(id);
@@ -16,6 +16,9 @@ export class PlaylistEntity {
@Column({ nullable: true })
error?: string;
@Column({default: false})
active?: boolean;
@Column({default: () => Date.now()})
createdAt?: number;
@@ -5,4 +5,5 @@ export interface PlaylistModel {
tracks?: any[]; //todo fix it
error?: string;
createdAt?: number;
active?: boolean;
}
+44 -8
View File
@@ -10,6 +10,7 @@ import {resolve} from "path";
import {ConfigService} from "@nestjs/config";
import {EnviromentEnum} from "../enviroment.enum";
import * as fs from 'fs';
import {Interval} from "@nestjs/schedule";
const fetch = require('isomorphic-unfetch');
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
@@ -26,8 +27,8 @@ export class PlaylistService {
private readonly configService: ConfigService,
) {}
findAll(): Promise<PlaylistEntity[]> {
return this.repository.find({relations: {tracks: true}});
findAll(relations: Record<string, boolean> = {tracks: true}, where?: Partial<PlaylistEntity>): Promise<PlaylistEntity[]> {
return this.repository.find({where, relations});
}
findOne(id: number): Promise<PlaylistEntity | null> {
@@ -48,10 +49,8 @@ export class PlaylistService {
} catch (err) {
playlist2Save = {...playlist, error: String(err)};
}
const savedPlaylist = await this.repository.save(playlist2Save);
this.io.emit('playlistNew', savedPlaylist);
const folderName = resolve(__dirname, '..',this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH), savedPlaylist.name);
!fs.existsSync(folderName) && fs.mkdirSync(folderName);
const savedPlaylist = await this.save(playlist2Save);
this.createPlaylistFolderStructure(savedPlaylist.name);
for(let track of details?.tracks ?? []) {
await this.trackService.create({
artist: track.artist,
@@ -61,9 +60,16 @@ export class PlaylistService {
}
}
async update(id: number, playlist: PlaylistEntity): Promise<void> {
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);
this.io.emit('trackPlaylist', playlist);
const dbPlaylist = await this.findOne(id);
this.io.emit('playlistUpdate', dbPlaylist);
}
async retryFailedOfPlaylist(id: number): Promise<void> {
@@ -74,4 +80,34 @@ export class PlaylistService {
}
}
}
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,
song: 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);
}
}
}
}
}
+2 -2
View File
@@ -28,7 +28,7 @@ export class TrackService {
private readonly configService: ConfigService,
) {}
findAll(where?: Partial<TrackEntity>, relations: string[] = []): Promise<TrackEntity[]> {
findAll(where?: {[key: string]: any}, relations: Record<string, boolean> = {}): Promise<TrackEntity[]> {
return this.repository.find({where, relations});
}
@@ -82,7 +82,7 @@ export class TrackService {
@Interval(1000)
async download() {
const queuedTracks = await this.findAll({status: TrackStatusEnum.Queued}, ['playlist']);
const queuedTracks = await this.findAll({status: TrackStatusEnum.Queued}, {playlist: true});
for (let track of queuedTracks) {
await this.update(track.id, {...track, status: TrackStatusEnum.Downloading});
}
@@ -16,6 +16,11 @@
</a>
</span>
<span>
<i class="fa-solid is-clickable hover-icon"
[ngClass]="playlist.active ? 'fa-lock' : 'fa-lock-open'"
[title]="playlist.active ? 'Subscribe to playlist changes' : 'Unsubscribe from playlist changes'"
(click)="setActive(playlist.id, playlist.active)"
></i>&nbsp;
<i class="fa-solid fa-repeat is-clickable hover-icon" title="Retry download failed tracks" (click)="retryFailed(playlist.id)"></i>&nbsp;
<i class="fa-solid fa-xmark is-clickable hover-icon" title="Remove playlist from list" (click)="delete(playlist.id)"></i>&nbsp;
<span *ngIf="!playlist.error" class="is-size-6 has-text-weight-medium">{{trackCompletedCount$ | async}}/{{trackCount$ | async}}</span>&nbsp;
@@ -57,4 +57,8 @@ export class PlaylistBoxComponent {
retryFailed(id: number): void {
this.service.retryFailed(id);
}
setActive(id: number, currentActive: boolean = false): void {
this.service.setActive(id, !currentActive)
}
}
@@ -26,6 +26,7 @@ export interface Playlist {
name?: string;
spotifyUrl: string;
error?: string;
active: boolean;
createdAt: number;
}
@@ -55,7 +56,7 @@ export class PlaylistService {
all$ = this.store.combine({
entities: this.store.pipe(selectAllEntities()),
UIEntities: this.store.pipe(selectEntities({ ref: UIEntitiesRef })),
}).pipe(unionEntities(), map(data => data.sort((a, b) => b.createdAt - a.createdAt)));
}).pipe(unionEntities(), map(data => data.sort((a, b) => this.sort(a, b))));
loading$ = this.store.pipe(joinRequestResult([STORE_NAME]));
createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' }));
@@ -100,7 +101,7 @@ export class PlaylistService {
if (playlist?.error || errorCount === trackCount) {
return PlaylistStatusEnum.Error;
} else if (trackCount === completedCount) {
return PlaylistStatusEnum.Completed;
return playlist?.active ? PlaylistStatusEnum.Subscribed : PlaylistStatusEnum.Completed;
} else if (errorCount > 1) {
return PlaylistStatusEnum.Warning;
}
@@ -157,4 +158,12 @@ export class PlaylistService {
retryFailed(id: number): void {
this.http.get<void>(`${ENDPOINT}/retry/${id}`).subscribe();
}
setActive(id: number, active: boolean): void {
this.http.put<void>(`${ENDPOINT}/${id}`, {active}).subscribe();
}
private sort(a: Playlist & PlaylistUi, b: Playlist & PlaylistUi): number {
return a.active === b.active ? (b.createdAt - a.createdAt) : (a.active < b.active ? 1 : -1);
}
}