playlist subscription functionality
This commit is contained in:
@@ -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 {PlaylistService} from "./playlist.service";
|
||||||
import {PlaylistModel} from "./playlist.model";
|
import {PlaylistModel} from "./playlist.model";
|
||||||
|
|
||||||
@@ -20,6 +20,11 @@ export class PlaylistController {
|
|||||||
await this.service.create(playlist);
|
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')
|
@Delete(':id')
|
||||||
remove(@Param('id') id: number): Promise<void> {
|
remove(@Param('id') id: number): Promise<void> {
|
||||||
return this.service.remove(id);
|
return this.service.remove(id);
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export class PlaylistEntity {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
||||||
|
@Column({default: false})
|
||||||
|
active?: boolean;
|
||||||
|
|
||||||
@Column({default: () => Date.now()})
|
@Column({default: () => Date.now()})
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export interface PlaylistModel {
|
|||||||
tracks?: any[]; //todo fix it
|
tracks?: any[]; //todo fix it
|
||||||
error?: string;
|
error?: string;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
|
active?: boolean;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import {resolve} from "path";
|
|||||||
import {ConfigService} from "@nestjs/config";
|
import {ConfigService} from "@nestjs/config";
|
||||||
import {EnviromentEnum} from "../enviroment.enum";
|
import {EnviromentEnum} from "../enviroment.enum";
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import {Interval} from "@nestjs/schedule";
|
||||||
const fetch = require('isomorphic-unfetch');
|
const fetch = require('isomorphic-unfetch');
|
||||||
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
|
const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info')(fetch);
|
||||||
|
|
||||||
@@ -26,8 +27,8 @@ export class PlaylistService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
findAll(): Promise<PlaylistEntity[]> {
|
findAll(relations: Record<string, boolean> = {tracks: true}, where?: Partial<PlaylistEntity>): Promise<PlaylistEntity[]> {
|
||||||
return this.repository.find({relations: {tracks: true}});
|
return this.repository.find({where, relations});
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number): Promise<PlaylistEntity | null> {
|
findOne(id: number): Promise<PlaylistEntity | null> {
|
||||||
@@ -48,10 +49,8 @@ export class PlaylistService {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
playlist2Save = {...playlist, error: String(err)};
|
playlist2Save = {...playlist, error: String(err)};
|
||||||
}
|
}
|
||||||
const savedPlaylist = await this.repository.save(playlist2Save);
|
const savedPlaylist = await this.save(playlist2Save);
|
||||||
this.io.emit('playlistNew', savedPlaylist);
|
this.createPlaylistFolderStructure(savedPlaylist.name);
|
||||||
const folderName = resolve(__dirname, '..',this.configService.get<string>(EnviromentEnum.DOWNLOADS_PATH), savedPlaylist.name);
|
|
||||||
!fs.existsSync(folderName) && fs.mkdirSync(folderName);
|
|
||||||
for(let track of details?.tracks ?? []) {
|
for(let track of details?.tracks ?? []) {
|
||||||
await this.trackService.create({
|
await this.trackService.create({
|
||||||
artist: track.artist,
|
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);
|
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> {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export class TrackService {
|
|||||||
private readonly configService: ConfigService,
|
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});
|
return this.repository.find({where, relations});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ export class TrackService {
|
|||||||
|
|
||||||
@Interval(1000)
|
@Interval(1000)
|
||||||
async download() {
|
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) {
|
for (let track of queuedTracks) {
|
||||||
await this.update(track.id, {...track, status: TrackStatusEnum.Downloading});
|
await this.update(track.id, {...track, status: TrackStatusEnum.Downloading});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<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>
|
||||||
<i class="fa-solid fa-repeat is-clickable hover-icon" title="Retry download failed tracks" (click)="retryFailed(playlist.id)"></i>
|
<i class="fa-solid fa-repeat is-clickable hover-icon" title="Retry download failed tracks" (click)="retryFailed(playlist.id)"></i>
|
||||||
<i class="fa-solid fa-xmark is-clickable hover-icon" title="Remove playlist from list" (click)="delete(playlist.id)"></i>
|
<i class="fa-solid fa-xmark is-clickable hover-icon" title="Remove playlist from list" (click)="delete(playlist.id)"></i>
|
||||||
<span *ngIf="!playlist.error" class="is-size-6 has-text-weight-medium">{{trackCompletedCount$ | async}}/{{trackCount$ | async}}</span>
|
<span *ngIf="!playlist.error" class="is-size-6 has-text-weight-medium">{{trackCompletedCount$ | async}}/{{trackCount$ | async}}</span>
|
||||||
|
|||||||
@@ -57,4 +57,8 @@ export class PlaylistBoxComponent {
|
|||||||
retryFailed(id: number): void {
|
retryFailed(id: number): void {
|
||||||
this.service.retryFailed(id);
|
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;
|
name?: string;
|
||||||
spotifyUrl: string;
|
spotifyUrl: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
active: boolean;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export class PlaylistService {
|
|||||||
all$ = this.store.combine({
|
all$ = this.store.combine({
|
||||||
entities: this.store.pipe(selectAllEntities()),
|
entities: this.store.pipe(selectAllEntities()),
|
||||||
UIEntities: this.store.pipe(selectEntities({ ref: UIEntitiesRef })),
|
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]));
|
loading$ = this.store.pipe(joinRequestResult([STORE_NAME]));
|
||||||
createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' }));
|
createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' }));
|
||||||
@@ -100,7 +101,7 @@ export class PlaylistService {
|
|||||||
if (playlist?.error || errorCount === trackCount) {
|
if (playlist?.error || errorCount === trackCount) {
|
||||||
return PlaylistStatusEnum.Error;
|
return PlaylistStatusEnum.Error;
|
||||||
} else if (trackCount === completedCount) {
|
} else if (trackCount === completedCount) {
|
||||||
return PlaylistStatusEnum.Completed;
|
return playlist?.active ? PlaylistStatusEnum.Subscribed : PlaylistStatusEnum.Completed;
|
||||||
} else if (errorCount > 1) {
|
} else if (errorCount > 1) {
|
||||||
return PlaylistStatusEnum.Warning;
|
return PlaylistStatusEnum.Warning;
|
||||||
}
|
}
|
||||||
@@ -157,4 +158,12 @@ export class PlaylistService {
|
|||||||
retryFailed(id: number): void {
|
retryFailed(id: number): void {
|
||||||
this.http.get<void>(`${ENDPOINT}/retry/${id}`).subscribe();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user