first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from './socket/server'
|
||||
@@ -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`)
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user