gomu/queue.go

621 lines
12 KiB
Go
Raw Normal View History

2020-06-22 00:05:56 +08:00
// Copyright (C) 2020 Raziman
2020-06-19 16:22:20 +08:00
package main
import (
2020-07-10 16:52:37 +08:00
"bufio"
"crypto/sha1"
"encoding/hex"
2020-07-06 21:04:18 +08:00
"fmt"
2020-07-10 16:52:37 +08:00
"io/ioutil"
2020-07-14 01:16:30 +08:00
"math/rand"
2020-07-10 16:52:37 +08:00
"os"
"path"
"path/filepath"
2020-07-21 00:26:26 +08:00
"strings"
2020-07-10 22:06:13 +08:00
"time"
2020-06-26 12:54:48 +08:00
2021-01-21 01:30:16 +08:00
"github.com/gdamore/tcell/v2"
2020-06-19 16:22:20 +08:00
"github.com/rivo/tview"
2020-07-21 12:22:00 +08:00
"github.com/ztrue/tracerr"
"github.com/issadarkthing/gomu/player"
2020-06-19 16:22:20 +08:00
)
2021-03-25 12:01:12 +08:00
// Queue shows queued songs for playing
2020-06-26 12:54:48 +08:00
type Queue struct {
*tview.List
2020-07-23 15:15:39 +08:00
savedQueuePath string
items []*player.AudioFile
2020-07-23 15:15:39 +08:00
isLoop bool
2020-06-26 12:54:48 +08:00
}
2020-06-19 16:22:20 +08:00
2020-07-23 15:34:56 +08:00
// Highlight the next item in the queue
2020-06-26 12:54:48 +08:00
func (q *Queue) next() {
currIndex := q.GetCurrentItem()
idx := currIndex + 1
if currIndex == q.GetItemCount()-1 {
idx = 0
}
q.SetCurrentItem(idx)
}
2020-06-19 16:22:20 +08:00
2020-07-23 15:34:56 +08:00
// Highlight the previous item in the queue
2020-06-26 12:54:48 +08:00
func (q *Queue) prev() {
currIndex := q.GetCurrentItem()
q.SetCurrentItem(currIndex - 1)
}
2020-06-19 16:22:20 +08:00
2020-07-23 15:34:56 +08:00
// Usually used with GetCurrentItem which can return -1 if
// no item highlighted
func (q *Queue) deleteItem(index int) (*player.AudioFile, error) {
2020-07-20 21:48:13 +08:00
2020-07-23 15:15:39 +08:00
if index > len(q.items)-1 {
2020-07-21 12:22:00 +08:00
return nil, tracerr.New("Index out of range")
2020-07-20 21:48:13 +08:00
}
2020-07-16 22:52:12 +08:00
// deleted audio file
var dAudio *player.AudioFile
2020-07-16 22:52:12 +08:00
2020-06-26 12:54:48 +08:00
if index != -1 {
q.RemoveItem(index)
2020-07-10 22:06:13 +08:00
var nItems []*player.AudioFile
2020-07-10 22:06:13 +08:00
2020-07-23 15:15:39 +08:00
for i, v := range q.items {
2020-07-16 22:52:12 +08:00
2020-07-10 22:06:13 +08:00
if i == index {
2020-07-16 22:52:12 +08:00
dAudio = v
2020-07-10 22:06:13 +08:00
continue
}
nItems = append(nItems, v)
}
2020-07-23 15:15:39 +08:00
q.items = nItems
// here we move to next item if not at the end
if index < len(q.items) {
q.next()
}
2020-07-23 15:15:39 +08:00
q.updateTitle()
2020-07-10 22:06:13 +08:00
}
2020-07-16 22:52:12 +08:00
2020-07-20 21:48:13 +08:00
return dAudio, nil
2020-07-10 22:06:13 +08:00
}
2020-07-16 22:52:12 +08:00
// Update queue title which shows number of items and total length
2020-08-10 20:45:13 +08:00
func (q *Queue) updateTitle() string {
2020-07-10 22:06:13 +08:00
var totalLength time.Duration
2020-07-23 15:15:39 +08:00
for _, v := range q.items {
totalLength += v.Len()
2020-06-19 16:22:20 +08:00
}
2020-07-10 22:06:13 +08:00
2020-07-24 14:52:42 +08:00
fmtTime := fmtDurationH(totalLength)
2020-07-10 22:06:13 +08:00
2020-07-24 14:52:42 +08:00
var count string
if len(q.items) > 1 {
count = "songs"
} else {
count = "song"
}
2021-02-02 22:52:37 +08:00
var loop string
2021-02-18 10:23:15 +08:00
isEmoji := gomu.anko.GetBool("General.use_emoji")
2021-02-02 22:52:37 +08:00
if q.isLoop {
if isEmoji {
2021-02-18 10:23:15 +08:00
loop = gomu.anko.GetString("Emoji.loop")
} else {
loop = "Loop"
}
2021-02-02 22:52:37 +08:00
} else {
if isEmoji {
2021-02-18 10:23:15 +08:00
loop = gomu.anko.GetString("Emoji.noloop")
} else {
loop = "No loop"
}
2021-02-02 22:52:37 +08:00
}
title := fmt.Sprintf("─ Queue ───┤ %d %s | %s | %s ├",
len(q.items), count, fmtTime, loop)
2020-07-10 22:06:13 +08:00
2020-08-10 20:45:13 +08:00
q.SetTitle(title)
return title
2020-06-26 12:54:48 +08:00
}
2020-07-16 22:52:12 +08:00
// Add item to the front of the queue
func (q *Queue) pushFront(audioFile *player.AudioFile) {
2020-07-16 22:52:12 +08:00
q.items = append([]*player.AudioFile{audioFile}, q.items...)
2020-07-16 22:52:12 +08:00
songLength := audioFile.Len()
2020-07-16 22:52:12 +08:00
2020-07-20 21:48:13 +08:00
queueItemView := fmt.Sprintf(
"[ %s ] %s", fmtDuration(songLength), getName(audioFile.Name()),
2020-07-20 21:48:13 +08:00
)
q.InsertItem(0, queueItemView, audioFile.Path(), 0, nil)
2020-07-23 15:15:39 +08:00
q.updateTitle()
2020-07-16 22:52:12 +08:00
}
2020-06-26 12:54:48 +08:00
// gets the first item and remove it from the queue
// app.Draw() must be called after calling this function
func (q *Queue) dequeue() (*player.AudioFile, error) {
2020-06-19 16:22:20 +08:00
2020-06-26 12:54:48 +08:00
if q.GetItemCount() == 0 {
2020-07-21 12:22:00 +08:00
return nil, tracerr.New("Empty list")
2020-06-19 16:22:20 +08:00
}
2020-07-23 15:15:39 +08:00
first := q.items[0]
q.deleteItem(0)
q.updateTitle()
2020-06-26 12:54:48 +08:00
return first, nil
}
2020-07-06 21:04:18 +08:00
// Add item to the list and returns the length of the queue
func (q *Queue) enqueue(audioFile *player.AudioFile) (int, error) {
2020-07-06 21:04:18 +08:00
if !audioFile.IsAudioFile() {
2021-02-02 22:52:37 +08:00
return q.GetItemCount(), nil
}
2020-07-23 15:15:39 +08:00
q.items = append(q.items, audioFile)
songLength, err := getTagLength(audioFile.Path())
2020-07-06 21:04:18 +08:00
if err != nil {
2020-07-21 12:22:00 +08:00
return 0, tracerr.Wrap(err)
2020-07-06 21:04:18 +08:00
}
2020-07-20 21:48:13 +08:00
queueItemView := fmt.Sprintf(
"[ %s ] %s", fmtDuration(songLength), getName(audioFile.Name()),
2020-07-20 21:48:13 +08:00
)
q.AddItem(queueItemView, audioFile.Path(), 0, nil)
2020-07-23 15:15:39 +08:00
q.updateTitle()
2020-07-06 21:04:18 +08:00
2020-07-20 21:48:13 +08:00
return q.GetItemCount(), nil
2020-07-06 21:04:18 +08:00
}
2020-07-23 15:15:39 +08:00
// getItems is used to get the secondary text
// which is used to store the path of the audio file
// this is for the sake of convenience
2020-07-23 15:15:39 +08:00
func (q *Queue) getItems() []string {
2020-06-26 12:54:48 +08:00
items := []string{}
for i := 0; i < q.GetItemCount(); i++ {
_, second := q.GetItemText(i)
items = append(items, second)
}
return items
}
2020-08-18 16:09:12 +08:00
// Save the current queue
2021-04-06 14:55:40 +08:00
func (q *Queue) saveQueue() error {
2020-07-10 16:52:37 +08:00
2020-07-23 15:15:39 +08:00
songPaths := q.getItems()
2020-07-21 00:26:26 +08:00
var content strings.Builder
2021-01-31 22:38:49 +08:00
if gomu.player.HasInit() && gomu.player.GetCurrentSong() != nil {
2021-02-26 11:18:58 +08:00
currentSongPath := gomu.player.GetCurrentSong().Path()
2021-02-02 22:52:37 +08:00
currentSongInQueue := false
for _, songPath := range songPaths {
2021-02-03 14:53:46 +08:00
if getName(songPath) == getName(currentSongPath) {
2021-02-02 22:52:37 +08:00
currentSongInQueue = true
}
}
if !currentSongInQueue && len(q.items) != 0 {
2021-02-02 22:52:37 +08:00
hashed := sha1Hex(getName(currentSongPath))
content.WriteString(hashed + "\n")
}
}
2020-07-10 16:52:37 +08:00
for _, songPath := range songPaths {
2020-08-10 20:45:13 +08:00
// hashed song name is easier to search through
2020-07-23 15:15:39 +08:00
hashed := sha1Hex(getName(songPath))
2020-07-21 00:26:26 +08:00
content.WriteString(hashed + "\n")
2020-07-10 16:52:37 +08:00
}
2021-02-02 22:52:37 +08:00
2020-08-10 20:45:13 +08:00
savedPath := expandTilde(q.savedQueuePath)
err := ioutil.WriteFile(savedPath, []byte(content.String()), 0644)
2020-07-10 16:52:37 +08:00
if err != nil {
2020-07-21 12:22:00 +08:00
return tracerr.Wrap(err)
2020-07-10 16:52:37 +08:00
}
return nil
}
2020-07-10 22:06:13 +08:00
// Clears current queue
2020-07-23 15:15:39 +08:00
func (q *Queue) clearQueue() {
2020-07-10 22:06:13 +08:00
q.items = []*player.AudioFile{}
2020-07-10 22:06:13 +08:00
q.Clear()
2020-07-23 15:15:39 +08:00
q.updateTitle()
2020-07-10 22:06:13 +08:00
}
2020-07-10 16:52:37 +08:00
// Loads previously saved list
2020-07-23 15:15:39 +08:00
func (q *Queue) loadQueue() error {
2020-07-10 16:52:37 +08:00
2020-07-23 15:15:39 +08:00
songs, err := q.getSavedQueue()
2020-07-10 16:52:37 +08:00
if err != nil {
2020-07-21 12:22:00 +08:00
return tracerr.Wrap(err)
2020-07-10 16:52:37 +08:00
}
for _, v := range songs {
2020-07-23 15:15:39 +08:00
audioFile, err := gomu.playlist.findAudioFile(v)
2020-07-10 16:52:37 +08:00
2020-07-21 12:22:00 +08:00
if err != nil {
2020-07-23 15:15:39 +08:00
logError(err)
2020-07-21 12:22:00 +08:00
continue
2020-07-10 16:52:37 +08:00
}
2020-07-21 12:22:00 +08:00
2020-07-23 15:15:39 +08:00
q.enqueue(audioFile)
2020-07-10 16:52:37 +08:00
}
return nil
}
// Get saved queue, if not exist, create it
2020-07-23 15:15:39 +08:00
func (q *Queue) getSavedQueue() ([]string, error) {
2020-07-10 16:52:37 +08:00
2020-07-23 15:15:39 +08:00
queuePath := expandTilde(q.savedQueuePath)
2020-07-10 16:52:37 +08:00
if _, err := os.Stat(queuePath); os.IsNotExist(err) {
dir, _ := path.Split(queuePath)
err := os.MkdirAll(dir, 0744)
if err != nil {
2020-07-21 12:22:00 +08:00
return nil, tracerr.Wrap(err)
2020-07-10 16:52:37 +08:00
}
_, err = os.Create(queuePath)
if err != nil {
2020-07-21 12:22:00 +08:00
return nil, tracerr.Wrap(err)
2020-07-10 16:52:37 +08:00
}
return []string{}, nil
}
f, err := os.Open(queuePath)
if err != nil {
2020-07-21 12:22:00 +08:00
return nil, tracerr.Wrap(err)
2020-07-10 16:52:37 +08:00
}
records := []string{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
records = append(records, scanner.Text())
}
if err := scanner.Err(); err != nil {
2020-07-21 12:22:00 +08:00
return nil, tracerr.Wrap(err)
2020-07-10 16:52:37 +08:00
}
return records, nil
}
2020-07-23 15:15:39 +08:00
func (q *Queue) help() []string {
2020-07-17 15:34:50 +08:00
return []string{
"j down",
"k up",
"l play selected song",
"d remove from queue",
"D clear queue",
"z toggle loop",
"s shuffle",
"/ find in queue",
"t lyric delay increase 0.5 second",
"r lyric delay decrease 0.5 second",
2020-07-17 15:34:50 +08:00
}
}
// Shuffles the queue
2020-07-23 15:15:39 +08:00
func (q *Queue) shuffle() {
2020-07-14 01:16:30 +08:00
rand.Seed(time.Now().UnixNano())
2020-07-23 15:15:39 +08:00
rand.Shuffle(len(q.items), func(i, j int) {
q.items[i], q.items[j] = q.items[j], q.items[i]
2020-07-20 21:48:13 +08:00
})
2020-07-14 01:16:30 +08:00
q.Clear()
2020-07-23 15:15:39 +08:00
for _, v := range q.items {
audioLen, err := getTagLength(v.Path())
2020-07-22 21:01:13 +08:00
if err != nil {
2020-07-23 15:15:39 +08:00
logError(err)
2020-07-22 21:01:13 +08:00
}
2020-07-14 01:16:30 +08:00
queueText := fmt.Sprintf("[ %s ] %s", fmtDuration(audioLen), v.Name())
q.AddItem(queueText, v.Path(), 0, nil)
2020-07-14 01:16:30 +08:00
}
2021-02-02 00:00:13 +08:00
// q.updateTitle()
2020-07-14 01:16:30 +08:00
}
2020-07-10 16:52:37 +08:00
// Initiliaze new queue with default values
2020-07-23 15:15:39 +08:00
func newQueue() *Queue {
2020-06-26 12:54:48 +08:00
2021-03-15 22:35:15 +08:00
list := tview.NewList()
cacheDir, err := os.UserCacheDir()
if err != nil {
logError(err)
}
cacheQueuePath := filepath.Join(cacheDir, "gomu", "queue.cache")
2020-07-10 16:52:37 +08:00
queue := &Queue{
List: list,
2021-02-15 11:56:02 +08:00
savedQueuePath: cacheQueuePath,
2020-07-10 16:52:37 +08:00
}
2020-06-26 12:54:48 +08:00
2021-03-17 21:46:23 +08:00
cmds := map[rune]string{
'j': "move_down",
'k': "move_up",
'd': "delete_item",
'D': "clear_queue",
'l': "play_selected",
'z': "toggle_loop",
's': "shuffle_queue",
'/': "queue_search",
't': "lyric_delay_increase",
'r': "lyric_delay_decrease",
}
for key, cmdName := range cmds {
src := fmt.Sprintf(`Keybinds.def_q("%c", %s)`, key, cmdName)
2021-03-19 23:10:16 +08:00
gomu.anko.Execute(src)
2021-03-17 21:46:23 +08:00
}
2020-06-26 12:54:48 +08:00
queue.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
2020-06-19 16:22:20 +08:00
if gomu.anko.KeybindExists("queue", e) {
2021-02-15 11:33:04 +08:00
err := gomu.anko.ExecKeybind("queue", e)
if err != nil {
errorPopup(err)
}
2020-06-19 16:42:30 +08:00
}
2020-06-19 16:22:20 +08:00
2020-06-19 16:42:30 +08:00
return nil
})
2020-06-19 16:22:20 +08:00
2020-07-23 15:15:39 +08:00
queue.updateTitle()
2021-03-15 22:35:15 +08:00
2020-07-10 22:06:13 +08:00
queue.
2021-03-15 22:35:15 +08:00
ShowSecondaryText(false).
2021-03-16 11:47:35 +08:00
SetSelectedBackgroundColor(gomu.colors.queueHi).
SetSelectedTextColor(gomu.colors.foreground).
2021-03-15 22:35:15 +08:00
SetHighlightFullLine(true)
queue.
SetBorder(true).
SetTitleAlign(tview.AlignLeft).
SetBorderPadding(0, 0, 1, 1).
SetBorderColor(gomu.colors.foreground).
SetBackgroundColor(gomu.colors.background)
2020-06-19 16:22:20 +08:00
2020-06-26 12:54:48 +08:00
return queue
2020-06-19 16:22:20 +08:00
}
2020-07-10 16:52:37 +08:00
2020-07-24 14:52:42 +08:00
// Convert string to sha1.
2020-07-23 15:15:39 +08:00
func sha1Hex(input string) string {
2020-07-10 16:52:37 +08:00
h := sha1.New()
h.Write([]byte(input))
return hex.EncodeToString(h.Sum(nil))
}
2021-02-15 11:55:34 +08:00
// Modify the title of songs in queue
func (q *Queue) renameItem(oldAudio *player.AudioFile, newAudio *player.AudioFile) error {
2021-04-01 15:24:31 +08:00
for i, v := range q.items {
if v.Name() != oldAudio.Name() {
2021-04-01 15:24:31 +08:00
continue
}
err := q.insertItem(i, newAudio)
if err != nil {
return tracerr.Wrap(err)
}
_, err = q.deleteItem(i + 1)
2021-04-01 15:24:31 +08:00
if err != nil {
return tracerr.Wrap(err)
}
}
2021-02-02 22:52:37 +08:00
return nil
2021-02-02 00:00:13 +08:00
}
// playQueue play the first item in the queue
func (q *Queue) playQueue() error {
audioFile, err := q.dequeue()
if err != nil {
return tracerr.Wrap(err)
}
err = gomu.player.Run(audioFile)
if err != nil {
return tracerr.Wrap(err)
}
return nil
}
func (q *Queue) insertItem(index int, audioFile *player.AudioFile) error {
if index > len(q.items)-1 {
return tracerr.New("Index out of range")
}
if index != -1 {
songLength, err := getTagLength(audioFile.Path())
if err != nil {
return tracerr.Wrap(err)
}
queueItemView := fmt.Sprintf(
"[ %s ] %s", fmtDuration(songLength), getName(audioFile.Name()),
)
q.InsertItem(index, queueItemView, audioFile.Path(), 0, nil)
var nItems []*player.AudioFile
for i, v := range q.items {
if i == index {
nItems = append(nItems, audioFile)
}
nItems = append(nItems, v)
}
q.items = nItems
q.updateTitle()
}
return nil
}
2021-04-03 18:55:54 +08:00
// update the path information in queue
func (q *Queue) updateQueuePath() {
2021-04-03 18:55:54 +08:00
var songs []string
if len(q.items) < 1 {
return
2021-04-03 18:55:54 +08:00
}
for _, v := range q.items {
song := sha1Hex(getName(v.Name()))
2021-04-03 18:55:54 +08:00
songs = append(songs, song)
}
q.clearQueue()
for _, v := range songs {
audioFile, err := gomu.playlist.findAudioFile(v)
if err != nil {
continue
}
q.enqueue(audioFile)
}
q.updateTitle()
2021-04-03 18:55:54 +08:00
}
// update current playing song name to reflect the changes during rename and paste
func (q *Queue) updateCurrentSongName(oldAudio *player.AudioFile, newAudio *player.AudioFile) error {
if !gomu.player.IsRunning() && !gomu.player.IsPaused() {
return nil
}
currentSong := gomu.player.GetCurrentSong()
position := gomu.playingBar.getProgress()
paused := gomu.player.IsPaused()
if oldAudio.Name() != currentSong.Name() {
return nil
}
// we insert it in the first of queue, then play it
gomu.queue.pushFront(newAudio)
tmpLoop := q.isLoop
q.isLoop = false
gomu.player.Skip()
gomu.player.Seek(position)
if paused {
gomu.player.TogglePause()
}
q.isLoop = tmpLoop
q.updateTitle()
return nil
}
// update current playing song path to reflect the changes during rename and paste
func (q *Queue) updateCurrentSongPath(oldAudio *player.AudioFile, newAudio *player.AudioFile) error {
if !gomu.player.IsRunning() && !gomu.player.IsPaused() {
return nil
}
currentSong := gomu.player.GetCurrentSong()
position := gomu.playingBar.getProgress()
paused := gomu.player.IsPaused()
// Here we check the situation when currentsong is under oldAudio folder
if !strings.Contains(currentSong.Path(), oldAudio.Path()) {
return nil
}
// Here is the handling of folder rename and paste
currentSongAudioFile, err := gomu.playlist.findAudioFile(sha1Hex(getName(currentSong.Name())))
if err != nil {
return tracerr.Wrap(err)
}
gomu.queue.pushFront(currentSongAudioFile)
tmpLoop := q.isLoop
q.isLoop = false
gomu.player.Skip()
gomu.player.Seek(position)
if paused {
gomu.player.TogglePause()
}
q.isLoop = tmpLoop
q.updateTitle()
return nil
}
// update current playing song simply delete it
func (q *Queue) updateCurrentSongDelete(oldAudio *player.AudioFile) {
if !gomu.player.IsRunning() && !gomu.player.IsPaused() {
return
}
currentSong := gomu.player.GetCurrentSong()
paused := gomu.player.IsPaused()
var delete bool
if oldAudio.IsAudioFile() {
if oldAudio.Name() == currentSong.Name() {
delete = true
}
} else {
if strings.Contains(currentSong.Path(), oldAudio.Path()) {
delete = true
}
}
if !delete {
return
}
tmpLoop := q.isLoop
q.isLoop = false
gomu.player.Skip()
if paused {
gomu.player.TogglePause()
}
q.isLoop = tmpLoop
q.updateTitle()
}