mirror of
https://github.com/issadarkthing/gomu.git
synced 2025-04-25 13:48:49 +08:00
379 lines
8.4 KiB
Go
379 lines
8.4 KiB
Go
// Copyright (C) 2020 Raziman
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tramhao/id3v2"
|
|
"github.com/ztrue/tracerr"
|
|
|
|
"github.com/issadarkthing/gomu/lyric"
|
|
"github.com/issadarkthing/gomu/player"
|
|
)
|
|
|
|
// logError logs the error message.
|
|
func logError(err error) {
|
|
log.Println("[ERROR]", tracerr.Sprint(err))
|
|
}
|
|
|
|
func logDebug(msg string) {
|
|
log.Println("[DEBUG]", msg)
|
|
}
|
|
|
|
// die logs the error message and call os.Exit(1)
|
|
// prefer this instead of panic
|
|
func die(err error) {
|
|
logError(err)
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Formats duration to my desired output mm:ss
|
|
func fmtDuration(input time.Duration) string {
|
|
|
|
val := input.Round(time.Second).String()
|
|
|
|
if !strings.Contains(val, "m") {
|
|
val = "0m" + val
|
|
}
|
|
val = strings.ReplaceAll(val, "h", ":")
|
|
val = strings.ReplaceAll(val, "m", ":")
|
|
val = strings.ReplaceAll(val, "s", "")
|
|
var result []string
|
|
|
|
for _, v := range strings.Split(val, ":") {
|
|
|
|
if len(v) < 2 {
|
|
result = append(result, "0"+v)
|
|
} else {
|
|
result = append(result, v)
|
|
}
|
|
|
|
}
|
|
|
|
return strings.Join(result, ":")
|
|
}
|
|
|
|
// fmtDurationH returns the formatted duration `x hr x min`
|
|
func fmtDurationH(input time.Duration) string {
|
|
|
|
re := regexp.MustCompile(`\d+s`)
|
|
val := input.Round(time.Second).String()
|
|
|
|
// remove seconds
|
|
result := re.ReplaceAllString(val, "")
|
|
result = strings.Replace(result, "h", " hr ", 1)
|
|
result = strings.Replace(result, "m", " min", 1)
|
|
|
|
if result == "" {
|
|
return "0 hr 0 min"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Expands relative path to absolute path and tilde to /home/(user)
|
|
func expandFilePath(path string) string {
|
|
p := expandTilde(path)
|
|
|
|
if filepath.IsAbs(p) {
|
|
return p
|
|
}
|
|
|
|
p, err := filepath.Abs(p)
|
|
if err != nil {
|
|
die(err)
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
// Expands tilde alias to /home/user
|
|
func expandTilde(_path string) string {
|
|
if !strings.HasPrefix(_path, "~") {
|
|
return _path
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
die(err)
|
|
}
|
|
|
|
return path.Join(home, strings.TrimPrefix(_path, "~"))
|
|
}
|
|
|
|
// Detects the filetype of file
|
|
func getFileContentType(out *os.File) (string, error) {
|
|
|
|
buffer := make([]byte, 512)
|
|
|
|
_, err := out.Read(buffer)
|
|
if err != nil {
|
|
return "", tracerr.Wrap(err)
|
|
}
|
|
|
|
contentType := http.DetectContentType(buffer)
|
|
|
|
return strings.SplitAfter(contentType, "/")[1], nil
|
|
}
|
|
|
|
// Gets the file name by removing extension and path
|
|
func getName(fn string) string {
|
|
return strings.TrimSuffix(path.Base(fn), ".mp3")
|
|
}
|
|
|
|
// This just parsing the output from the ytdl to get the audio path
|
|
// This is used because we need to get the song name
|
|
// example ~/path/to/song/song.mp3
|
|
func extractFilePath(output []byte, dir string) string {
|
|
|
|
regexSearch := fmt.Sprintf(`\[ffmpeg\] Destination: %s\/.*.mp3`,
|
|
escapeBackSlash(dir))
|
|
|
|
parseAudioPathOnly := regexp.MustCompile(`\/.*mp3$`)
|
|
|
|
re := regexp.MustCompile(regexSearch)
|
|
|
|
return string(parseAudioPathOnly.Find(re.Find(output)))
|
|
|
|
}
|
|
|
|
func escapeBackSlash(input string) string {
|
|
return strings.ReplaceAll(input, "/", `\/`)
|
|
}
|
|
|
|
// progresStr creates a simple progress bar
|
|
// example: =====-----
|
|
func progresStr(progress, maxProgress, maxLength int,
|
|
fill, empty string) string {
|
|
|
|
currLength := maxLength * progress / maxProgress
|
|
|
|
return fmt.Sprintf("%s%s",
|
|
strings.Repeat(fill, currLength),
|
|
strings.Repeat(empty, maxLength-currLength),
|
|
)
|
|
}
|
|
|
|
// padHex pad the neccessary 0 to create six hex digit
|
|
func padHex(r, g, b int32) string {
|
|
|
|
var result strings.Builder
|
|
|
|
for _, v := range []int32{r, g, b} {
|
|
hex := fmt.Sprintf("%x", v)
|
|
|
|
if len(hex) == 1 {
|
|
result.WriteString(fmt.Sprintf("0%s", hex))
|
|
} else {
|
|
result.WriteString(hex)
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
func validHexColor(color string) bool {
|
|
reg := regexp.MustCompile(`^#([A-Fa-f0-9]{6})$`)
|
|
return reg.MatchString(color)
|
|
}
|
|
|
|
func contains(needle int, haystack []int) bool {
|
|
for _, i := range haystack {
|
|
if needle == i {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// appendFile appends to a file, create the file if not exists
|
|
func appendFile(path string, content string) error {
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
if err != os.ErrNotExist {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
// create the neccessary parent directory
|
|
err = os.MkdirAll(filepath.Dir(expandFilePath(path)), os.ModePerm)
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
}
|
|
defer f.Close()
|
|
if _, err := f.WriteString(content); err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func shell(input string) (string, error) {
|
|
|
|
args := strings.Split(input, " ")
|
|
for i, arg := range args {
|
|
args[i] = strings.Trim(arg, " ")
|
|
}
|
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if stderr.Len() != 0 {
|
|
return "", errors.New(stderr.String())
|
|
}
|
|
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
func embedLyric(songPath string, lyricTobeWritten *lyric.Lyric, isDelete bool) (err error) {
|
|
|
|
var tag *id3v2.Tag
|
|
tag, err = id3v2.Open(songPath, id3v2.Options{Parse: true})
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
defer tag.Close()
|
|
usltFrames := tag.GetFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
|
|
tag.DeleteFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
|
|
// We delete the lyric frame with same language by delete all and add others back
|
|
for _, f := range usltFrames {
|
|
uslf, ok := f.(id3v2.UnsynchronisedLyricsFrame)
|
|
if !ok {
|
|
die(errors.New("uslt error"))
|
|
}
|
|
if uslf.ContentDescriptor == lyricTobeWritten.LangExt {
|
|
continue
|
|
}
|
|
tag.AddUnsynchronisedLyricsFrame(uslf)
|
|
}
|
|
syltFrames := tag.GetFrames(tag.CommonID("Synchronised lyrics/text"))
|
|
tag.DeleteFrames(tag.CommonID("Synchronised lyrics/text"))
|
|
for _, f := range syltFrames {
|
|
sylf, ok := f.(id3v2.SynchronisedLyricsFrame)
|
|
if !ok {
|
|
die(errors.New("sylt error"))
|
|
}
|
|
if strings.Contains(sylf.ContentDescriptor, lyricTobeWritten.LangExt) {
|
|
continue
|
|
}
|
|
tag.AddSynchronisedLyricsFrame(sylf)
|
|
}
|
|
|
|
if !isDelete {
|
|
tag.AddUnsynchronisedLyricsFrame(id3v2.UnsynchronisedLyricsFrame{
|
|
Encoding: id3v2.EncodingUTF8,
|
|
Language: "eng",
|
|
ContentDescriptor: lyricTobeWritten.LangExt,
|
|
Lyrics: lyricTobeWritten.AsLRC(),
|
|
})
|
|
var lyric lyric.Lyric
|
|
err := lyric.NewFromLRC(lyricTobeWritten.AsLRC())
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
tag.AddSynchronisedLyricsFrame(id3v2.SynchronisedLyricsFrame{
|
|
Encoding: id3v2.EncodingUTF8,
|
|
Language: "eng",
|
|
TimestampFormat: 2,
|
|
ContentType: 1,
|
|
ContentDescriptor: lyricTobeWritten.LangExt,
|
|
SynchronizedTexts: lyric.SyncedCaptions,
|
|
})
|
|
|
|
}
|
|
|
|
err = tag.Save()
|
|
if err != nil {
|
|
return tracerr.Wrap(err)
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
func embedLength(songPath string) (time.Duration, error) {
|
|
tag, err := id3v2.Open(songPath, id3v2.Options{Parse: true})
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
defer tag.Close()
|
|
|
|
var lengthSongTimeDuration time.Duration
|
|
lengthSongTimeDuration, err = player.GetLength(songPath)
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
|
|
lengthSongString := strconv.FormatInt(lengthSongTimeDuration.Milliseconds(), 10)
|
|
lengthFrame := id3v2.UserDefinedTextFrame{
|
|
Encoding: id3v2.EncodingUTF8,
|
|
Description: "TLEN",
|
|
Value: lengthSongString,
|
|
}
|
|
tag.AddUserDefinedTextFrame(lengthFrame)
|
|
|
|
err = tag.Save()
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
return lengthSongTimeDuration, err
|
|
}
|
|
|
|
func getTagLength(songPath string) (songLength time.Duration, err error) {
|
|
var tag *id3v2.Tag
|
|
tag, err = id3v2.Open(songPath, id3v2.Options{Parse: true})
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
defer tag.Close()
|
|
tlenFrames := tag.GetFrames(tag.CommonID("User defined text information frame"))
|
|
if tlenFrames == nil {
|
|
songLength, err = embedLength(songPath)
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
return songLength, nil
|
|
}
|
|
for _, tlenFrame := range tlenFrames {
|
|
if tlenFrame.(id3v2.UserDefinedTextFrame).Description == "TLEN" {
|
|
songLengthString := tlenFrame.(id3v2.UserDefinedTextFrame).Value
|
|
songLengthInt64, err := strconv.ParseInt(songLengthString, 10, 64)
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
songLength = (time.Duration)(songLengthInt64) * time.Millisecond
|
|
break
|
|
}
|
|
}
|
|
if songLength != 0 {
|
|
return songLength, nil
|
|
}
|
|
songLength, err = embedLength(songPath)
|
|
if err != nil {
|
|
return 0, tracerr.Wrap(err)
|
|
}
|
|
|
|
return songLength, err
|
|
}
|