elf integration
This commit is contained in:
Generated
+67
-1
@@ -8,7 +8,17 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"spooty-be",
|
"spooty-be",
|
||||||
"spooty-fe"
|
"spooty-fe"
|
||||||
]
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@ngneat/elf": "^2.5.1",
|
||||||
|
"@ngneat/elf-cli-ng": "^1.0.0",
|
||||||
|
"@ngneat/elf-devtools": "^1.3.0",
|
||||||
|
"@ngneat/elf-entities": "^5.0.2",
|
||||||
|
"@ngneat/elf-pagination": "^1.1.0",
|
||||||
|
"@ngneat/elf-persist-state": "^1.2.1",
|
||||||
|
"@ngneat/elf-requests": "^1.9.2",
|
||||||
|
"@ngneat/elf-state-history": "^1.4.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
@@ -4603,6 +4613,62 @@
|
|||||||
"typeorm": "^0.3.0"
|
"typeorm": "^0.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ngneat/elf": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf/-/elf-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-13BItNZFgHglTiXuP9XhisNczwQ5QSzH+imAv9nAPsdbCq/3ortqkIYRnlxB8DGPVcuIjLujQ4OcZa+9QWgZtw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"rxjs": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-cli-ng": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-cli-ng/-/elf-cli-ng-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-twAvWL5tEtBy/Q65kqoa5aaSE4kG1sFGLZH7QPJwLEBuFayDIzyhT/swJQVLZc+Dv6M4cdHpAyLAg3OfeuWd+Q=="
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-devtools": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-devtools/-/elf-devtools-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-J9+4Vk/S/nKFGnXGZUDYrIx/K8Jfv4TLpzR3voBSOtSeq+c8Q0hdmo8iW7oaK6y5FWFDk0VJktlEh9cjylYxFg=="
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-entities": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-entities/-/elf-entities-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-G4ag51lvM3tOSgpxVVFYAgsh/bOL5BkNb4Z0VtosaM/CTWTHoNrf8UdvcaeJ3+sP1RS3bmEdZ9xUE8ifnVxssA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ngneat/elf": ">=2.5.0",
|
||||||
|
"rxjs": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-pagination": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-pagination/-/elf-pagination-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MhaeHxRXlLMDg4SIidWS/USr82l8tJ5/QKl8JbOTWDu6YAygVb2pwlXITPFG0Nk2dusRTt5/P2JcGdcxQK6//Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ngneat/elf-entities": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-persist-state": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-persist-state/-/elf-persist-state-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-R+5IRLC35cDT403sSs37UeqqOpHNnCwHl3eicPv/Rc+JJ7Av1bcQClSF2mHC0jE4pkYmEuVghpzUntvmrKCCwA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"rxjs": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-requests": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-requests/-/elf-requests-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-DSlUVGlltkxcCjH0lWIjSJLz7SAR6ZdGtVwQumip7pXdxeSYmYFnqIwDObR4Cbd3Jbmx0jQs+C9VSbgTDhtdLA=="
|
||||||
|
},
|
||||||
|
"node_modules/@ngneat/elf-state-history": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngneat/elf-state-history/-/elf-state-history-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-tjKGVfd96npOyxQUgMSzfeybtRxSlvCcIWCeMTeWOfwYyPO2Z3FjGIzDupDUZdV0MY/6jWOiCxP4aNhj0z6YRQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ngneat/elf-entities": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ngtools/webpack": {
|
"node_modules/@ngtools/webpack": {
|
||||||
"version": "18.0.5",
|
"version": "18.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.5.tgz",
|
||||||
|
|||||||
+6
-2
@@ -1,12 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "spooty",
|
"name": "spooty",
|
||||||
"workspaces": ["spooty-be", "spooty-fe"],
|
"workspaces": [
|
||||||
|
"spooty-be",
|
||||||
|
"spooty-fe"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start:be": "npm run start:dev -w spooty-be",
|
"start:be": "npm run start:dev -w spooty-be",
|
||||||
"start:fe": "npm run start -w spooty-fe",
|
"start:fe": "npm run start -w spooty-fe",
|
||||||
"build:be": "npm run build -w spooty-be",
|
"build:be": "npm run build -w spooty-be",
|
||||||
"build:fe": "npm run build -w spooty-fe",
|
"build:fe": "npm run build -w spooty-fe",
|
||||||
"build": "npm run build:be npm run build:fe",
|
"build": "npm run build:be npm run build:fe",
|
||||||
|
"gen:fe": "npm run gen -w spooty-fe",
|
||||||
"start": "npm run start:prod -w spooty-be"
|
"start": "npm run start:prod -w spooty-be"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ export class PlaylistController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() playlist: PlaylistModel): any {
|
async create(@Body() playlist: PlaylistModel): Promise<void> {
|
||||||
getDetails(playlist.spotifyUrl).then(async details => {
|
await getDetails(playlist.spotifyUrl).then(async details => {
|
||||||
const savedPlaylist = await this.service.create({...playlist, name: details.preview.title});
|
const savedPlaylist = await this.service.create({...playlist, name: details.preview.title});
|
||||||
console.log(savedPlaylist);
|
|
||||||
details.tracks.forEach(track => {
|
details.tracks.forEach(track => {
|
||||||
this.trackService.create({
|
this.trackService.create({
|
||||||
artist: track.artist,
|
artist: track.artist,
|
||||||
@@ -32,6 +31,5 @@ export class PlaylistController {
|
|||||||
}, savedPlaylist);
|
}, savedPlaylist);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
//return {url: url.url};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-1
@@ -6,7 +6,8 @@
|
|||||||
"start": "ng serve --proxy-config proxy.conf.json",
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test",
|
||||||
|
"gen": "ng generate"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -19,6 +20,14 @@
|
|||||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||||
"@angular/router": "^18.0.0",
|
"@angular/router": "^18.0.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
|
"@ngneat/elf": "^2.5.1",
|
||||||
|
"@ngneat/elf-cli-ng": "^1.0.0",
|
||||||
|
"@ngneat/elf-devtools": "^1.3.0",
|
||||||
|
"@ngneat/elf-entities": "^5.0.2",
|
||||||
|
"@ngneat/elf-pagination": "^1.1.0",
|
||||||
|
"@ngneat/elf-persist-state": "^1.2.1",
|
||||||
|
"@ngneat/elf-requests": "^1.9.2",
|
||||||
|
"@ngneat/elf-state-history": "^1.4.0",
|
||||||
"bulma": "^1.0.1",
|
"bulma": "^1.0.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<section class="hero is-primary">
|
<section class="hero is-primary">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<p class="title">
|
<p class="title">
|
||||||
<span><i class="fa-solid fa-music"></i> Spooty</span>
|
<i class="fa-brands fa-spotify"></i>
|
||||||
|
<span>Spooty</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="subtitle"><i class="fa-solid fa-house"></i> Self-hosted <i class="fa-brands fa-spotify"></i> spotify <i class="fa-solid fa-download"></i>downloader</p>
|
<p class="subtitle">Self-hosted spotify downloader</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
<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" (click)="download()">
|
<button class="button is-primary" [class.is-loading]="(createLoading$ | async)?.isLoading" (click)="download()">
|
||||||
<i class="fa-solid fa-download"></i> Download
|
<i class="fa-solid fa-download"></i> Download
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,18 +22,25 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="is-flex is-align-items-center">
|
<div class="is-flex is-align-items-center">
|
||||||
<p class="subtitle">List</p>
|
<p class="subtitle">List</p>
|
||||||
<button class="button" (click)="get()"><i class="fa-solid fa-arrows-rotate"></i> Refresh</button>
|
<i class="fa-solid fa-arrows-rotate is-clickable" [class.loading-animation]="(loading$ | async)?.isLoading" (click)="fetchPlaylists()"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="panel is-primary" *ngFor="let playlist of playlists">
|
<article class="panel is-primary" *ngFor="let playlist of playlists$ | async">
|
||||||
<p class="panel-heading is-flex is-justify-content-space-between">
|
<p class="panel-heading is-flex is-justify-content-space-between">
|
||||||
<span><i class="fa-solid fa-caret-down"></i> {{playlist.name}}</span>
|
<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>
|
<a [href]="playlist.spotifyUrl" target="_blank" class="is-color-black"><i class="fa-brands fa-spotify"></i></a>
|
||||||
</p>
|
</p>
|
||||||
|
<ng-container *ngIf="playlist.collapsed">
|
||||||
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of playlist.tracks">
|
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of playlist.tracks">
|
||||||
<div>
|
<div>
|
||||||
<a href="http://localhost:3000/track/download/{{track.id}}" class="panel-icon is-color-info" download>
|
<a href="api/track/download/{{track.id}}" class="panel-icon is-color-info" download>
|
||||||
<i class="fa-solid fa-download"></i>
|
<i class="fa-solid fa-download"></i>
|
||||||
</a>
|
</a>
|
||||||
<span>{{track.artist}} - {{track.song}}</span>
|
<span>{{track.artist}} - {{track.song}}</span>
|
||||||
@@ -51,6 +59,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
</ng-container>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
.panel-heading {
|
.panel-heading {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-block {
|
.panel-block {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-animation {
|
||||||
|
animation: spin 0.3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform:rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import {HttpClient} from "@angular/common/http";
|
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import {NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
|
import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
|
||||||
|
import {PlaylistService} from "./services/playlist.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase],
|
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase],
|
||||||
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: any[] = [];
|
playlists$ = this.playlistService.all$;
|
||||||
tracks = [
|
loading$ = this.playlistService.loading$;
|
||||||
{code: 'CC', name: 'Aaaa'},
|
createLoading$ = this.playlistService.createLoading$;
|
||||||
{code: 'CrC', name: 'Aaaadfdfd'},
|
|
||||||
]
|
|
||||||
|
|
||||||
constructor(private readonly http: HttpClient) {
|
constructor(private readonly playlistService: PlaylistService) {
|
||||||
this.get();
|
this.fetchPlaylists();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPlaylists(): void {
|
||||||
|
this.playlistService.fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
download(): void {
|
download(): void {
|
||||||
this.http.post('/api/playlist', {spotifyUrl: this.url}).subscribe(() => this.get());
|
this.playlistService.create(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): void {
|
toggleCollapse(playlistId: number): void {
|
||||||
this.http.get('/api/playlist').subscribe(data => this.playlists = data as any);
|
this.playlistService.toggleCollapsed(playlistId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PlaylistService } from './playlist.service';
|
||||||
|
|
||||||
|
describe('PlaylistService', () => {
|
||||||
|
let service: PlaylistService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(PlaylistService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {createStore} from "@ngneat/elf";
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {
|
||||||
|
addEntities,
|
||||||
|
selectAllEntities, selectEntities,
|
||||||
|
setEntities,
|
||||||
|
UIEntitiesRef,
|
||||||
|
unionEntities, updateEntities,
|
||||||
|
withEntities,
|
||||||
|
withUIEntities
|
||||||
|
} from "@ngneat/elf-entities";
|
||||||
|
import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests";
|
||||||
|
import {tap} from "rxjs";
|
||||||
|
|
||||||
|
const STORE_NAME = 'playlist';
|
||||||
|
const ENDPOINT = '/api/playlist';
|
||||||
|
const CREATE_LOADING = 'CREATE_LOADING';
|
||||||
|
|
||||||
|
export interface Playlist {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
spotifyUrl: string;
|
||||||
|
tracks?: any[]; //todo fix it
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlaylistUi {
|
||||||
|
id: number,
|
||||||
|
collapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PlaylistService {
|
||||||
|
|
||||||
|
private store = createStore(
|
||||||
|
{ name: STORE_NAME },
|
||||||
|
withEntities<Playlist>(),
|
||||||
|
withUIEntities<PlaylistUi>()
|
||||||
|
);
|
||||||
|
all$ = this.store.combine({
|
||||||
|
entities: this.store.pipe(selectAllEntities()),
|
||||||
|
UIEntities: this.store.pipe(selectEntities({ ref: UIEntitiesRef })),
|
||||||
|
}).pipe(unionEntities());
|
||||||
|
|
||||||
|
loading$ = this.store.pipe(joinRequestResult([STORE_NAME]));
|
||||||
|
createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' }));
|
||||||
|
|
||||||
|
constructor(private readonly http: HttpClient) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(): void {
|
||||||
|
this.http.get<Playlist[]>(ENDPOINT).pipe(
|
||||||
|
tap((data: Playlist[]) => this.store.update(
|
||||||
|
setEntities(data),
|
||||||
|
setEntities(data.map(item => ({id: item.id, collapsed: false})), {ref: UIEntitiesRef})
|
||||||
|
)),
|
||||||
|
trackRequestResult([STORE_NAME], { skipCache: true }),
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(spotifyUrl: string): void {
|
||||||
|
this.http.post(ENDPOINT, {spotifyUrl}).pipe(
|
||||||
|
trackRequestResult([CREATE_LOADING], { skipCache: true })
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCollapsed(id: number): void {
|
||||||
|
this.store.update(updateEntities(id, old => ({...old, collapsed: !old.collapsed}), { ref: UIEntitiesRef }))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { AppComponent } from './app/app.component';
|
import { AppComponent } from './app/app.component';
|
||||||
|
import { devTools } from '@ngneat/elf-devtools';
|
||||||
|
|
||||||
bootstrapApplication(AppComponent, appConfig)
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
devTools();
|
||||||
|
|||||||
Reference in New Issue
Block a user