playlist download progress, fix library error downloading
This commit is contained in:
Generated
+103
-19
@@ -2581,6 +2581,33 @@
|
|||||||
"node": ">=10.0.0"
|
"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": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.3.tgz",
|
||||||
@@ -6341,7 +6368,6 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||||
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
@@ -10493,6 +10519,29 @@
|
|||||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||||
"devOptional": true
|
"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": {
|
"node_modules/http-deceiver": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||||
@@ -14673,6 +14722,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": 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": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
@@ -14727,6 +14781,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -15005,8 +15064,7 @@
|
|||||||
"node_modules/requires-port": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
@@ -17162,6 +17220,36 @@
|
|||||||
"node": ">=0.6"
|
"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": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@@ -17646,7 +17734,7 @@
|
|||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.18.0.tgz",
|
||||||
"integrity": "sha512-nT8jjv/fE9Et1ilR6QoW8ingRTY2Pp4l2RUrdzV5Yz35RJDrtPc1DXvuNqcpsJSGIRHFdt3YKKktTzJA6r0fTA==",
|
"integrity": "sha512-nT8jjv/fE9Et1ilR6QoW8ingRTY2Pp4l2RUrdzV5Yz35RJDrtPc1DXvuNqcpsJSGIRHFdt3YKKktTzJA6r0fTA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
}
|
}
|
||||||
@@ -17790,6 +17878,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -19039,19 +19136,6 @@
|
|||||||
"yt-search-video": "bin/mpv_video.sh"
|
"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": {
|
"node_modules/zone.js": {
|
||||||
"version": "0.14.7",
|
"version": "0.14.7",
|
||||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
|
||||||
@@ -19061,6 +19145,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@distube/ytdl-core": "^4.13.5",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^3.2.2",
|
"@nestjs/config": "^3.2.2",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
@@ -19076,8 +19161,7 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"spotify-url-info": "^3.2.15",
|
"spotify-url-info": "^3.2.15",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"yt-search": "^2.11.0",
|
"yt-search": "^2.11.0"
|
||||||
"ytdl-core": "^4.11.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@distube/ytdl-core": "^4.13.5",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/config": "^3.2.2",
|
"@nestjs/config": "^3.2.2",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
@@ -35,8 +36,7 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"spotify-url-info": "^3.2.15",
|
"spotify-url-info": "^3.2.15",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"yt-search": "^2.11.0",
|
"yt-search": "^2.11.0"
|
||||||
"ytdl-core": "^4.11.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ export class PlaylistEntity {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column()
|
@Column({ nullable: true })
|
||||||
name: string;
|
name?: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
spotifyUrl: string;
|
spotifyUrl: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
error?: string;
|
||||||
|
|
||||||
@Column({default: () => Date.now()})
|
@Column({default: () => Date.now()})
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export interface PlaylistModel {
|
export interface PlaylistModel {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name?: string;
|
||||||
spotifyUrl: string;
|
spotifyUrl: string;
|
||||||
tracks?: any[]; //todo fix it
|
tracks?: any[]; //todo fix it
|
||||||
|
error?: string;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
}
|
}
|
||||||
@@ -33,10 +33,17 @@ export class PlaylistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(playlist: PlaylistEntity): Promise<void> {
|
async create(playlist: PlaylistEntity): Promise<void> {
|
||||||
const details = await getDetails(playlist.spotifyUrl);
|
let details;
|
||||||
const savedPlaylist = await this.repository.save({...playlist, name: details.preview.title});
|
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);
|
this.io.emit('playlistNew', savedPlaylist);
|
||||||
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,
|
||||||
song: track.name,
|
song: track.name,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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 * as ytdl from '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";
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
<p class="subtitle">Download</p>
|
<p class="subtitle">Download</p>
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
<input class="input" type="text" [(ngModel)]="url" placeholder="Paste playlist/song/artist url"/>
|
<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
|
<i class="fa-solid fa-download"></i> Download
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,22 +28,7 @@
|
|||||||
<div class="is-flex is-align-items-center">
|
<div class="is-flex is-align-items-center">
|
||||||
<p class="subtitle">Playlists</p>
|
<p class="subtitle">Playlists</p>
|
||||||
</div>
|
</div>
|
||||||
|
<app-playlist-box *ngFor="let playlist of playlists$ | async" [playlist]="playlist"></app-playlist-box>
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
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, NgSwitch, NgSwitchCase} from "@angular/common";
|
import {CommonModule, NgFor} from "@angular/common";
|
||||||
import {PlaylistService} from "./services/playlist.service";
|
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({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase, TrackListComponent],
|
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, PlaylistBoxComponent],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss'
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
|
||||||
url = ''
|
url = ''
|
||||||
playlists$ = this.playlistService.all$;
|
|
||||||
loading$ = this.playlistService.loading$;
|
|
||||||
createLoading$ = this.playlistService.createLoading$;
|
createLoading$ = this.playlistService.createLoading$;
|
||||||
|
playlists$ = this.playlistService.all$;
|
||||||
|
|
||||||
constructor(private readonly playlistService: PlaylistService) {
|
constructor(private readonly playlistService: PlaylistService) {
|
||||||
this.fetchPlaylists();
|
this.fetchPlaylists();
|
||||||
@@ -28,10 +27,7 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
download(): void {
|
download(): void {
|
||||||
this.playlistService.create(this.url);
|
this.url && this.playlistService.create(this.url);
|
||||||
}
|
this.url = '';
|
||||||
|
|
||||||
toggleCollapse(playlistId: number): void {
|
|
||||||
this.playlistService.toggleCollapsed(playlistId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,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 {map, tap} from "rxjs";
|
import {map, Observable, 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";
|
||||||
|
|
||||||
@@ -20,8 +20,9 @@ const CREATE_LOADING = 'CREATE_LOADING';
|
|||||||
|
|
||||||
export interface Playlist {
|
export interface Playlist {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name?: string;
|
||||||
spotifyUrl: string;
|
spotifyUrl: string;
|
||||||
|
error?: string;
|
||||||
createdAt: number;
|
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 {
|
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(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import {createStore} from "@ngneat/elf";
|
import {createStore} from "@ngneat/elf";
|
||||||
import {selectManyByPredicate, upsertEntities, withEntities} from "@ngneat/elf-entities";
|
import {selectManyByPredicate, upsertEntities, withEntities} from "@ngneat/elf-entities";
|
||||||
import {Socket} from "ngx-socket-io";
|
import {Socket} from "ngx-socket-io";
|
||||||
import {tap} from "rxjs";
|
import {map, Observable, tap} from "rxjs";
|
||||||
import {trackRequestResult} from "@ngneat/elf-requests";
|
import {trackRequestResult} from "@ngneat/elf-requests";
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
|
||||||
@@ -39,10 +39,14 @@ export class TrackService {
|
|||||||
withEntities<Track>(),
|
withEntities<Track>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
getAllByPlaylist(id: number) {
|
getAllByPlaylist(id: number): Observable<Track[]> {
|
||||||
return this.store.pipe(selectManyByPredicate(({playlistId}) => playlistId === id))
|
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(
|
constructor(
|
||||||
private readonly http: HttpClient,
|
private readonly http: HttpClient,
|
||||||
private readonly socket: Socket,
|
private readonly socket: Socket,
|
||||||
|
|||||||
Reference in New Issue
Block a user