websockets for tracks and ui improvement
This commit is contained in:
Generated
+130
-27
@@ -8,17 +8,7 @@
|
||||
"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",
|
||||
@@ -4391,6 +4381,24 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/@nestjs/platform-socket.io": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.10.tgz",
|
||||
"integrity": "sha512-LRd+nGWhUu9hND1txCLPZd78Hea+qKJVENb+c9aDU04T24GRjsInDF2RANMR16JLQFcI9mclktDWX4plE95SHg==",
|
||||
"dependencies": {
|
||||
"socket.io": "4.7.5",
|
||||
"tslib": "2.6.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.2.tgz",
|
||||
@@ -4613,6 +4621,28 @@
|
||||
"typeorm": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/websockets": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.10.tgz",
|
||||
"integrity": "sha512-F/fhAC0ylAhjfCZj4Xrgc0yTJ/qltooDCa+fke7BFZLofLmE0yj7WzBVrBHsk/46kppyRcs5XrYjIQLqcDze8g==",
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "3.0.0",
|
||||
"tslib": "2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.0.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
||||
"rxjs": "^7.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/platform-socket.io": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ngneat/elf": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngneat/elf/-/elf-2.5.1.tgz",
|
||||
@@ -5386,8 +5416,7 @@
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@sqltools/formatter": {
|
||||
"version": "1.2.5",
|
||||
@@ -5547,8 +5576,7 @@
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
@@ -5560,7 +5588,6 @@
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -5706,7 +5733,6 @@
|
||||
"version": "20.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
|
||||
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
@@ -6945,7 +6971,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
@@ -8075,6 +8100,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.37.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz",
|
||||
"integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.37.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||
@@ -8748,7 +8783,6 @@
|
||||
"version": "6.5.5",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
|
||||
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -8765,11 +8799,22 @@
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
||||
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
||||
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
@@ -8778,7 +8823,6 @@
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -13281,6 +13325,29 @@
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||
},
|
||||
"node_modules/ngx-socket-io": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-socket-io/-/ngx-socket-io-4.7.0.tgz",
|
||||
"integrity": "sha512-2uwc5OWPOXM2EmiTrsMpWKm97gE6K3Zuzo1JeScncWQ3r/VxLQuGismVXtGF4Dv3h1Orvurt3ISiNztqOuL3gQ==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.0.0",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"socket.io": "^4.7.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^18.0.0",
|
||||
"@angular/core": "^18.0.0",
|
||||
"rxjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-socket-io/node_modules/reflect-metadata": {
|
||||
"version": "0.1.14",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
|
||||
"integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A=="
|
||||
},
|
||||
"node_modules/nice-napi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
|
||||
@@ -13722,6 +13789,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
@@ -15800,7 +15875,6 @@
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
|
||||
"integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
@@ -15818,17 +15892,29 @@
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
|
||||
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
@@ -17568,8 +17654,7 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
"version": "5.0.0",
|
||||
@@ -18842,7 +18927,6 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -18859,6 +18943,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
@@ -18973,9 +19065,11 @@
|
||||
"@nestjs/config": "^3.2.2",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.3.10",
|
||||
"@nestjs/schedule": "^4.0.2",
|
||||
"@nestjs/serve-static": "^4.0.2",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@nestjs/websockets": "^10.3.10",
|
||||
"@types/yt-search": "^2.10.3",
|
||||
"isomorphic-unfetch": "^4.0.2",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
@@ -19021,7 +19115,16 @@
|
||||
"@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",
|
||||
"ngx-socket-io": "^4.7.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.3"
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
"@nestjs/config": "^3.2.2",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.3.10",
|
||||
"@nestjs/schedule": "^4.0.2",
|
||||
"@nestjs/serve-static": "^4.0.2",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@nestjs/websockets": "^10.3.10",
|
||||
"@types/yt-search": "^2.10.3",
|
||||
"isomorphic-unfetch": "^4.0.2",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
|
||||
@@ -13,6 +13,9 @@ export class PlaylistEntity {
|
||||
@Column()
|
||||
spotifyUrl: string;
|
||||
|
||||
@Column({default: () => Date.now()})
|
||||
createdAt?: number;
|
||||
|
||||
@OneToMany(() => TrackEntity, track => track.playlist)
|
||||
tracks?: TrackEntity[];
|
||||
}
|
||||
@@ -3,4 +3,5 @@ export interface PlaylistModel {
|
||||
name: string;
|
||||
spotifyUrl: string;
|
||||
tracks?: any[]; //todo fix it
|
||||
createdAt?: number;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const { getData, getPreview, getTracks, getDetails } = require('spotify-url-info
|
||||
|
||||
@Injectable()
|
||||
export class PlaylistService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(PlaylistEntity)
|
||||
private repository: Repository<PlaylistEntity>,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {Body, Controller, Delete, Get, Param, Post, Put, Res, StreamableFile} from '@nestjs/common';
|
||||
import {Controller, Get, Param, Res, StreamableFile} from '@nestjs/common';
|
||||
import {TrackService} from "./track.service";
|
||||
import {TrackModel} from "./track.model";
|
||||
import {TrackEntity} from "./track.entity";
|
||||
import {createReadStream} from "fs";
|
||||
import { resolve } from 'path';
|
||||
import type { Response } from 'express';
|
||||
@@ -19,24 +18,9 @@ export class TrackController {
|
||||
return this.service.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
get(@Param('id') id: number): Promise<TrackModel> {
|
||||
return this.service.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() track: TrackModel): Promise<TrackModel> {
|
||||
return this.service.create(track as TrackEntity);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(@Param('id') id: number, @Body() track: TrackModel): Promise<void> {
|
||||
return this.service.update(id, track as TrackEntity);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
delete(@Param('id') id: number): Promise<void> {
|
||||
return this.service.remove(id);
|
||||
@Get('playlist/:id')
|
||||
getAllByPlaylist(@Param('id') playlistId: number): Promise<TrackModel[]> {
|
||||
return this.service.getAllByPlaylist(playlistId);
|
||||
}
|
||||
|
||||
@Get('download/:id')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||
import {Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn} from 'typeorm';
|
||||
import {PlaylistEntity} from "../playlist/playlist.entity";
|
||||
import {TrackStatusEnum} from "./track.model";
|
||||
|
||||
@@ -23,6 +23,12 @@ export class TrackEntity {
|
||||
@Column({default: TrackStatusEnum.New})
|
||||
status?: TrackStatusEnum;
|
||||
|
||||
@Column({ nullable: true })
|
||||
error?: string;
|
||||
|
||||
@Column({default: Date.now()})
|
||||
createdAt?: number;
|
||||
|
||||
@ManyToOne(() => PlaylistEntity, playlist => playlist.tracks)
|
||||
playlist?: PlaylistEntity;
|
||||
}
|
||||
@@ -8,10 +8,15 @@ export interface TrackModel {
|
||||
youtubeUrl?: string;
|
||||
status?: TrackStatusEnum,
|
||||
playlist?: PlaylistModel;
|
||||
createdAt?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export enum TrackStatusEnum {
|
||||
New,
|
||||
Searching,
|
||||
Queued,
|
||||
Downloading,
|
||||
Completed,
|
||||
Error,
|
||||
}
|
||||
@@ -10,9 +10,16 @@ import * as ytdl from 'ytdl-core';
|
||||
import * as fs from 'fs';
|
||||
import {ConfigService} from "@nestjs/config";
|
||||
import {resolve} from "path";
|
||||
import {WebSocketGateway, WebSocketServer} from "@nestjs/websockets";
|
||||
import {Server} from "socket.io";
|
||||
import {SearchResult} from "yt-search";
|
||||
|
||||
@WebSocketGateway()
|
||||
@Injectable()
|
||||
export class TrackService {
|
||||
|
||||
@WebSocketServer() io: Server;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TrackEntity)
|
||||
private repository: Repository<TrackEntity>,
|
||||
@@ -23,6 +30,10 @@ export class TrackService {
|
||||
return this.repository.find({where: criteria});
|
||||
}
|
||||
|
||||
getAllByPlaylist(id: number): Promise<TrackEntity[]> {
|
||||
return this.repository.find({where: {playlist: {id}}});
|
||||
}
|
||||
|
||||
findOne(id: number): Promise<TrackEntity | null> {
|
||||
return this.repository.findOneBy({ id });
|
||||
}
|
||||
@@ -31,41 +42,65 @@ export class TrackService {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
async create(track: TrackEntity, playlist?: PlaylistEntity): Promise<TrackEntity> {
|
||||
return this.repository.save({...track, playlist});
|
||||
async create(track: TrackEntity, playlist?: PlaylistEntity): Promise<void> {
|
||||
const savedTrack = await this.repository.save({...track, playlist});
|
||||
this.io.emit('trackNew', {track: savedTrack, playlistId: playlist.id});
|
||||
}
|
||||
|
||||
async update(id: number, track: TrackEntity): Promise<void> {
|
||||
await this.repository.update(id, track);
|
||||
this.io.emit('track', track);
|
||||
}
|
||||
|
||||
@Interval(10000)
|
||||
@Interval(1000)
|
||||
async findOnYoutube() {
|
||||
const newTracks = await this.findAll({status: TrackStatusEnum.New});
|
||||
for (let track of newTracks) {
|
||||
await this.update(track.id, {...track, status: TrackStatusEnum.Searching});
|
||||
}
|
||||
for(let track of newTracks) {
|
||||
const youtubeResult = await yts(`${track.artist} - ${track.song}`);
|
||||
await this.update(track.id, {
|
||||
...track,
|
||||
youtubeUrl: youtubeResult.videos[0].url,
|
||||
status: TrackStatusEnum.Queued
|
||||
});
|
||||
let youtubeResult: SearchResult;
|
||||
let updatedTrack: TrackEntity;
|
||||
try {
|
||||
youtubeResult = await yts(`${track.artist} - ${track.song}`);
|
||||
updatedTrack = {...track, youtubeUrl: youtubeResult.videos[0].url, status: TrackStatusEnum.Queued};
|
||||
} catch (err) {
|
||||
updatedTrack = {...track, error: String(err), status: TrackStatusEnum.Error};
|
||||
}
|
||||
await this.update(track.id, updatedTrack);
|
||||
}
|
||||
}
|
||||
|
||||
@Interval(15000)
|
||||
@Interval(1000)
|
||||
async download() {
|
||||
const queuedTracks = await this.findAll({status: TrackStatusEnum.Queued});
|
||||
for (let track of queuedTracks) {
|
||||
await this.update(track.id, {...track, status: TrackStatusEnum.Downloading});
|
||||
}
|
||||
for(let track of queuedTracks) {
|
||||
let error: string;
|
||||
try {
|
||||
await this.youtubeDownload(track);
|
||||
await this.update(track.id, {...track, status: TrackStatusEnum.Completed})
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
error = String(err);
|
||||
}
|
||||
const updatedTrack = {
|
||||
...track,
|
||||
status: error ? TrackStatusEnum.Error : TrackStatusEnum.Completed,
|
||||
...(error ? {error} : {}),
|
||||
};
|
||||
await this.update(track.id, updatedTrack);
|
||||
}
|
||||
}
|
||||
|
||||
private youtubeDownload(track: TrackEntity): Promise<void> {
|
||||
return new Promise((res, _reject) =>
|
||||
ytdl(track.youtubeUrl, {quality: "highestaudio", filter: "audioonly"}).pipe(
|
||||
return new Promise((res, reject) =>
|
||||
ytdl(track.youtubeUrl, {quality: "highestaudio", filter: "audioonly"})
|
||||
.on('error', (err) => reject(err)).pipe(
|
||||
fs.createWriteStream(resolve(__dirname, '..', this.configService.get<string>('DOWNLOADS'), `${track.artist} - ${track.song.replace('/', '')}.mp3`))
|
||||
.on('finish', () => res())
|
||||
.on('error', (err) => reject(err))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@ngneat/elf-requests": "^1.9.2",
|
||||
"@ngneat/elf-state-history": "^1.4.0",
|
||||
"bulma": "^1.0.1",
|
||||
"ngx-socket-io": "^4.7.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.3"
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
},
|
||||
"/socket.io/*": {
|
||||
"target": "http://localhost:3000/socket.io/",
|
||||
"ws": true,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
<hr>
|
||||
<div class="box">
|
||||
<div class="is-flex is-align-items-center">
|
||||
<p class="subtitle">List</p>
|
||||
<i class="fa-solid fa-arrows-rotate is-clickable" [class.loading-animation]="(loading$ | async)?.isLoading" (click)="fetchPlaylists()"></i>
|
||||
<p class="subtitle">Playlists</p>
|
||||
</div>
|
||||
|
||||
<article class="panel is-primary" *ngFor="let playlist of playlists$ | async">
|
||||
@@ -38,27 +37,7 @@
|
||||
<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="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>
|
||||
<a [href]="track.spotifyUrl" target="_blank" class="is-color-primary margin-left">
|
||||
<i class="fa-brands fa-spotify"></i>
|
||||
</a>
|
||||
<a [href]="track.youtubeUrl" target="_blank" class="is-color-danger is-color-black">
|
||||
<i class="fa-brands fa-youtube"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<ng-container [ngSwitch]="track.status">
|
||||
<span *ngSwitchCase="0" class="tag is-info">New</span>
|
||||
<span *ngSwitchCase="1" class="tag is-warning">Queued</span>
|
||||
<span *ngSwitchCase="2" class="tag is-success">Completed</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</a>
|
||||
<app-track-list [playlistId]="playlist.id"></app-track-list>
|
||||
</ng-container>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { RouterOutlet } from '@angular/router';
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
|
||||
import {PlaylistService} from "./services/playlist.service";
|
||||
import {TrackListComponent} from "./components/track-list/track-list.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase],
|
||||
imports: [CommonModule, RouterOutlet, FormsModule, NgFor, NgSwitch, NgSwitchCase, TrackListComponent],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import {provideHttpClient} from "@angular/common/http";
|
||||
import {SocketIoModule} from "ngx-socket-io";
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
]
|
||||
importProvidersFrom(SocketIoModule.forRoot({url: ''}))
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<a class="panel-block is-flex is-justify-content-space-between" *ngFor="let track of tracks$ | async">
|
||||
<div>
|
||||
<span>{{ track.artist }} - {{ track.song }}</span>
|
||||
<a [href]="track.spotifyUrl" target="_blank" class="is-color-primary margin-left">
|
||||
<i class="fa-brands fa-spotify"></i>
|
||||
</a>
|
||||
<a [href]="track.youtubeUrl" target="_blank" class="is-color-danger is-color-black">
|
||||
<i class="fa-brands fa-youtube"></i>
|
||||
</a>
|
||||
<a *ngIf="track.status === trackStatuses.Completed" href="api/track/download/{{track.id}}" class="is-color-info" download>
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<ng-container [ngSwitch]="track.status">
|
||||
<span *ngSwitchCase="trackStatuses.New" class="tag is-info">New</span>
|
||||
<span *ngSwitchCase="trackStatuses.Searching" class="tag is-warning">Searching</span>
|
||||
<span *ngSwitchCase="trackStatuses.Queued" class="tag is-warning">Queued</span>
|
||||
<span *ngSwitchCase="trackStatuses.Downloading" class="tag is-warning">Downloading</span>
|
||||
<span *ngSwitchCase="trackStatuses.Completed" class="tag is-success">Completed</span>
|
||||
<span *ngSwitchCase="trackStatuses.Error" class="tag is-danger" [title]="track.error">Error</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,3 @@
|
||||
.panel-block {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TrackListComponent } from './track-list.component';
|
||||
|
||||
describe('TrackListComponent', () => {
|
||||
let component: TrackListComponent;
|
||||
let fixture: ComponentFixture<TrackListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TrackListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TrackListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {CommonModule, NgFor, NgSwitch, NgSwitchCase} from "@angular/common";
|
||||
import {Track, TrackService, TrackStatusEnum} from "../../services/track.service";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-track-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgFor, NgSwitch, NgSwitchCase],
|
||||
templateUrl: './track-list.component.html',
|
||||
styleUrl: './track-list.component.scss'
|
||||
})
|
||||
export class TrackListComponent {
|
||||
|
||||
@Input() set playlistId(value: number) {
|
||||
console.log(value);
|
||||
this.tracks$ = this.service.getAllByPlaylist(value);
|
||||
}
|
||||
tracks$!: Observable<Track[]>;
|
||||
trackStatuses = TrackStatusEnum;
|
||||
|
||||
constructor(
|
||||
private readonly service: TrackService,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,17 @@ import { Injectable } from '@angular/core';
|
||||
import {createStore} from "@ngneat/elf";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {
|
||||
addEntities,
|
||||
selectAllEntities, selectEntities,
|
||||
setEntities,
|
||||
UIEntitiesRef,
|
||||
unionEntities, updateEntities,
|
||||
unionEntities, updateEntities, upsertEntities,
|
||||
withEntities,
|
||||
withUIEntities
|
||||
} from "@ngneat/elf-entities";
|
||||
import {joinRequestResult, trackRequestResult} from "@ngneat/elf-requests";
|
||||
import {tap} from "rxjs";
|
||||
import {map, tap} from "rxjs";
|
||||
import {TrackService} from "./track.service";
|
||||
import {PlaylistEntity} from "spooty-be/dist/playlist/playlist.entity";
|
||||
|
||||
const STORE_NAME = 'playlist';
|
||||
const ENDPOINT = '/api/playlist';
|
||||
@@ -21,7 +22,7 @@ export interface Playlist {
|
||||
id: number;
|
||||
name: string;
|
||||
spotifyUrl: string;
|
||||
tracks?: any[]; //todo fix it
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface PlaylistUi {
|
||||
@@ -42,12 +43,14 @@ export class PlaylistService {
|
||||
all$ = this.store.combine({
|
||||
entities: this.store.pipe(selectAllEntities()),
|
||||
UIEntities: this.store.pipe(selectEntities({ ref: UIEntitiesRef })),
|
||||
}).pipe(unionEntities());
|
||||
}).pipe(unionEntities(), map(data => data.sort((a, b) => b.createdAt - a.createdAt)));
|
||||
|
||||
loading$ = this.store.pipe(joinRequestResult([STORE_NAME]));
|
||||
createLoading$ = this.store.pipe(joinRequestResult([CREATE_LOADING], { initialStatus: 'idle' }));
|
||||
|
||||
constructor(private readonly http: HttpClient) {
|
||||
constructor(private readonly http: HttpClient,
|
||||
private readonly trackService: TrackService,
|
||||
) {
|
||||
}
|
||||
|
||||
fetch(): void {
|
||||
@@ -56,6 +59,7 @@ export class PlaylistService {
|
||||
setEntities(data),
|
||||
setEntities(data.map(item => ({id: item.id, collapsed: false})), {ref: UIEntitiesRef})
|
||||
)),
|
||||
tap((data: Playlist[]) => data.forEach(playlist => this.trackService.fetch(playlist.id))),
|
||||
trackRequestResult([STORE_NAME], { skipCache: true }),
|
||||
).subscribe();
|
||||
}
|
||||
@@ -63,7 +67,13 @@ export class PlaylistService {
|
||||
create(spotifyUrl: string): void {
|
||||
this.http.post(ENDPOINT, {spotifyUrl}).pipe(
|
||||
trackRequestResult([CREATE_LOADING], { skipCache: true })
|
||||
).subscribe();
|
||||
).subscribe((playlist: Partial<Playlist>) => {
|
||||
this.store.update(upsertEntities(playlist));
|
||||
this.store.update(
|
||||
upsertEntities(playlist),
|
||||
upsertEntities({id: playlist.id, collapsed: false}, {ref: UIEntitiesRef})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
toggleCollapsed(id: number): void {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TrackService } from './track.service';
|
||||
|
||||
describe('TrackService', () => {
|
||||
let service: TrackService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(TrackService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
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 {trackRequestResult} from "@ngneat/elf-requests";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
|
||||
const STORE_NAME = 'track';
|
||||
const ENDPOINT = '/api/track';
|
||||
|
||||
export interface Track {
|
||||
id: number;
|
||||
artist: string;
|
||||
song: string;
|
||||
spotifyUrl: string;
|
||||
youtubeUrl: string;
|
||||
status: TrackStatusEnum;
|
||||
playlistId?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export enum TrackStatusEnum {
|
||||
New,
|
||||
Searching,
|
||||
Queued,
|
||||
Downloading,
|
||||
Completed,
|
||||
Error,
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TrackService {
|
||||
|
||||
private store = createStore(
|
||||
{ name: STORE_NAME },
|
||||
withEntities<Track>(),
|
||||
);
|
||||
|
||||
getAllByPlaylist(id: number) {
|
||||
return this.store.pipe(selectManyByPredicate(({playlistId}) => playlistId === id))
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
private readonly socket: Socket,
|
||||
) {
|
||||
this.socket.on('track', (track: Track) => this.store.update(upsertEntities(track)));
|
||||
this.socket.on('trackNew', ({track, playlistId}: {track: Track, playlistId: number}) =>
|
||||
this.store.update(upsertEntities([{...track, playlistId}]))
|
||||
);
|
||||
}
|
||||
|
||||
fetch(playlistId: number): void {
|
||||
this.http.get<Track[]>(`${ENDPOINT}/playlist/${playlistId}`).pipe(
|
||||
tap((data: Track[]) => this.store.update(upsertEntities(data.map(track => ({...track, playlistId}))))),
|
||||
trackRequestResult([STORE_NAME], { skipCache: true }),
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user