-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathqueue.go
More file actions
839 lines (702 loc) · 28.7 KB
/
queue.go
File metadata and controls
839 lines (702 loc) · 28.7 KB
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
package main
import (
"fmt"
"log"
"os/exec"
"strconv"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
// This is the main function that plays the queue
// - It will play the queue until it's empty
// - If the queue is empty, it will leave the voice channel
func playQueue(m *discordgo.MessageCreate, isManual bool) {
// Prevent multiple simultaneous playQueue calls
if v.nowPlaying != (Song{}) {
log.Printf("WARN: playQueue called while already playing: %s", v.nowPlaying.Title)
return
}
// Pre-download first 3 songs before starting playback
queueMutex.Lock()
initialQueue := make([]Song, len(queue))
copy(initialQueue, queue)
queueMutex.Unlock()
if len(initialQueue) > 0 && !isManual {
log.Printf("INFO: Pre-downloading initial songs before playback")
err := bufferManager.PreDownloadInitialSongs(initialQueue, s, m.ChannelID)
if err != nil {
log.Printf("ERROR: Failed to pre-download songs: %v", err)
}
}
// Establish voice connection once for the entire queue
if v.voice == nil || !v.voice.Ready {
log.Printf("INFO: Establishing voice connection for queue playback")
// Find voice channel
voiceChannelID, err := v.findUserVoiceChannel()
if err != nil || voiceChannelID == "" {
log.Printf("ERROR: Failed to find a voice channel: %v", err)
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Could not find a voice channel to join!")
return
}
// Join voice channel once
v.voice, err = s.ChannelVoiceJoin(v.guildID, voiceChannelID, false, false)
if err != nil {
log.Printf("ERROR: Failed to join voice channel: %v", err)
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Failed to join voice channel!")
return
}
// Wait for voice connection to be ready
ready := false
for i := 0; i < 5; i++ {
if v.voice != nil && v.voice.Ready {
ready = true
log.Printf("INFO: Voice connection ready for queue playback")
break
}
log.Printf("INFO: Waiting for voice connection (attempt %d/5)", i+1)
time.Sleep(1 * time.Second)
}
if !ready {
log.Printf("ERROR: Voice connection failed to become ready")
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Voice connection failed!")
if v.voice != nil {
v.voice.Disconnect()
v.voice = nil
}
return
}
log.Printf("INFO: Voice connection established successfully")
}
// Start the buffer manager for ongoing downloads
bufferManager.StartBuffering(s, m.ChannelID)
// Iterate through the queue, playing each song
currentPlayingIndex := 0
for {
// Thread-safe queue access
queueMutex.Lock()
if len(queue) == 0 {
queueMutex.Unlock()
break
}
v.nowPlaying, queue = queue[0], queue[1:]
// Track when this song started playing for history
v.playStartTime = time.Now()
// Update buffer manager with current queue state
bufferManager.UpdateQueue(queue, currentPlayingIndex)
// Check if there's a next song for messaging
var hasNextSong bool
if len(queue) > 0 {
hasNextSong = true
}
queueMutex.Unlock()
log.Printf("INFO: Starting playback of: %s", v.nowPlaying.Title)
// Reset stop flag for this song
v.stop = false
if v.voice != nil {
v.voice.Speaking(true)
}
// Create a channel to signal when audio playback is complete
audioComplete := make(chan bool, 1)
// Start audio playback in a separate goroutine
go func() {
if isManual {
v.DCA(v.nowPlaying.Title, isManual, true)
} else {
v.DCA(v.nowPlaying.VideoURL, isManual, true)
}
audioComplete <- true
}()
// Monitor for skip commands while audio plays
skipDetected := false
ticker := time.NewTicker(100 * time.Millisecond)
monitorLoop:
for {
select {
case <-ticker.C:
// Check if skip was called
if v.stop {
log.Printf("INFO: Skip detected in playQueue monitor, stopping current song")
skipDetected = true
ticker.Stop()
break monitorLoop
}
case <-audioComplete:
// Audio finished normally
log.Printf("INFO: Audio playback completed normally")
ticker.Stop()
// Record song in history
if historyManager != nil && v.nowPlaying.Title != "" {
playDuration := time.Since(v.playStartTime)
guildName := ""
if guild, err := s.State.Guild(v.guildID); err == nil {
guildName = guild.Name
}
if err := historyManager.AddEntry(v.nowPlaying, v.guildID, guildName, playDuration); err != nil {
log.Printf("WARN: Failed to add song to history: %v", err)
}
}
break monitorLoop
}
}
if skipDetected {
log.Printf("INFO: Skip detected, moving to next song")
// Record skipped song in history (with partial play duration)
if historyManager != nil && v.nowPlaying.Title != "" {
playDuration := time.Since(v.playStartTime)
guildName := ""
if guild, err := s.State.Guild(v.guildID); err == nil {
guildName = guild.Name
}
if err := historyManager.AddEntry(v.nowPlaying, v.guildID, guildName, playDuration); err != nil {
log.Printf("WARN: Failed to add skipped song to history: %v", err)
}
}
// Give a moment for cleanup
time.Sleep(100 * time.Millisecond)
continue // Skip to next song
}
// Song completed normally, show next song message if queue not empty
queueMutex.Lock()
hasNextSong = len(queue) > 0 && queue[0].Title != ""
var nextSongTitle string
if hasNextSong {
nextSongTitle = queue[0].Title
}
queueMutex.Unlock()
if hasNextSong {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Next! Now playing ["+nextSongTitle+"] :loop:")
}
}
// No more songs in the queue, reset and disconnect voice
setPlaybackEnding(true) // Set flag to prevent inappropriate error messages
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Nothing left to play, peace! :v:")
v.stop = true
v.nowPlaying = Song{}
queueMutex.Lock()
queue = []Song{}
queueMutex.Unlock()
// Stop the buffer manager
bufferManager.StopBuffering()
// Disconnect voice connection only when queue is fully complete
if v.voice != nil {
log.Printf("INFO: Queue finished, disconnecting from voice channel")
v.voice.Disconnect()
v.voice = nil
}
// Cleanup the encoder
if v.encoder != nil {
v.encoder.Cleanup()
}
// Reset the playback ending flag after a short delay
go func() {
time.Sleep(2 * time.Second)
setPlaybackEnding(false)
setPlaybackState(false) // Reset playback state when queue finishes
}()
}
// queueSingleSong fetches metadata and queues a single video
func queueSingleSong(m *discordgo.MessageCreate, link string) {
log.Printf("[DEBUG] Attempting to get video from link: %s", link)
// Extract video ID first for cache checking
var videoID string
if strings.Contains(link, "youtube.com/watch?v=") {
parts := strings.Split(link, "v=")
if len(parts) > 1 {
videoID = strings.Split(parts[1], "&")[0]
}
} else if strings.Contains(link, "youtu.be/") {
parts := strings.Split(link, "youtu.be/")
if len(parts) > 1 {
videoID = strings.Split(parts[1], "?")[0]
}
}
// Check if song is already cached
if videoID != "" {
if cachedMetadata, exists := metadataManager.GetSong(videoID); exists {
log.Printf("[INFO] Found cached song: %s", cachedMetadata.Title)
// Check for similar songs with duplicate detection
similarSongs := metadataManager.FindSimilarSongs(cachedMetadata.Title, 0.8)
if len(similarSongs) > 1 {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** Found %d similar songs in cache, using: [%s] :recycle:", len(similarSongs), cachedMetadata.Title))
}
// Create song with cached data
song = fillSongInfo(m.ChannelID, m.Author.ID, m.ID, cachedMetadata.Title, cachedMetadata.VideoID, cachedMetadata.Duration)
song.VideoURL = cachedMetadata.FilePath
// Thread-safe queue append
queueMutex.Lock()
queue = append(queue, song)
queueMutex.Unlock()
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Adding cached ["+cachedMetadata.Title+"] to the Queue :musical_note:")
return
}
}
// First try to get video metadata using YouTube client
video, err := client.GetVideo(link)
if err != nil {
log.Printf("[ERROR] Failed to get video with YouTube client: %v", err)
// Check if it's an age restriction, embedding disabled error, or cipher issue
if strings.Contains(err.Error(), "age restriction") ||
strings.Contains(err.Error(), "embedding") ||
strings.Contains(err.Error(), "disabled") ||
strings.Contains(err.Error(), "cipher") ||
strings.Contains(err.Error(), "signature") {
log.Printf("[INFO] Attempting fallback with yt-dlp for restricted/problematic video")
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Video has restrictions or compatibility issues, using enhanced method...")
// Try using yt-dlp as fallback for restricted videos (including age-restricted)
if queueWithYtDlp(m, link) {
return // Success with yt-dlp
}
}
youtubeErr := NewYouTubeError("Failed to get video information",
"Failed to get video information. The video may be private, age-restricted, or unavailable in your region.", err).
WithContext("video_url", link).
WithContext("user_id", m.Author.ID)
errorHandler.Handle(youtubeErr, m.ChannelID)
return
}
log.Printf("[DEBUG] Successfully retrieved video: %s (ID: %s)", video.Title, video.ID)
log.Printf("[DEBUG] Video duration: %s", video.Duration)
// Check for similar songs in cache before downloading
similarSongs := metadataManager.FindSimilarSongs(video.Title, 0.8)
if len(similarSongs) > 0 {
log.Printf("[INFO] Found %d similar songs in cache for: %s", len(similarSongs), video.Title)
// Show user the similar songs found
similarTitles := make([]string, len(similarSongs))
for i, similar := range similarSongs {
similarTitles[i] = similar.Title
}
if len(similarTitles) == 1 {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** :warning: Found similar song in cache: [%s]. Adding new version anyway.", similarTitles[0]))
} else {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** :warning: Found %d similar songs in cache. Adding new version anyway.", len(similarTitles)))
}
}
// Always create song with proper metadata first
song = fillSongInfo(m.ChannelID, m.Author.ID, m.ID, video.Title, video.ID, video.Duration.String())
// Now try to get the stream URL or use cached file
url, err := getStreamURL(video.ID)
if err != nil {
log.Printf("[ERROR] Failed to get stream URL: %v", err)
// Try yt-dlp fallback if stream URL fails
log.Printf("[INFO] Stream URL failed, trying yt-dlp fallback")
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Stream access failed, trying alternative download method...")
if queueWithYtDlp(m, link) {
return // Success with yt-dlp
}
audioErr := NewAudioError("Failed to get working stream",
"Sorry, I couldn't get a working stream for this video :(", err).
WithContext("video_id", video.ID).
WithContext("video_title", video.Title).
WithContext("user_id", m.Author.ID)
errorHandler.Handle(audioErr, m.ChannelID)
return
}
// Set the video URL (could be stream URL or link to original video for yt-dlp processing)
song.VideoURL = url
// Thread-safe queue append
queueMutex.Lock()
queue = append(queue, song)
queueMutex.Unlock()
// Message the user
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Adding ["+video.Title+"] to the Queue :musical_note:")
}
// Queue the playlist - Gets the playlist ID and searches for all individual videos & queue's them
func queuePlaylist(playlistID string, m *discordgo.MessageCreate) {
nextPageToken := "" // Used to iterate through videos in a playlist
for {
// Retrieve next set of items in the playlist.
var snippet = []string{"snippet"}
playlistResponse := playlistItemsList(service, snippet, playlistID, nextPageToken)
for _, playlistItem := range playlistResponse.Items {
videoId := playlistItem.Snippet.ResourceId.VideoId
content := "https://www.youtube.com/watch?v=" + videoId
// Get Video Data
video, err := client.GetVideo(content)
if err != nil {
log.Println(err)
} else {
format := video.Formats.WithAudioChannels() // Get matches with audio channels only
song = fillSongInfo(m.ChannelID, m.Author.ID, m.ID, video.Title, video.ID, video.Duration.String())
formatList := prepSongFormat(format)
url, err := client.GetStreamURL(video, formatList)
if err != nil {
log.Println(err)
} else {
song.VideoURL = url
queue = append(queue, song)
}
}
}
// Set the token to retrieve the next page of results
nextPageToken = playlistResponse.NextPageToken
// Nothing left, break out
if nextPageToken == "" {
break
}
}
}
// Plays the chosen song from a list provided by the search function
func playFromSearch(input int, m *discordgo.MessageCreate) {
if input <= len(searchQueue) && input > 0 {
selectedSong := searchQueue[input-1]
videoURL := "https://www.youtube.com/watch?v=" + selectedSong.Id
// Check if this song is already cached before downloading
if cachedMetadata, exists := metadataManager.GetSong(selectedSong.Id); exists {
log.Printf("[INFO] Found cached song from search selection: %s", cachedMetadata.Title)
// Check for similar songs with duplicate detection
similarSongs := metadataManager.FindSimilarSongs(cachedMetadata.Title, 0.8)
if len(similarSongs) > 1 {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** Found %d similar songs in cache, using: [%s] :recycle:", len(similarSongs), cachedMetadata.Title))
}
// Create song with cached data
song = fillSongInfo(m.ChannelID, m.Author.ID, m.ID, cachedMetadata.Title, cachedMetadata.VideoID, cachedMetadata.Duration)
song.VideoURL = cachedMetadata.FilePath
// Thread-safe queue append
queueMutex.Lock()
queue = append(queue, song)
queueMutex.Unlock()
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Adding cached ["+cachedMetadata.Title+"] from search to the Queue :musical_note:")
} else {
// Not cached, proceed with normal download and queue
queueSingleSong(m, videoURL)
}
} else {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** The value you entered was outside the range of the search...")
}
searchRequested = false
}
// Plays the chosen song from the queue
func playFromQueue(input int, m *discordgo.MessageCreate) {
if input <= len(queue) && input > 0 {
var tmp []Song
for i, value := range queue {
switch i {
case 0:
tmp = append(tmp, queue[input-1])
tmp = append(tmp, value)
case input - 1:
default:
tmp = append(tmp, value)
}
}
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Moved "+queue[input-1].Title+" to the top of the queue")
queue = tmp
prepSkip()
} else {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Selected input was not in queue range")
}
}
// Prepares queue display
func prepDisplayQueue(commData []string, queueLenBefore int, m *discordgo.MessageCreate) {
// Don't show error messages if stop was recently requested or playback is ending
if isStopRequested() || isPlaybackEnding() {
log.Printf("[DEBUG] prepDisplayQueue skipped - stop requested: %t, playback ending: %t", isStopRequested(), isPlaybackEnding())
return
}
// Debug logging to understand when this function is called inappropriately
log.Printf("[DEBUG] prepDisplayQueue called: commData=%v, queueLenBefore=%d, currentQueueLen=%d", commData, queueLenBefore, len(queue))
// Display queue if it grew in size (new items added)
if queueLenBefore < len(queue) {
log.Printf("[DEBUG] Queue grew from %d to %d, displaying updated queue", queueLenBefore, len(queue))
displayQueue(m)
return
}
// Don't show error messages for certain commands that shouldn't trigger them
if len(commData) > 0 {
firstCommand := strings.ToLower(commData[0])
if firstCommand == "stop" || firstCommand == "skip" || firstCommand == "queue" {
log.Printf("[DEBUG] Skipping error message for %s command", firstCommand)
return
}
}
// Check if this is a numeric input (search result selection)
if len(commData) > 1 {
if _, err := strconv.Atoi(commData[1]); err == nil {
log.Printf("[DEBUG] Skipping error message - detected numeric input: %s", commData[1])
return
}
}
// Check if we're currently playing something - if so, we're likely not in an error state
if v.nowPlaying != (Song{}) {
log.Printf("[DEBUG] Skipping error message - something is currently playing")
return
}
// Only show error message if we're actually trying to add content
// Skip if this seems to be an end-of-queue or stop scenario
// FIXED: Using thread-safe stopRequested access to prevent race conditions
if !isStopRequested() && !isPlaybackEnding() && len(commData) > 1 && (strings.Contains(commData[1], "http") || len(commData[1]) > 3) {
// Additional check: don't show error if the command looks like it might be successful later
// (e.g., during playlist processing)
if strings.Contains(commData[1], "playlist") || strings.Contains(commData[1], "list=") {
log.Printf("[DEBUG] Skipping error message - appears to be playlist processing")
return
}
log.Printf("[DEBUG] Showing 'nothing added' message for command: %v", commData)
nothingAddedMessage := "**[Muse]** Nothing was added, playlist or song was empty...\n"
nothingAddedMessage = nothingAddedMessage + "Note:\n"
nothingAddedMessage = nothingAddedMessage + "- Playlists should have the following url structure: <https://www.youtube.com/playlist?list=><PLAYLIST IDENTIFIER>\n"
nothingAddedMessage = nothingAddedMessage + "- Videos should have the following url structure: <https://www.youtube.com/watch?v=><VIDEO IDENTIFIER>\n"
nothingAddedMessage = nothingAddedMessage + "- Youtu.be links or links set at a certain time (t=#s) have not been implemented - sorry!"
s.ChannelMessageSend(m.ChannelID, nothingAddedMessage)
} else {
log.Printf("[DEBUG] Skipping error message - command doesn't seem to be adding content: %v", commData)
}
}
// queueWithYtDlp uses yt-dlp as a fallback for restricted videos
func queueWithYtDlp(m *discordgo.MessageCreate, link string) bool {
// Extract video ID from the link
var videoID string
if strings.Contains(link, "youtube.com/watch?v=") {
parts := strings.Split(link, "v=")
if len(parts) > 1 {
videoID = strings.Split(parts[1], "&")[0]
}
} else if strings.Contains(link, "youtu.be/") {
parts := strings.Split(link, "youtu.be/")
if len(parts) > 1 {
videoID = strings.Split(parts[1], "?")[0]
}
}
if videoID == "" {
log.Printf("[ERROR] Could not extract video ID from URL: %s", link)
return false
}
log.Printf("[INFO] Using yt-dlp fallback for video ID: %s", videoID)
// Use yt-dlp to get video info with comprehensive age restriction bypass
var output []byte
var err error
// Try different bypass methods in order of preference
bypasses := [][]string{
// Method 1: Basic age bypass
{"--no-download", "--print", "title", "--print", "duration", "--age-limit", "99", "--no-check-certificate"},
// Method 2: With Chrome cookies
{"--no-download", "--print", "title", "--print", "duration", "--age-limit", "99", "--no-check-certificate", "--cookies-from-browser", "chrome"},
// Method 3: With Safari cookies (macOS)
{"--no-download", "--print", "title", "--print", "duration", "--age-limit", "99", "--no-check-certificate", "--cookies-from-browser", "safari"},
// Method 4: With Firefox cookies
{"--no-download", "--print", "title", "--print", "duration", "--age-limit", "99", "--no-check-certificate", "--cookies-from-browser", "firefox"},
}
for i, args := range bypasses {
cmd := exec.Command("yt-dlp", append(args, link)...)
output, err = cmd.Output()
if err == nil {
if i > 0 {
log.Printf("[INFO] yt-dlp succeeded with bypass method %d (using browser cookies)", i+1)
}
break
}
log.Printf("[DEBUG] yt-dlp bypass method %d failed: %v", i+1, err)
}
if err != nil {
log.Printf("[ERROR] All yt-dlp bypass methods failed: %v", err)
return false
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) < 2 {
log.Printf("[ERROR] yt-dlp returned unexpected output format")
return false
}
title := strings.TrimSpace(lines[0])
duration := strings.TrimSpace(lines[1])
log.Printf("[INFO] yt-dlp got video info: %s (duration: %s)", title, duration)
// For yt-dlp fallback, we'll use the download approach since streaming might not work
// Create the song entry with a special flag to indicate it needs yt-dlp download
song = fillSongInfo(m.ChannelID, m.Author.ID, m.ID, title, videoID, duration)
song.VideoURL = link // Store original URL for yt-dlp processing
// Thread-safe queue append
queueMutex.Lock()
queue = append(queue, song)
queueMutex.Unlock()
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Adding ["+title+"] to the Queue (using fallback method) :musical_note:")
log.Printf("[INFO] Successfully queued restricted video using yt-dlp: %s", title)
return true
}
// queuePlaylistThreaded processes playlists by downloading all songs first
// then starting playback once everything is ready
func queuePlaylistThreaded(playlistID string, m *discordgo.MessageCreate) {
// Check user-specific cooldown to prevent spam
userKey := m.Author.ID + ":playlist"
if isCommandActive(userKey, "playlist") {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** ⏳ Please wait a moment before adding another playlist. (User rate limit)")
log.Printf("WARN: Playlist processing rejected - user %s rate limited", m.Author.ID)
return
}
setCommandActive(userKey, "playlist")
defer clearCommandActive(userKey, "playlist")
// Acquire playlist processing semaphore with timeout
select {
case playlistSemaphore <- struct{}{}:
// Got the semaphore, proceed
defer func() { <-playlistSemaphore }()
case <-time.After(5 * time.Second): // Add timeout to prevent hanging
s.ChannelMessageSend(m.ChannelID, "**[Muse]** 🚫 Playlist processing system is at capacity. Please try again in a moment.")
log.Printf("WARN: Playlist processing rejected - semaphore timeout (system at capacity)")
return
}
log.Printf("INFO: Starting threaded playlist processing for: %s", playlistID)
// Use yt-dlp as primary method for playlist processing due to YouTube client issues
playlistURL := "https://www.youtube.com/playlist?list=" + playlistID
log.Printf("INFO: Using yt-dlp for playlist processing: %s", playlistURL)
s.ChannelMessageSend(m.ChannelID, "**[Muse]** 🔍 Scanning playlist with enhanced method (this may take a moment)...")
// Get playlist info using yt-dlp with age restriction bypass
cmd := exec.Command("yt-dlp",
"--flat-playlist",
"--print", "id",
"--print", "title",
"--no-warnings",
"--age-limit", "99", // Bypass age restrictions
"--no-check-certificate", // Skip SSL verification if needed
playlistURL)
output, err := cmd.Output()
if err != nil {
log.Printf("ERROR: yt-dlp failed to get playlist info: %v", err)
s.ChannelMessageSend(m.ChannelID, "**[Muse]** ❌ Failed to access playlist. It may be private, deleted, or region-restricted.")
return
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) < 2 {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** ❌ Playlist appears to be empty or inaccessible.")
return
}
// Parse yt-dlp output (alternating lines: id, title, id, title, ...)
var videoData []struct {
ID string
Title string
}
for i := 0; i < len(lines); i += 2 {
if i+1 < len(lines) {
videoData = append(videoData, struct {
ID string
Title string
}{
ID: strings.TrimSpace(lines[i]),
Title: strings.TrimSpace(lines[i+1]),
})
}
}
if len(videoData) == 0 {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** No videos found in playlist or playlist is private.")
return
}
// Check if playlist is too large
if len(videoData) > 100 {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** 🚫 Playlist too large! (%d songs). Maximum allowed is 100 songs to prevent system overload.", len(videoData)))
log.Printf("WARN: Playlist rejected - too large (%d songs)", len(videoData))
return
}
// Check if adding this playlist would exceed queue limit
queueMutex.Lock()
currentQueueSize := len(queue)
queueMutex.Unlock()
if currentQueueSize+len(videoData) > maxQueueSize {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** 🚫 Adding this playlist (%d songs) would exceed the maximum queue size (%d). Current queue: %d songs.", len(videoData), maxQueueSize, currentQueueSize))
log.Printf("WARN: Playlist rejected - would exceed queue limit")
return
}
log.Printf("INFO: Found %d videos in playlist using yt-dlp", len(videoData))
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** Found %d videos! Processing playlist... ⏳", len(videoData)))
// Process all videos and queue them before starting playback
songsProcessed := 0
successfullyQueued := 0
// Reduced concurrent processing to prevent overwhelming
maxConcurrent := 2
semaphore := make(chan struct{}, maxConcurrent)
// Structure to hold results with their original index
type processResult struct {
index int
song Song
success bool
}
// Channel to collect results from goroutines
resultChan := make(chan processResult, len(videoData))
// Process all videos in parallel
for i, video := range videoData {
// Check if we should stop processing (user might have stopped)
if isStopRequested() || isPlaybackEnding() {
log.Printf("INFO: Stopping playlist processing due to stop request")
s.ChannelMessageSend(m.ChannelID, "**[Muse]** Playlist processing stopped.")
return
}
semaphore <- struct{}{} // Acquire semaphore
go func(videoID, title string, index int) {
defer func() { <-semaphore }() // Release semaphore
videoURL := "https://www.youtube.com/watch?v=" + videoID
log.Printf("INFO: Processing video %d/%d: %s - %s", index+1, len(videoData), title, videoID)
// Get duration using yt-dlp (more reliable than YouTube client)
durationCmd := exec.Command("yt-dlp",
"--no-download",
"--print", "duration_string",
"--no-warnings",
"--age-limit", "99", // Bypass age restrictions
"--no-check-certificate", // Skip SSL verification if needed
videoURL)
durationOutput, err := durationCmd.Output()
duration := "Unknown"
if err == nil {
duration = strings.TrimSpace(string(durationOutput))
}
// Create song with yt-dlp metadata
song := fillSongInfo(m.ChannelID, m.Author.ID, m.ID, title, videoID, duration)
song.VideoURL = videoURL // Store original URL for yt-dlp processing
log.Printf("[INFO] Successfully processed (%d/%d): %s", index+1, len(videoData), title)
resultChan <- processResult{index: index, song: song, success: true}
}(video.ID, video.Title, i)
}
// Collect all results with timeout protection
results := make([]processResult, len(videoData))
timeout := time.NewTimer(5 * time.Minute) // 5 minute timeout for playlist processing
defer timeout.Stop()
for i := 0; i < len(videoData); i++ {
select {
case result := <-resultChan:
results[result.index] = result
songsProcessed++
if result.success {
successfullyQueued++
}
// Send progress updates less frequently to avoid spam
if songsProcessed%10 == 0 || songsProcessed == len(videoData) {
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** Processed %d/%d songs from playlist... 🎵", songsProcessed, len(videoData)))
}
case <-timeout.C:
s.ChannelMessageSend(m.ChannelID, "**[Muse]** ⏰ Playlist processing timed out. Some songs may not have been added.")
log.Printf("ERROR: Playlist processing timed out after 5 minutes")
goto processResults
}
}
processResults:
log.Printf("INFO: Playlist processing complete. Successfully queued %d/%d songs", successfullyQueued, len(videoData))
if successfullyQueued == 0 {
s.ChannelMessageSend(m.ChannelID, "**[Muse]** No songs could be processed from the playlist. All videos may be unavailable or restricted.")
return
}
// Add successful songs to queue in original playlist order
queueMutex.Lock()
for _, result := range results {
if result.success {
queue = append(queue, result.song)
}
}
queueMutex.Unlock()
// Now start playback with all songs queued
s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("**[Muse]** ✅ Playlist ready! Added %d songs to queue. 🎵", successfullyQueued))
// **ALWAYS SHOW COMPLETE QUEUE** after playlist processing
log.Printf("INFO: Displaying complete queue after playlist processing")
displayQueue(m)
// **SMART PLAYBACK MANAGEMENT** - Start playback only if nothing is playing
queueMutex.Lock()
currentQueueSize = len(queue)
queueMutex.Unlock()
if v.nowPlaying == (Song{}) && currentQueueSize >= 1 && !getPlaybackState() {
// Nothing is playing, start playback
log.Printf("INFO: Starting playback for playlist with %d songs", currentQueueSize)
joinVoiceChannel()
prepFirstSongEntered(m, false)
} else if getPlaybackState() || v.nowPlaying != (Song{}) {
// Something is already playing, just notify that songs were added
log.Printf("INFO: Playback already in progress, playlist songs added to queue")
s.ChannelMessageSend(m.ChannelID, "**[Muse]** ✅ Playlist added to queue! Songs will play after current music. 🎵")
}
log.Printf("INFO: Threaded playlist processing completed for %d videos", len(videoData))
}