first commit

This commit is contained in:
2024-05-28 10:13:33 +02:00
commit 83d33d2847
18 changed files with 2718 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
export * from './socket/server'
+82
View File
@@ -0,0 +1,82 @@
import { Server as SocketIOServer, type Socket } from 'socket.io'
import 'dotenv/config'
import { SpotifyService } from '../spotify/service'
import { z } from 'zod'
const envSchema = z.object({
CLIENT_ID: z.string().min(1),
CLIENT_SECRET: z.string().min(1),
REFRESH_TOKEN: z.string().min(1),
CORS_ORIGIN: z.string().optional(),
})
const { CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, CORS_ORIGIN } =
envSchema.parse(process.env)
const io = new SocketIOServer(3000, {
cors: {
origin: CORS_ORIGIN ?? '*',
},
})
const spotify = new SpotifyService({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
refreshToken: REFRESH_TOKEN,
})
let interval: ReturnType<typeof setInterval> | null = null
let currentSong: string | null = null
const sendNowPlayingData = async () => {
const song = await spotify.getCurrentSong()
try {
switch (true) {
case song?.is_playing && JSON.stringify(song) !== currentSong:
currentSong = JSON.stringify(song)
io.emit('nowPlayingData', currentSong)
break
case !song?.is_playing && currentSong !== null:
if (currentSong == null) return
currentSong = null
io.emit('nowPlayingData', currentSong)
break
default:
return
}
} catch (error) {
console.error('Error fetching song data:', error)
}
}
io.on('connection', async (socket: Socket) => {
switch (currentSong) {
case null:
socket.emit('nowPlayingData', null)
break
default:
socket.emit('nowPlayingData', currentSong)
break
}
if (!interval) {
interval = setInterval(() => {
sendNowPlayingData().catch((err) => {
console.error('Error sending NowPlayingData:', err)
})
}, 3000)
}
socket.on('disconnect', () => {
if (io.sockets.sockets.size === 0 && interval) {
clearInterval(interval)
interval = null
}
})
socket.on('error', (err) => {
console.error('WebSocket error:', err)
})
})
console.log(`\u001b[1;32m[SPOTIFY-WS] \x1b[34mSTARTED ON PORT 3000\x1b[0m\n`)
+28
View File
@@ -0,0 +1,28 @@
import type { SongResult, Artist, Item } from './types'
export class SongResultMap {
public static parseSong(result: {
progress_ms: number
item: Item
is_playing: boolean
}): SongResult {
const { item } = result
return {
is_playing: result.is_playing,
title: item.name,
album: {
name: item.album.name,
image: item.album.images[0]?.url,
release_date: item.album.release_date,
},
artists: {
name: item.artists.map((x: Artist) => x.name),
url: item.artists.map((x: Artist) => x.external_urls.spotify),
},
url: item.external_urls.spotify,
progress: result.progress_ms,
duration: item.duration_ms,
}
}
}
+69
View File
@@ -0,0 +1,69 @@
import axios, { type AxiosResponse } from 'axios'
import { SongResultMap } from './result'
import type { SongResult, Item } from './types'
interface SpotifyCredentials {
clientId: string
clientSecret: string
refreshToken: string
}
export class SpotifyService {
private readonly credentials: SpotifyCredentials
private accessToken?: string
constructor(credentials: SpotifyCredentials) {
this.credentials = credentials
}
private hasAccessToken(): boolean {
return !!this.accessToken
}
private setAccessToken(token: string): void {
this.accessToken = token
}
private async refreshAccessToken(): Promise<void> {
try {
const response: AxiosResponse<{ access_token: string }> =
await axios.post('https://accounts.spotify.com/api/token', null, {
params: {
client_id: this.credentials.clientId,
client_secret: this.credentials.clientSecret,
refresh_token: this.credentials.refreshToken,
grant_type: 'refresh_token',
},
})
this.setAccessToken(response.data.access_token)
} catch (error) {
throw new Error('Invalid credentials were given')
}
}
public async getCurrentSong(): Promise<SongResult | undefined> {
try {
if (!this.hasAccessToken()) {
await this.refreshAccessToken()
}
const response: AxiosResponse<{
progress_ms: number
item: Item
is_playing: boolean
}> = await axios.get(
'https://api.spotify.com/v1/me/player/currently-playing',
{
headers: {
Authorization: 'Bearer ' + this.accessToken,
},
}
)
return SongResultMap.parseSong(response.data)
} catch (error) {
await this.refreshAccessToken()
}
}
}
+37
View File
@@ -0,0 +1,37 @@
export type SongResult = {
progress: number
album: {
name: string
image?: string
release_date: string
}
artists: {
name: string[]
url: string[]
}
url: string
title: string
duration: number
is_playing: boolean
}
export interface Album {
name: string
images: {
url: string
}[]
release_date: string
}
export interface Artist {
name: string
external_urls: { spotify: string }
}
export interface Item {
name: string
album: Album
artists: Artist[]
external_urls: { spotify: string }
duration_ms: number
}