elf integration
This commit is contained in:
Generated
+67
-1
@@ -8,7 +8,17 @@
|
||||
"workspaces": [
|
||||
"spooty-be",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
@@ -4603,6 +4613,62 @@
|
||||
"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": {
|
||||
"version": "18.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.0.5.tgz",
|
||||
|
||||
+5
-1
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "spooty",
|
||||
"workspaces": ["spooty-be", "spooty-fe"],
|
||||
"workspaces": [
|
||||
"spooty-be",
|
||||
"spooty-fe"
|
||||
],
|
||||
"scripts": {
|
||||
"start:be": "npm run start:dev -w spooty-be",
|
||||
"start:fe": "npm run start -w spooty-fe",
|
||||
"build:be": "npm run build -w spooty-be",
|
||||
"build:fe": "npm run build -w spooty-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"
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,9 @@ export class PlaylistController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() playlist: PlaylistModel): any {
|
||||
getDetails(playlist.spotifyUrl).then(async details => {
|
||||
async create(@Body() playlist: PlaylistModel): Promise<void> {
|
||||
await getDetails(playlist.spotifyUrl).then(async details => {
|
||||
const savedPlaylist = await this.service.create({...playlist, name: details.preview.title});
|
||||
console.log(savedPlaylist);
|
||||
details.tracks.forEach(track => {
|
||||
this.trackService.create({
|
||||
artist: track.artist,
|
||||
@@ -32,6 +31,5 @@ export class PlaylistController {
|
||||
}, savedPlaylist);
|
||||
});
|
||||
});
|
||||
//return {url: url.url};
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -6,7 +6,8 @@
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
"test": "ng test",
|
||||
"gen": "ng generate"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -19,6 +20,14 @@
|
||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||
"@angular/router": "^18.0.0",
|
||||
"@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",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<section class="hero is-primary">
|
||||
<div class="hero-body">
|
||||
<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 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>
|
||||
</section>
|
||||
|
||||
@@ -13,7 +14,7 @@
|
||||
<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" (click)="download()">
|
||||
<button class="button is-primary" [class.is-loading]="(createLoading$ | async)?.isLoading" (click)="download()">
|
||||
<i class="fa-solid fa-download"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
@@ -21,18 +22,25 @@
|
||||
<hr>
|
||||
<div class="box">
|
||||
<div class="is-flex is-align-items-center">
|
||||
<p class="subtitle">List</p>
|
||||
<button class="button" (click)="get()"><i class="fa-solid fa-arrows-rotate"></i> Refresh</button>
|
||||
<p class="subtitle">List</p>
|
||||
<i class="fa-solid fa-arrows-rotate is-clickable" [class.loading-animation]="(loading$ | async)?.isLoading" (click)="fetchPlaylists()"></i>
|
||||
</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">
|
||||
<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>
|
||||
</p>
|
||||
<ng-container *ngIf="playlist.collapsed">
|
||||
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of playlist.tracks">
|
||||
<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>
|
||||
</a>
|
||||
<span>{{track.artist}} - {{track.song}}</span>
|
||||
@@ -51,6 +59,7 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
.panel-heading {
|
||||
padding: 5px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
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 { RouterOutlet } from '@angular/router';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
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({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase],
|
||||
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
export class AppComponent {
|
||||
|
||||
url = ''
|
||||
playlists: any[] = [];
|
||||
tracks = [
|
||||
{code: 'CC', name: 'Aaaa'},
|
||||
{code: 'CrC', name: 'Aaaadfdfd'},
|
||||
]
|
||||
playlists$ = this.playlistService.all$;
|
||||
loading$ = this.playlistService.loading$;
|
||||
createLoading$ = this.playlistService.createLoading$;
|
||||
|
||||
constructor(private readonly http: HttpClient) {
|
||||
this.get();
|
||||
constructor(private readonly playlistService: PlaylistService) {
|
||||
this.fetchPlaylists();
|
||||
}
|
||||
|
||||
fetchPlaylists(): void {
|
||||
this.playlistService.fetch();
|
||||
}
|
||||
|
||||
download(): void {
|
||||
this.http.post('/api/playlist', {spotifyUrl: this.url}).subscribe(() => this.get());
|
||||
this.playlistService.create(this.url);
|
||||
}
|
||||
|
||||
get(): void {
|
||||
this.http.get('/api/playlist').subscribe(data => this.playlists = data as any);
|
||||
toggleCollapse(playlistId: number): void {
|
||||
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 { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { devTools } from '@ngneat/elf-devtools';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
devTools();
|
||||
|
||||
Reference in New Issue
Block a user