playlist download progress, fix library error downloading

This commit is contained in:
raiper34
2024-07-13 15:51:28 +02:00
parent 80a562c5cd
commit 06a8608f7c
15 changed files with 244 additions and 77 deletions
+103 -19
View File
@@ -2581,6 +2581,33 @@
"node": ">=10.0.0"
}
},
"node_modules/@distube/ytdl-core": {
"version": "4.13.5",
"resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.13.5.tgz",
"integrity": "sha512-g+4UJIR/auAJbia7iB0aWvaJDbs22P53NySWa47b1NT4xMTDJYguxHFArPrvRkcJrb/AgKjv/XoSZGghpL0CJA==",
"dependencies": {
"http-cookie-agent": "^6.0.5",
"m3u8stream": "^0.8.6",
"miniget": "^4.2.3",
"sax": "^1.4.1",
"tough-cookie": "^4.1.4",
"undici": "^6.19.2"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/distubejs/ytdl-core?sponsor"
}
},
"node_modules/@distube/ytdl-core/node_modules/undici": {
"version": "6.19.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz",
"integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==",
"engines": {
"node": ">=18.17"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.3.tgz",
@@ -6341,7 +6368,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4"
},
@@ -10493,6 +10519,29 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"devOptional": true
},
"node_modules/http-cookie-agent": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-6.0.5.tgz",
"integrity": "sha512-sfZ8fDgDP3B1YB+teqSnAK1aPgBu8reUUGxSsndP2XnYN6cM29EURXWXZqQQiaRdor3B4QjpkUNfv21syaO4DA==",
"dependencies": {
"agent-base": "^7.1.1"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/3846masa"
},
"peerDependencies": {
"tough-cookie": "^4.0.0",
"undici": "^5.11.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"undici": {
"optional": true
}
}
},
"node_modules/http-deceiver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
@@ -14673,6 +14722,11 @@
"dev": true,
"optional": true
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -14727,6 +14781,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -15005,8 +15064,7 @@
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/resolve": {
"version": "1.22.8",
@@ -17162,6 +17220,36 @@
"node": ">=0.6"
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -17646,7 +17734,7 @@
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.18.0.tgz",
"integrity": "sha512-nT8jjv/fE9Et1ilR6QoW8ingRTY2Pp4l2RUrdzV5Yz35RJDrtPc1DXvuNqcpsJSGIRHFdt3YKKktTzJA6r0fTA==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=18.17"
}
@@ -17790,6 +17878,15 @@
"node": ">=6"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -19039,19 +19136,6 @@
"yt-search-video": "bin/mpv_video.sh"
}
},
"node_modules/ytdl-core": {
"version": "4.11.5",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.5.tgz",
"integrity": "sha512-27LwsW4n4nyNviRCO1hmr8Wr5J1wLLMawHCQvH8Fk0hiRqrxuIu028WzbJetiYH28K8XDbeinYW4/wcHQD1EXA==",
"dependencies": {
"m3u8stream": "^0.8.6",
"miniget": "^4.2.2",
"sax": "^1.1.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/zone.js": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
@@ -19061,6 +19145,7 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@distube/ytdl-core": "^4.13.5",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
@@ -19076,8 +19161,7 @@
"rxjs": "^7.8.1",
"spotify-url-info": "^3.2.15",
"sqlite3": "^5.1.7",
"yt-search": "^2.11.0",
"ytdl-core": "^4.11.5"
"yt-search": "^2.11.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
+2 -2
View File
@@ -20,6 +20,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@distube/ytdl-core": "^4.13.5",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
@@ -35,8 +36,7 @@
"rxjs": "^7.8.1",
"spotify-url-info": "^3.2.15",
"sqlite3": "^5.1.7",
"yt-search": "^2.11.0",
"ytdl-core": "^4.11.5"
"yt-search": "^2.11.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
+5 -2
View File
@@ -7,12 +7,15 @@ export class PlaylistEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ nullable: true })
name?: string;
@Column()
spotifyUrl: string;
@Column({ nullable: true })
error?: string;
@Column({default: () => Date.now()})
createdAt?: number;
+2 -1
View File
@@ -1,7 +1,8 @@
export interface PlaylistModel {
id: number;
name: string;
name?: string;
spotifyUrl: string;
tracks?: any[]; //todo fix it
error?: string;
createdAt?: number;
}
+10 -3
View File
@@ -33,10 +33,17 @@ export class PlaylistService {
}
async create(playlist: PlaylistEntity): Promise<void> {
const details = await getDetails(playlist.spotifyUrl);
const savedPlaylist = await this.repository.save({...playlist, name: details.preview.title});
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.repository.save(playlist2Save);
this.io.emit('playlistNew', savedPlaylist);
for(let track of details.tracks) {
for(let track of details?.tracks ?? []) {
await this.trackService.create({
artist: track.artist,
song: track.name,
+1 -1
View File
@@ -6,7 +6,7 @@ import {PlaylistEntity} from "../playlist/playlist.entity";
import {Interval} from "@nestjs/schedule";
import {TrackStatusEnum} from "./track.model";
import * as yts from 'yt-search';
import * as ytdl from 'ytdl-core';
import * as ytdl from '@distube/ytdl-core';
import * as fs from 'fs';
import {ConfigService} from "@nestjs/config";
import {resolve} from "path";
+6 -17
View File
@@ -14,7 +14,11 @@
<p class="subtitle">Download</p>
<div class="is-flex">
<input class="input" type="text" [(ngModel)]="url" placeholder="Paste playlist/song/artist url"/>
<button class="button is-primary" [class.is-loading]="(createLoading$ | async)?.isLoading" (click)="download()">
<button class="button is-primary"
[class.is-loading]="(createLoading$ | async)?.isLoading"
(click)="download()"
[disabled]="!url"
>
<i class="fa-solid fa-download"></i> Download
</button>
</div>
@@ -24,22 +28,7 @@
<div class="is-flex is-align-items-center">
<p class="subtitle">Playlists</p>&nbsp;
</div>
<article class="panel is-primary" *ngFor="let playlist of playlists$ | async">
<p class="panel-heading is-flex is-justify-content-space-between">
<span>
<i class="is-clickable fa-solid"
[ngClass]="playlist.collapsed ? 'fa-caret-up' : 'fa-caret-down'"
(click)="toggleCollapse(playlist.id)"
></i>&nbsp;
<span>{{playlist.name}}</span>
</span>
<a [href]="playlist.spotifyUrl" target="_blank" class="is-color-black"><i class="fa-brands fa-spotify"></i></a>
</p>
<ng-container *ngIf="playlist.collapsed">
<app-track-list [playlistId]="playlist.id"></app-track-list>
</ng-container>
</article>
<app-playlist-box *ngFor="let playlist of playlists$ | async" [playlist]="playlist"></app-playlist-box>
</div>
</div>
</section>
-18
View File
@@ -1,18 +0,0 @@
.panel-heading {
padding: 5px 10px;
border-radius: 10px;
}
.panel {
margin: 5px 0;
}
.loading-animation {
animation: spin 0.3s linear infinite;
}
@keyframes spin {
100% {
transform:rotate(360deg);
}
}
+6 -10
View File
@@ -1,23 +1,22 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {FormsModule} from "@angular/forms";
import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
import {CommonModule, NgFor} from "@angular/common";
import {PlaylistService} from "./services/playlist.service";
import {TrackListComponent} from "./components/track-list/track-list.component";
import {PlaylistBoxComponent} from "./components/playlist-box/playlist-box.component";
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase, TrackListComponent],
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, PlaylistBoxComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
url = ''
playlists$ = this.playlistService.all$;
loading$ = this.playlistService.loading$;
createLoading$ = this.playlistService.createLoading$;
playlists$ = this.playlistService.all$;
constructor(private readonly playlistService: PlaylistService) {
this.fetchPlaylists();
@@ -28,10 +27,7 @@ export class AppComponent {
}
download(): void {
this.playlistService.create(this.url);
}
toggleCollapse(playlistId: number): void {
this.playlistService.toggleCollapsed(playlistId);
this.url && this.playlistService.create(this.url);
this.url = '';
}
}
@@ -0,0 +1,21 @@
<article class="panel" [ngClass]="playlist.error ? 'is-danger' : 'is-primary'">
<p class="panel-heading is-flex is-justify-content-space-between">
<span>
<i class="is-clickable fa-solid"
[ngClass]="playlist.collapsed ? 'fa-caret-up' : 'fa-caret-down'"
(click)="toggleCollapse(playlist.id)"
></i>&nbsp;
<span *ngIf="playlist.error; else noErrorTemplate" class="is-size-6 has-text-weight-medium">{{playlist.error}}</span>
<ng-template #noErrorTemplate>
<span class="is-size-6 has-text-weight-semibold">{{playlist.name}}</span>
</ng-template>&nbsp;
<a [href]="playlist.spotifyUrl" target="_blank" class="is-color-black"><i class="fa-brands fa-spotify"></i></a>
</span>
<span *ngIf="!playlist.error">
<span class="is-size-6 has-text-weight-medium">{{trackCompletedCount$ | async}}/{{trackCount$ | async}}</span>&nbsp;
</span>
</p>
<ng-container *ngIf="playlist.collapsed">
<app-track-list [playlistId]="playlist.id"></app-track-list>
</ng-container>
</article>
@@ -0,0 +1,8 @@
.panel-heading {
padding: 5px 10px;
border-radius: 10px;
}
.panel {
margin: 5px 0;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlaylistBoxComponent } from './playlist-box.component';
describe('PlaylistListComponent', () => {
let component: PlaylistBoxComponent;
let fixture: ComponentFixture<PlaylistBoxComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PlaylistBoxComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PlaylistBoxComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
import {Component, Input} from '@angular/core';
import {AsyncPipe, CommonModule, NgForOf, NgIf} from "@angular/common";
import {TrackListComponent} from "../track-list/track-list.component";
import {Playlist, PlaylistService, PlaylistUi} from "../../services/playlist.service";
import {Observable} from "rxjs";
@Component({
selector: 'app-playlist-box',
standalone: true,
imports: [
CommonModule,
AsyncPipe,
NgForOf,
NgIf,
TrackListComponent
],
templateUrl: './playlist-box.component.html',
styleUrl: './playlist-box.component.scss'
})
export class PlaylistBoxComponent {
@Input() set playlist(val: Playlist & PlaylistUi) {
this._playlist = val;
this.trackCount$ = this.playlistService.getTrackCount(val.id);
this.trackCompletedCount$ = this.playlistService.getCompletedTrackCount(val.id);
}
get playlist(): Playlist & PlaylistUi {
return this._playlist;
}
_playlist!: Playlist & PlaylistUi;
trackCount$!: Observable<number>;
trackCompletedCount$!: Observable<number>;
constructor(private readonly playlistService: PlaylistService) { }
toggleCollapse(playlistId: number): void {
this.playlistService.toggleCollapsed(playlistId);
}
}
+11 -2
View File
@@ -10,7 +10,7 @@ import {
withUIEntities
} from "@ngneat/elf-entities";
import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests";
import {map, tap} from "rxjs";
import {map, Observable, tap} from "rxjs";
import {Track, TrackService} from "./track.service";
import {Socket} from "ngx-socket-io";
@@ -20,8 +20,9 @@ const CREATE_LOADING = 'CREATE_LOADING';
export interface Playlist {
id: number;
name: string;
name?: string;
spotifyUrl: string;
error?: string;
createdAt: number;
}
@@ -61,6 +62,14 @@ export class PlaylistService {
);
}
getTrackCount(id: number): Observable<number> {
return this.trackService.getAllByPlaylist(id).pipe(map(data => data.length));
}
getCompletedTrackCount(id: number): Observable<number> {
return this.trackService.getCompletedByPlaylist(id).pipe(map(data => data.length));
}
fetch(): void {
this.http.get<Playlist[]>(ENDPOINT).pipe(
tap((data: Playlist[]) => this.store.update(
+6 -2
View File
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import {createStore} from "@ngneat/elf";
import {selectManyByPredicate, upsertEntities, withEntities} from "@ngneat/elf-entities";
import {Socket} from "ngx-socket-io";
import {tap} from "rxjs";
import {map, Observable, tap} from "rxjs";
import {trackRequestResult} from "@ngneat/elf-requests";
import {HttpClient} from "@angular/common/http";
@@ -39,10 +39,14 @@ export class TrackService {
withEntities<Track>(),
);
getAllByPlaylist(id: number) {
getAllByPlaylist(id: number): Observable<Track[]> {
return this.store.pipe(selectManyByPredicate(({playlistId}) => playlistId === id))
}
getCompletedByPlaylist(id: number): Observable<Track[]> {
return this.getAllByPlaylist(id).pipe(map(data => data.filter(item => item.status === TrackStatusEnum.Completed)));
}
constructor(
private readonly http: HttpClient,
private readonly socket: Socket,