Merge pull request #105 from nkhoit/master
Added addnext command and an argument for listsongs command (fixes https://github.com/matthieugrieger/mumbledj/issues/62)
This commit is contained in:
commit
da096944f2
|
@ -44,9 +44,10 @@ These are all of the chat commands currently supported by MumbleDJ. All command
|
||||||
|
|
||||||
Command | Description | Arguments | Admin | Example
|
Command | Description | Arguments | Admin | Example
|
||||||
--------|-------------|-----------|-------|--------
|
--------|-------------|-----------|-------|--------
|
||||||
**add** | Adds audio from a url to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. Please note, however, that if a YouTube playlist contains over 25 videos only the first 25 videos will be placed in the song queue. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys`
|
**add** | Adds audio from a url to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. The maximum amount of songs that can be added from a playlist is specified in `mumbledj.gcfg`. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys`
|
||||||
|
**addnext** | Adds audio from a url to the song queue after the current song. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. The maximum amount of songs that can be added from a playlist is specified in `mumbledj.gcfg`. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | Yes | `!addnext https://www.youtube.com/watch?v=5xfEr2Oxdys`
|
||||||
**skip**| Submits a vote to skip the current song. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the song will be skipped and the next will start playing. Each user may only submit one skip per song. | None | No | `!skip`
|
**skip**| Submits a vote to skip the current song. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the song will be skipped and the next will start playing. Each user may only submit one skip per song. | None | No | `!skip`
|
||||||
**skipplaylist** | Submits a vote to skip the current playlist. Once the skip ratio target (specified in mumbledj.gcfg) is met, the playlist will be skipped and the next song/playlist will start playing. Each user may only submit one skip per playlist. | None | No | `!skipplaylist`
|
**skipplaylist** | Submits a vote to skip the current playlist. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the playlist will be skipped and the next song/playlist will start playing. Each user may only submit one skip per playlist. | None | No | `!skipplaylist`
|
||||||
**forceskip** | An admin command that forces a song skip. | None | Yes | `!forceskip`
|
**forceskip** | An admin command that forces a song skip. | None | Yes | `!forceskip`
|
||||||
**forceskipplaylist** | An admin command that forces a playlist skip. | None | Yes | `!forceskipplaylist`
|
**forceskipplaylist** | An admin command that forces a playlist skip. | None | Yes | `!forceskipplaylist`
|
||||||
**shuffle** | An admin command that shuffles the current queue. | None | Yes | `!shuffle`
|
**shuffle** | An admin command that shuffles the current queue. | None | Yes | `!shuffle`
|
||||||
|
@ -60,7 +61,7 @@ Command | Description | Arguments | Admin | Example
|
||||||
**numsongs** | Outputs the number of songs in the queue in chat. Individual songs and songs within playlists are both counted. | None | No | `!numsongs`
|
**numsongs** | Outputs the number of songs in the queue in chat. Individual songs and songs within playlists are both counted. | None | No | `!numsongs`
|
||||||
**nextsong** | Outputs the title and name of the submitter of the next song in the queue if it exists. | None | No | `!nextsong`
|
**nextsong** | Outputs the title and name of the submitter of the next song in the queue if it exists. | None | No | `!nextsong`
|
||||||
**currentsong** | Outputs the title and name of the submitter of the song currently playing. | None | No | `!currentsong`
|
**currentsong** | Outputs the title and name of the submitter of the song currently playing. | None | No | `!currentsong`
|
||||||
**listsongs** | Outputs a list of the songs currently in the queue. | None | No | `!listsongs`
|
**listsongs** | Outputs a list of the songs currently in the queue. | None or desired number of songs to list | No | `!listsongs`
|
||||||
**setcomment** | Sets the comment for the bot. If no argument is given, the current comment will be removed. | None OR new_comment | Yes | `!setcomment Hello! I am a bot. Type !help for the available commands.`
|
**setcomment** | Sets the comment for the bot. If no argument is given, the current comment will be removed. | None OR new_comment | Yes | `!setcomment Hello! I am a bot. Type !help for the available commands.`
|
||||||
**numcached** | Outputs the number of songs currently cached on disk. | None | Yes | `!numcached`
|
**numcached** | Outputs the number of songs currently cached on disk. | None | Yes | `!numcached`
|
||||||
**cachesize** | Outputs the total file size of the cache in MB. | None | Yes | `!cachesize`
|
**cachesize** | Outputs the total file size of the cache in MB. | None | Yes | `!cachesize`
|
||||||
|
|
44
commands.go
44
commands.go
|
@ -41,6 +41,13 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Addnext command
|
||||||
|
case dj.conf.Aliases.AddNextAlias:
|
||||||
|
if dj.HasPermission(username, dj.conf.Permissions.AdminAddNext) {
|
||||||
|
addNext(user, argument)
|
||||||
|
} else {
|
||||||
|
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||||
|
}
|
||||||
// Skip command
|
// Skip command
|
||||||
case dj.conf.Aliases.SkipAlias:
|
case dj.conf.Aliases.SkipAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
|
||||||
|
@ -181,7 +188,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
// ListSongs command
|
// ListSongs command
|
||||||
case dj.conf.Aliases.ListSongsAlias:
|
case dj.conf.Aliases.ListSongsAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminListSongs) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminListSongs) {
|
||||||
listSongs(user)
|
listSongs(user, argument)
|
||||||
} else {
|
} else {
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
@ -205,6 +212,25 @@ func add(user *gumble.User, url string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addnext performs !addnext functionality. Checks input URL for service, and adds
|
||||||
|
// the URL to the queue as the next song if the format matches.
|
||||||
|
func addNext(user *gumble.User, url string) error {
|
||||||
|
if !dj.audioStream.IsPlaying() {
|
||||||
|
return add(user, url)
|
||||||
|
} else {
|
||||||
|
if url == "" {
|
||||||
|
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
||||||
|
return errors.New("NO_ARGUMENT")
|
||||||
|
} else {
|
||||||
|
err := FindServiceAndInsertNext(user, url)
|
||||||
|
if err != nil {
|
||||||
|
dj.SendPrivateMessage(user, err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// skip performs !skip functionality. Adds a skip to the skippers slice for the current song, and then
|
// skip performs !skip functionality. Adds a skip to the skippers slice for the current song, and then
|
||||||
// evaluates if a skip should be performed. Both skip and forceskip are implemented here.
|
// evaluates if a skip should be performed. Both skip and forceskip are implemented here.
|
||||||
func skip(user *gumble.User, admin, playlistSkip bool) {
|
func skip(user *gumble.User, admin, playlistSkip bool) {
|
||||||
|
@ -452,11 +478,23 @@ func toggleAutomaticShuffle(activate bool, user *gumble.User, username string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// listSongs handles !listSongs functionality. Sends a private message to the user a list of all songs in the queue
|
// listSongs handles !listSongs functionality. Sends a private message to the user a list of all songs in the queue
|
||||||
func listSongs(user *gumble.User) {
|
func listSongs(user *gumble.User, value string) {
|
||||||
if dj.audioStream.IsPlaying() {
|
if dj.audioStream.IsPlaying() {
|
||||||
|
num := 0
|
||||||
|
if value == "" {
|
||||||
|
num = dj.queue.Len()
|
||||||
|
} else {
|
||||||
|
if parsedNum, err := strconv.Atoi(value); err != nil {
|
||||||
|
num = dj.queue.Len()
|
||||||
|
} else {
|
||||||
|
num = parsedNum
|
||||||
|
}
|
||||||
|
}
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
dj.queue.Traverse(func(i int, song Song) {
|
dj.queue.Traverse(func(i int, song Song) {
|
||||||
buffer.WriteString(fmt.Sprintf(SONG_LIST_HTML, song.Title(), song.Submitter()))
|
if i < num {
|
||||||
|
buffer.WriteString(fmt.Sprintf(SONG_LIST_HTML, i+1, song.Title(), song.Submitter()))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
dj.SendPrivateMessage(user, buffer.String())
|
dj.SendPrivateMessage(user, buffer.String())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -70,6 +70,10 @@ HighestVolume = 0.8
|
||||||
# DEFAULT VALUE: "add"
|
# DEFAULT VALUE: "add"
|
||||||
AddAlias = "add"
|
AddAlias = "add"
|
||||||
|
|
||||||
|
# Alias used for addnext command
|
||||||
|
# DEFAULT VALUE: "addnext"
|
||||||
|
AddNextAlias = "addnext"
|
||||||
|
|
||||||
# Alias used for skip command
|
# Alias used for skip command
|
||||||
# DEFAULT VALUE: "skip"
|
# DEFAULT VALUE: "skip"
|
||||||
SkipAlias = "skip"
|
SkipAlias = "skip"
|
||||||
|
@ -169,6 +173,10 @@ Admins = "Matt"
|
||||||
# DEFAULT VALUE: false
|
# DEFAULT VALUE: false
|
||||||
AdminAdd = false
|
AdminAdd = false
|
||||||
|
|
||||||
|
# Make addnext an admin command?
|
||||||
|
# DEFAULT VALUE: true
|
||||||
|
AdminAddNext = true
|
||||||
|
|
||||||
# Make playlist adds an admin only action?
|
# Make playlist adds an admin only action?
|
||||||
# DEFAULT VALUE: false
|
# DEFAULT VALUE: false
|
||||||
AdminAddPlaylists = false
|
AdminAddPlaylists = false
|
||||||
|
|
|
@ -37,6 +37,7 @@ type DjConfig struct {
|
||||||
}
|
}
|
||||||
Aliases struct {
|
Aliases struct {
|
||||||
AddAlias string
|
AddAlias string
|
||||||
|
AddNextAlias string
|
||||||
SkipAlias string
|
SkipAlias string
|
||||||
SkipPlaylistAlias string
|
SkipPlaylistAlias string
|
||||||
AdminSkipAlias string
|
AdminSkipAlias string
|
||||||
|
@ -62,6 +63,7 @@ type DjConfig struct {
|
||||||
AdminsEnabled bool
|
AdminsEnabled bool
|
||||||
Admins []string
|
Admins []string
|
||||||
AdminAdd bool
|
AdminAdd bool
|
||||||
|
AdminAddNext bool
|
||||||
AdminAddPlaylists bool
|
AdminAddPlaylists bool
|
||||||
AdminSkip bool
|
AdminSkip bool
|
||||||
AdminHelp bool
|
AdminHelp bool
|
||||||
|
|
63
service.go
63
service.go
|
@ -115,7 +115,7 @@ func FindServiceAndAdd(user *gumble.User, url string) error {
|
||||||
|
|
||||||
// Starts playing the new song if nothing else is playing
|
// Starts playing the new song if nothing else is playing
|
||||||
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
||||||
if (dj.conf.General.AutomaticShuffleOn){
|
if dj.conf.General.AutomaticShuffleOn {
|
||||||
dj.queue.RandomNextSong(true)
|
dj.queue.RandomNextSong(true)
|
||||||
}
|
}
|
||||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
if err := dj.queue.CurrentSong().Download(); err == nil {
|
||||||
|
@ -130,6 +130,67 @@ func FindServiceAndAdd(user *gumble.User, url string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindServiceAndInsertNext tries the given url with each service
|
||||||
|
// and inserts the song/playlist with the correct service into the slot after the current one
|
||||||
|
func FindServiceAndInsertNext(user *gumble.User, url string) error {
|
||||||
|
var urlService Service
|
||||||
|
|
||||||
|
// Checks all services to see if any can take the URL
|
||||||
|
for _, service := range services {
|
||||||
|
if service.URLRegex(url) {
|
||||||
|
urlService = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlService == nil {
|
||||||
|
return errors.New(INVALID_URL_MSG)
|
||||||
|
} else {
|
||||||
|
var title string
|
||||||
|
var songsAdded = 0
|
||||||
|
var songArray []Song
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Get service to create songs
|
||||||
|
if songArray, err = urlService.NewRequest(user, url); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Playlist Permission
|
||||||
|
if len(songArray) > 1 && !dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
||||||
|
return errors.New(NO_PLAYLIST_PERMISSION_MSG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all songs and add to the queue
|
||||||
|
i := 0
|
||||||
|
for _, song := range songArray {
|
||||||
|
i++
|
||||||
|
// Check song is not too long
|
||||||
|
if dj.conf.General.MaxSongDuration == 0 || int(song.Duration().Seconds()) <= dj.conf.General.MaxSongDuration {
|
||||||
|
if !isNil(song.Playlist()) {
|
||||||
|
title = song.Playlist().Title()
|
||||||
|
} else {
|
||||||
|
title = song.Title()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add song to queue
|
||||||
|
dj.queue.InsertSong(song, i)
|
||||||
|
songsAdded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert channel of added song/playlist
|
||||||
|
if songsAdded == 0 {
|
||||||
|
return errors.New(fmt.Sprintf(TRACK_TOO_LONG_MSG, urlService.ServiceName()))
|
||||||
|
} else if songsAdded == 1 {
|
||||||
|
dj.client.Self.Channel.Send(fmt.Sprintf(NEXT_SONG_ADDED_HTML, user.Name, title), false)
|
||||||
|
} else {
|
||||||
|
dj.client.Self.Channel.Send(fmt.Sprintf(NEXT_PLAYLIST_ADDED_HTML, user.Name, title), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RegexpFromURL loops through an array of patterns to see if it matches the url
|
// RegexpFromURL loops through an array of patterns to see if it matches the url
|
||||||
func RegexpFromURL(url string, patterns []string) *regexp.Regexp {
|
func RegexpFromURL(url string, patterns []string) *regexp.Regexp {
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
|
|
30
songqueue.go
30
songqueue.go
|
@ -10,12 +10,12 @@ package main
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rand.Seed(time.Now().UTC().UnixNano())
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SongQueue type declaration.
|
// SongQueue type declaration.
|
||||||
|
@ -40,6 +40,16 @@ func (q *SongQueue) AddSong(s Song) error {
|
||||||
return errors.New("Could not add Song to the SongQueue.")
|
return errors.New("Could not add Song to the SongQueue.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertSong inserts a Song to the SongQueue at a location.
|
||||||
|
func (q *SongQueue) InsertSong(s Song, i int) error {
|
||||||
|
beforeLen := q.Len()
|
||||||
|
q.queue = append(q.queue[:i], append([]Song{s}, q.queue[i:]...)...)
|
||||||
|
if len(q.queue) == beforeLen+1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("Could not insert Song to the SongQueue.")
|
||||||
|
}
|
||||||
|
|
||||||
// CurrentSong returns the current Song.
|
// CurrentSong returns the current Song.
|
||||||
func (q *SongQueue) CurrentSong() Song {
|
func (q *SongQueue) CurrentSong() Song {
|
||||||
return q.queue[0]
|
return q.queue[0]
|
||||||
|
@ -64,7 +74,7 @@ func (q *SongQueue) NextSong() {
|
||||||
// PeekNext peeks at the next Song and returns it.
|
// PeekNext peeks at the next Song and returns it.
|
||||||
func (q *SongQueue) PeekNext() (Song, error) {
|
func (q *SongQueue) PeekNext() (Song, error) {
|
||||||
if q.Len() > 1 {
|
if q.Len() > 1 {
|
||||||
if dj.conf.General.AutomaticShuffleOn{ //Shuffle mode is active
|
if dj.conf.General.AutomaticShuffleOn { //Shuffle mode is active
|
||||||
q.RandomNextSong(false)
|
q.RandomNextSong(false)
|
||||||
}
|
}
|
||||||
return q.queue[1], nil
|
return q.queue[1], nil
|
||||||
|
@ -115,21 +125,21 @@ func (q *SongQueue) PrepareAndPlayNextSong() {
|
||||||
|
|
||||||
// Shuffles the songqueue using inside-out algorithm
|
// Shuffles the songqueue using inside-out algorithm
|
||||||
func (q *SongQueue) ShuffleSongs() {
|
func (q *SongQueue) ShuffleSongs() {
|
||||||
for i := range q.queue[1:] { //Don't touch currently playing song
|
for i := range q.queue[1:] { //Don't touch currently playing song
|
||||||
j := rand.Intn(i + 1)
|
j := rand.Intn(i + 1)
|
||||||
q.queue[i + 1], q.queue[j + 1] = q.queue[j + 1], q.queue[i + 1]
|
q.queue[i+1], q.queue[j+1] = q.queue[j+1], q.queue[i+1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets a random song as next song to be played
|
// Sets a random song as next song to be played
|
||||||
// queueWasEmpty wether the queue was empty before adding the last song
|
// queueWasEmpty wether the queue was empty before adding the last song
|
||||||
func (q *SongQueue) RandomNextSong(queueWasEmpty bool){
|
func (q *SongQueue) RandomNextSong(queueWasEmpty bool) {
|
||||||
if (q.Len() > 1){
|
if q.Len() > 1 {
|
||||||
nextSongIndex := 1
|
nextSongIndex := 1
|
||||||
if queueWasEmpty{
|
if queueWasEmpty {
|
||||||
nextSongIndex = 0
|
nextSongIndex = 0
|
||||||
}
|
}
|
||||||
swapIndex := nextSongIndex + rand.Intn(q.Len() - 1)
|
swapIndex := nextSongIndex + rand.Intn(q.Len()-1)
|
||||||
q.queue[nextSongIndex], q.queue[swapIndex] = q.queue[swapIndex], q.queue[nextSongIndex]
|
q.queue[nextSongIndex], q.queue[swapIndex] = q.queue[swapIndex], q.queue[nextSongIndex]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
strings.go
15
strings.go
|
@ -99,6 +99,16 @@ const PLAYLIST_ADDED_HTML = `
|
||||||
<b>%s</b> has added the playlist "%s" to the queue.
|
<b>%s</b> has added the playlist "%s" to the queue.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Message shown to channel when a song is added to the queue by a user after the current song.
|
||||||
|
const NEXT_SONG_ADDED_HTML = `
|
||||||
|
<b>%s</b> has added "%s" to the queue after the current song.
|
||||||
|
`
|
||||||
|
|
||||||
|
// Message shown to channel when a playlist is added to the queue by a user after the current song.
|
||||||
|
const NEXT_PLAYLIST_ADDED_HTML = `
|
||||||
|
<b>%s</b> has added the playlist "%s" to the queue after the current song.
|
||||||
|
`
|
||||||
|
|
||||||
// Message shown to channel when a song has been skipped.
|
// Message shown to channel when a song has been skipped.
|
||||||
const SONG_SKIPPED_HTML = `
|
const SONG_SKIPPED_HTML = `
|
||||||
The number of votes required for a skip has been met. <b>Skipping song!</b>
|
The number of votes required for a skip has been met. <b>Skipping song!</b>
|
||||||
|
@ -123,6 +133,7 @@ const HELP_HTML = `<br/>
|
||||||
<p><b>!currentsong</b> - Shows the title and submitter of the song currently playing.</p>
|
<p><b>!currentsong</b> - Shows the title and submitter of the song currently playing.</p>
|
||||||
<p style="-qt-paragraph-type:empty"><br/></p>
|
<p style="-qt-paragraph-type:empty"><br/></p>
|
||||||
<p><b>Admin Commands:</b></p>
|
<p><b>Admin Commands:</b></p>
|
||||||
|
<p><b>!addnext</b> - Adds songs/playlists to queue after the current song.</p>
|
||||||
<p><b>!reset</b> - An admin command that resets the song queue. </p>
|
<p><b>!reset</b> - An admin command that resets the song queue. </p>
|
||||||
<p><b>!forceskip</b> - An admin command that forces a song skip. </p>
|
<p><b>!forceskip</b> - An admin command that forces a song skip. </p>
|
||||||
<p><b>!forceskipplaylist</b> - An admin command that forces a playlist skip. </p>
|
<p><b>!forceskipplaylist</b> - An admin command that forces a playlist skip. </p>
|
||||||
|
@ -192,6 +203,8 @@ const CURRENT_SONG_HTML = `
|
||||||
const CURRENT_SONG_PLAYLIST_HTML = `
|
const CURRENT_SONG_PLAYLIST_HTML = `
|
||||||
The %s currently playing is "%s", added <b>%s</b> from the %s "%s".
|
The %s currently playing is "%s", added <b>%s</b> from the %s "%s".
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Message shown to user when the listsongs command is issued
|
||||||
const SONG_LIST_HTML = `
|
const SONG_LIST_HTML = `
|
||||||
<br>"%s", added by <b>%s</b<.</br>
|
<br>%d: "%s", added by <b>%s</b<.</br>
|
||||||
`
|
`
|
||||||
|
|
Reference in a new issue