clear playlist mass operations and retry functionality
This commit is contained in:
@@ -24,4 +24,9 @@ export class PlaylistController {
|
|||||||
remove(@Param('id') id: number): Promise<void> {
|
remove(@Param('id') id: number): Promise<void> {
|
||||||
return this.service.remove(id);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('retry/:id')
|
||||||
|
retryFailedOfPlaylist(@Param('id') id: number): Promise<void> {
|
||||||
|
return this.service.retryFailedOfPlaylist(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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 {TrackStatusEnum} from "../track/track.model";
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -57,4 +59,13 @@ export class PlaylistService {
|
|||||||
await this.repository.update(id, playlist);
|
await this.repository.update(id, playlist);
|
||||||
this.io.emit('trackPlaylist', playlist);
|
this.io.emit('trackPlaylist', playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -35,4 +35,9 @@ export class TrackController {
|
|||||||
remove(@Param('id') id: number): Promise<void> {
|
remove(@Param('id') id: number): Promise<void> {
|
||||||
return this.service.remove(id);
|
return this.service.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('retry/:id')
|
||||||
|
retry(@Param('id') id: number): Promise<void> {
|
||||||
|
return this.service.retry(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,13 @@ import {PlaylistEntity} from "../playlist/playlist.entity";
|
|||||||
import {Interval} from "@nestjs/schedule";
|
import {Interval} from "@nestjs/schedule";
|
||||||
import {TrackStatusEnum} from "./track.model";
|
import {TrackStatusEnum} from "./track.model";
|
||||||
import * as yts from 'yt-search';
|
import * as yts from 'yt-search';
|
||||||
|
import {SearchResult} from 'yt-search';
|
||||||
import * as ytdl from '@distube/ytdl-core';
|
import * as ytdl from '@distube/ytdl-core';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {ConfigService} from "@nestjs/config";
|
import {ConfigService} from "@nestjs/config";
|
||||||
import {resolve} from "path";
|
import {resolve} from "path";
|
||||||
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
||||||
import {Server} from "socket.io";
|
import {Server} from "socket.io";
|
||||||
import {SearchResult} from "yt-search";
|
|
||||||
|
|
||||||
@WebSocketGateway()
|
@WebSocketGateway()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -53,6 +53,11 @@ export class TrackService {
|
|||||||
this.io.emit('trackUpdate', track);
|
this.io.emit('trackUpdate', track);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async retry(id: number): Promise<void> {
|
||||||
|
const track = await this.findOne(id);
|
||||||
|
await this.update(id, {...track, status: TrackStatusEnum.New});
|
||||||
|
}
|
||||||
|
|
||||||
@Interval(1000)
|
@Interval(1000)
|
||||||
async findOnYoutube() {
|
async findOnYoutube() {
|
||||||
const newTracks = await this.findAll({status: TrackStatusEnum.New});
|
const newTracks = await this.findAll({status: TrackStatusEnum.New});
|
||||||
@@ -66,6 +71,7 @@ export class TrackService {
|
|||||||
youtubeResult = await yts(`${track.artist} - ${track.song}`);
|
youtubeResult = await yts(`${track.artist} - ${track.song}`);
|
||||||
updatedTrack = {...track, youtubeUrl: youtubeResult.videos[0].url, status: TrackStatusEnum.Queued};
|
updatedTrack = {...track, youtubeUrl: youtubeResult.videos[0].url, status: TrackStatusEnum.Queued};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
updatedTrack = {...track, error: String(err), status: TrackStatusEnum.Error};
|
updatedTrack = {...track, error: String(err), status: TrackStatusEnum.Error};
|
||||||
}
|
}
|
||||||
await this.update(track.id, updatedTrack);
|
await this.update(track.id, updatedTrack);
|
||||||
|
|||||||
@@ -25,8 +25,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="is-flex is-align-items-center">
|
<div class="is-flex is-justify-content-space-between">
|
||||||
<p class="subtitle">Playlists</p>
|
<p class="subtitle">Playlists</p>
|
||||||
|
<div class="buttons has-addons">
|
||||||
|
<button class="button is-outlined is-success" title="Remove completed from list" (click)="deleteCompleted()">
|
||||||
|
<i class="fa-solid fa-check"></i>
|
||||||
|
</button>
|
||||||
|
<button class="button is-outlined is-danger" title="Remove failed from list" (click)="deleteFailed()">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-playlist-box *ngFor="let playlist of playlists$ | async" [playlist]="playlist"></app-playlist-box>
|
<app-playlist-box *ngFor="let playlist of playlists$ | async" [playlist]="playlist"></app-playlist-box>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import {RouterOutlet} from '@angular/router';
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {CommonModule, NgFor} from "@angular/common";
|
import {CommonModule, NgFor} from "@angular/common";
|
||||||
import {PlaylistService} from "./services/playlist.service";
|
import {PlaylistService, PlaylistStatusEnum} from "./services/playlist.service";
|
||||||
import {PlaylistBoxComponent} from "./components/playlist-box/playlist-box.component";
|
import {PlaylistBoxComponent} from "./components/playlist-box/playlist-box.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -30,4 +30,12 @@ export class AppComponent {
|
|||||||
this.url && this.playlistService.create(this.url);
|
this.url && this.playlistService.create(this.url);
|
||||||
this.url = '';
|
this.url = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteCompleted(): void {
|
||||||
|
this.playlistService.deleteAllByStatus(PlaylistStatusEnum.Completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFailed(): void {
|
||||||
|
this.playlistService.deleteAllByStatus(PlaylistStatusEnum.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<i class="fa-solid fa-xmark is-clickable hover-icon" title="Remove track from list" (click)="delete(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>
|
||||||
<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>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {AsyncPipe, CommonModule, NgForOf, NgIf} from "@angular/common";
|
import {AsyncPipe, CommonModule, NgForOf, NgIf} from "@angular/common";
|
||||||
import {TrackListComponent} from "../track-list/track-list.component";
|
import {TrackListComponent} from "../track-list/track-list.component";
|
||||||
import {Playlist, PlaylistService, PLaylistStatusEnum, PlaylistUi} from "../../services/playlist.service";
|
import {Playlist, PlaylistService, PlaylistStatusEnum, PlaylistUi} from "../../services/playlist.service";
|
||||||
import {Observable, map} from "rxjs";
|
import {Observable, map} from "rxjs";
|
||||||
|
|
||||||
const STATUS2CLASS = {
|
const STATUS2CLASS = {
|
||||||
[PLaylistStatusEnum.Completed]: 'is-success',
|
[PlaylistStatusEnum.Completed]: 'is-success',
|
||||||
[PLaylistStatusEnum.InProgress]: 'is-info',
|
[PlaylistStatusEnum.InProgress]: 'is-info',
|
||||||
[PLaylistStatusEnum.Warning]: 'is-warning',
|
[PlaylistStatusEnum.Warning]: 'is-warning',
|
||||||
[PLaylistStatusEnum.Error]: 'is-danger',
|
[PlaylistStatusEnum.Error]: 'is-danger',
|
||||||
[PLaylistStatusEnum.Subscribed]: 'is-primary',
|
[PlaylistStatusEnum.Subscribed]: 'is-primary',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -53,4 +53,8 @@ export class PlaylistBoxComponent {
|
|||||||
delete(id: number): void {
|
delete(id: number): void {
|
||||||
this.service.delete(id);
|
this.service.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retryFailed(id: number): void {
|
||||||
|
this.service.retryFailed(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<i *ngIf="track.status === trackStatuses.Error" class="fa-solid fa-repeat hover-icon" title="Retry to download" (click)="retry(track.id)"></i>
|
||||||
<i class="fa-solid fa-xmark hover-icon" title="Remove track from list" (click)="delete(track.id)"></i>
|
<i class="fa-solid fa-xmark hover-icon" title="Remove track from list" (click)="delete(track.id)"></i>
|
||||||
<ng-container [ngSwitch]="track.status">
|
<ng-container [ngSwitch]="track.status">
|
||||||
<span *ngSwitchCase="trackStatuses.New" class="tag is-info">New</span>
|
<span *ngSwitchCase="trackStatuses.New" class="tag is-info">New</span>
|
||||||
|
|||||||
@@ -25,4 +25,8 @@ export class TrackListComponent {
|
|||||||
delete(id: number): void {
|
delete(id: number): void {
|
||||||
this.service.delete(id);
|
this.service.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry(id: number): void {
|
||||||
|
this.service.retry(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
withUIEntities
|
withUIEntities
|
||||||
} from "@ngneat/elf-entities";
|
} from "@ngneat/elf-entities";
|
||||||
import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests";
|
import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests";
|
||||||
import {combineLatest, map, Observable, tap} from "rxjs";
|
import {combineLatest, filter, first, map, Observable, of, switchMap, tap} from "rxjs";
|
||||||
import {Track, TrackService} from "./track.service";
|
import {Track, TrackService} from "./track.service";
|
||||||
import {Socket} from "ngx-socket-io";
|
import {Socket} from "ngx-socket-io";
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export interface PlaylistUi {
|
|||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PLaylistStatusEnum {
|
export enum PlaylistStatusEnum {
|
||||||
InProgress,
|
InProgress,
|
||||||
Completed,
|
Completed,
|
||||||
Warning,
|
Warning,
|
||||||
@@ -90,7 +90,7 @@ export class PlaylistService {
|
|||||||
return this.trackService.getErrorByPlaylist(id).pipe(map(data => data.length));
|
return this.trackService.getErrorByPlaylist(id).pipe(map(data => data.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus$(id: number): Observable<PLaylistStatusEnum> {
|
getStatus$(id: number): Observable<PlaylistStatusEnum> {
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
this.getById(id),
|
this.getById(id),
|
||||||
this.getTrackCount(id),
|
this.getTrackCount(id),
|
||||||
@@ -98,16 +98,33 @@ export class PlaylistService {
|
|||||||
this.getErrorTrackCount(id),
|
this.getErrorTrackCount(id),
|
||||||
]).pipe(map(([playlist, trackCount, completedCount, errorCount]) => {
|
]).pipe(map(([playlist, trackCount, completedCount, errorCount]) => {
|
||||||
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 PlaylistStatusEnum.Completed;
|
||||||
} else if (errorCount > 1) {
|
} else if (errorCount > 1) {
|
||||||
return PLaylistStatusEnum.Warning;
|
return PlaylistStatusEnum.Warning;
|
||||||
}
|
}
|
||||||
return PLaylistStatusEnum.InProgress;
|
return PlaylistStatusEnum.InProgress;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAllByStatus(status: PlaylistStatusEnum): void {
|
||||||
|
this.all$.pipe(
|
||||||
|
first(),
|
||||||
|
switchMap(playlists =>
|
||||||
|
combineLatest(playlists.map(item => this.deleteIfStatusEquals$(item.id, status)))
|
||||||
|
)
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteIfStatusEquals$(id: number, status2Filter: PlaylistStatusEnum): Observable<void> {
|
||||||
|
return combineLatest([of(id), this.getStatus$(id)]).pipe(
|
||||||
|
first(),
|
||||||
|
filter(([_, status]) => status === status2Filter),
|
||||||
|
switchMap(([id]) => this.delete$(id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fetch(): void {
|
fetch(): void {
|
||||||
this.http.get<Playlist[]>(ENDPOINT).pipe(
|
this.http.get<Playlist[]>(ENDPOINT).pipe(
|
||||||
tap((data: Playlist[]) => this.store.update(
|
tap((data: Playlist[]) => this.store.update(
|
||||||
@@ -130,6 +147,14 @@ export class PlaylistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(id: number): void {
|
delete(id: number): void {
|
||||||
this.http.delete(`${ENDPOINT}/${id}`).subscribe();
|
this.delete$(id).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private delete$(id: number): Observable<void> {
|
||||||
|
return this.http.delete<void>(`${ENDPOINT}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
retryFailed(id: number): void {
|
||||||
|
this.http.get<void>(`${ENDPOINT}/retry/${id}`).subscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,8 @@ export class TrackService {
|
|||||||
delete(id: number): void {
|
delete(id: number): void {
|
||||||
this.http.delete(`${ENDPOINT}/${id}`).subscribe();
|
this.http.delete(`${ENDPOINT}/${id}`).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry(id: number): void {
|
||||||
|
this.http.get(`${ENDPOINT}/retry/${id}`).subscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user