File size: 5,479 Bytes
00a912e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import type { PlayMode, Track } from '@music-together/shared'
import { LIMITS } from '@music-together/shared'
import { roomRepo } from '../repositories/roomRepository.js'

export function addTrack(roomId: string, track: Track): boolean {
  const room = roomRepo.get(roomId)
  if (!room) return false
  if (room.queue.length >= LIMITS.QUEUE_MAX_SIZE) return false
  room.queue.push(track)
  roomRepo.persist(roomId)
  return true
}

/**
 * Add multiple tracks at once (from playlist import).
 * Respects QUEUE_MAX_SIZE — adds as many as fit.
 * @returns Number of tracks actually added.
 */
export function addBatchTracks(roomId: string, tracks: Track[]): number {
  const room = roomRepo.get(roomId)
  if (!room) return 0
  const available = LIMITS.QUEUE_MAX_SIZE - room.queue.length
  if (available <= 0) return 0
  const toAdd = tracks.slice(0, available)
  room.queue.push(...toAdd)
  roomRepo.persist(roomId)
  return toAdd.length
}

/**
 * Insert a new track right after the current playing track.
 * If current track is missing from the queue (edge race), insert to the front.
 */
export function insertAfterCurrent(roomId: string, track: Track): boolean {
  const room = roomRepo.get(roomId)
  if (!room) return false
  if (room.queue.length >= LIMITS.QUEUE_MAX_SIZE) return false

  const currentId = room.currentTrack?.id
  const currentIndex = currentId ? room.queue.findIndex((t) => t.id === currentId) : -1
  const insertIndex = currentIndex >= 0 ? currentIndex + 1 : 0
  room.queue.splice(insertIndex, 0, track)
  roomRepo.persist(roomId)
  return true
}

export function removeTrack(roomId: string, trackId: string): void {
  const room = roomRepo.get(roomId)
  if (room) {
    room.queue = room.queue.filter((t) => t.id !== trackId)
    roomRepo.persist(roomId)
  }
}

export function clearQueue(roomId: string): void {
  const room = roomRepo.get(roomId)
  if (room) {
    room.queue = []
    roomRepo.persist(roomId)
  }
}

export function reorderTracks(roomId: string, trackIds: string[]): void {
  const room = roomRepo.get(roomId)
  if (!room) return
  if (!Array.isArray(trackIds) || trackIds.length === 0) return

  const trackMap = new Map(room.queue.map((t) => [t.id, t]))
  const seen = new Set<string>()
  const reordered: Track[] = []
  for (const id of trackIds) {
    if (seen.has(id)) continue
    seen.add(id)
    const track = trackMap.get(id)
    if (track) reordered.push(track)
  }
  // Append any tracks that were NOT included in trackIds (prevent accidental drops)
  for (const track of room.queue) {
    if (!seen.has(track.id)) {
      reordered.push(track)
    }
  }
  room.queue = reordered
  roomRepo.persist(roomId)
}

export function updateBilibiliMetadata(
  roomId: string,
  trackId: string,
  metadata: Pick<Track, 'metadataSource' | 'lyricId' | 'picId' | 'cover'>,
): Track | null {
  const room = roomRepo.get(roomId)
  if (!room) return null
  const index = room.queue.findIndex((track) => track.id === trackId)
  const existing = room.queue[index]
  if (!existing || existing.source !== 'bilibili') return null

  const updated = { ...existing, ...metadata }
  room.queue[index] = updated
  if (room.currentTrack?.id === trackId) {
    // The current track includes the resolved, short-lived stream URL while
    // the queue entry normally does not. Preserve it when refreshing metadata.
    room.currentTrack = { ...room.currentTrack, ...metadata }
  }
  roomRepo.persist(roomId)
  return updated
}

/**
 * Get the next track based on the play mode.
 *
 * - sequential: next in queue; null at end
 * - loop-all:   next in queue; wraps to first at end
 * - loop-one:   returns the current track itself
 * - shuffle:    random track from queue (excludes current; returns self if queue has 1 item)
 */
export function getNextTrack(roomId: string, playMode?: PlayMode): Track | null {
  const room = roomRepo.get(roomId)
  if (!room || room.queue.length === 0) return null

  const mode = playMode ?? room.playMode ?? 'sequential'

  const currentIndex = room.currentTrack ? room.queue.findIndex((t) => t.id === room.currentTrack!.id) : -1

  switch (mode) {
    case 'loop-one':
      // Replay the current track; fall back to next if current is gone
      if (room.currentTrack && currentIndex >= 0) return room.currentTrack
      return room.queue[0] ?? null

    case 'loop-all': {
      const nextIndex = currentIndex + 1
      return nextIndex < room.queue.length ? room.queue[nextIndex] : room.queue[0] // wrap to first
    }

    case 'shuffle': {
      if (room.queue.length === 1) return room.queue[0]
      // Pick a random track excluding the current one
      const candidates = room.queue.filter((_, i) => i !== currentIndex)
      return candidates[Math.floor(Math.random() * candidates.length)] ?? room.queue[0]
    }

    case 'sequential':
    default: {
      const nextIndex = currentIndex + 1
      return nextIndex < room.queue.length ? room.queue[nextIndex] : null
    }
  }
}

export function getPreviousTrack(roomId: string): Track | null {
  const room = roomRepo.get(roomId)
  if (!room || room.queue.length === 0) return null

  const currentIndex = room.currentTrack ? room.queue.findIndex((t) => t.id === room.currentTrack!.id) : -1

  // For loop-all, wrap to last track when at the beginning
  if (room.playMode === 'loop-all' && currentIndex <= 0) {
    return room.queue[room.queue.length - 1]
  }

  const prevIndex = currentIndex - 1
  return prevIndex >= 0 ? room.queue[prevIndex] : null
}