init
This commit is contained in:
234
config/config.go
Normal file
234
config/config.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"git.nobrain.org/r4/dischord/extractor"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenNotSet = errors.New("bot token not set")
|
||||
ErrInvalidYoutubeDlPath = errors.New("invalid youtube-dl path")
|
||||
ErrInvalidFfmpegPath = errors.New("invalid FFmpeg path")
|
||||
ErrYoutubeDlNotFound = errors.New("youtube-dl not found, please install it from https://youtube-dl.org/ first")
|
||||
ErrFfmpegNotFound = errors.New("FFmpeg not found, please install it from https://ffmpeg.org first")
|
||||
ErrPythonNotInstalled = errors.New("python not installed")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Token string `toml:"bot-token"`
|
||||
FfmpegPath string `toml:"ffmpeg-path"`
|
||||
Extractors extractor.Config `toml:"extractors"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultToken = "insert your Discord bot token here"
|
||||
)
|
||||
|
||||
var (
|
||||
ffmpegPaths = []string{"ffmpeg", "./ffmpeg"}
|
||||
youtubeDlPaths = []string{"youtube-dl", "./youtube-dl", "yt-dlp", "./yt-dlp", "youtube-dlc", "./youtube-dlc"}
|
||||
)
|
||||
|
||||
// Returns a valid path if one exists. Returns "" if none found.
|
||||
func searchExecPaths(paths ...string) string {
|
||||
for _, pathbase := range paths {
|
||||
path := pathbase
|
||||
if runtime.GOOS == "windows" {
|
||||
path += ".exe"
|
||||
}
|
||||
if _, err := exec.LookPath(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func write(filename string, cfg *Config) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := file.WriteString(`# Insert your Discord bot token here.
|
||||
# It can be found at https://discord.com/developers/applications -> <your application> -> Bot -> Reset Token.
|
||||
# Make sure to keep the "" around your token text.
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
enc := toml.NewEncoder(file)
|
||||
enc.Indent = ""
|
||||
if err := enc.Encode(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func macosEnableExecutable(filename string) error {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return nil
|
||||
}
|
||||
// Commands from http://www.osxexperts.net/
|
||||
if runtime.GOARCH == "arm64" {
|
||||
cmd := exec.Command("xattr", "-cr", filename)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd = exec.Command("codesign", "-s", "-", filename)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if runtime.GOARCH == "amd64" {
|
||||
cmd := exec.Command("xattr", "-dr", "com.apple.quarantine", filename)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tries to load the given TOML config file. Returns an error if the
|
||||
// configuration file does not exist or is invalid.
|
||||
func Load(filename string) (*Config, error) {
|
||||
cfg := &Config{}
|
||||
_, err := toml.DecodeFile(filename, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.Token == defaultToken || cfg.Token == "" {
|
||||
return nil, ErrTokenNotSet
|
||||
}
|
||||
if err := cfg.Extractors.CheckTypes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := exec.LookPath(cfg.Extractors["youtube-dl"]["youtube-dl-path"].(string)); err != nil {
|
||||
return nil, ErrInvalidYoutubeDlPath
|
||||
}
|
||||
if _, err := exec.LookPath(cfg.FfmpegPath); err != nil {
|
||||
return nil, ErrInvalidFfmpegPath
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Automatically creates a TOML configuration file with the default values and
|
||||
// prints information and instructions for the user to stdout.
|
||||
func Autoconf(filename string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Token: defaultToken,
|
||||
Extractors: extractor.DefaultConfig(),
|
||||
}
|
||||
|
||||
download := func(executable bool, urlsByOS map[string]map[string]string) (filename string, err error) {
|
||||
filename, err = download(executable, urlsByOS, func(progress float32){
|
||||
fmt.Printf("Progress: %.1f%%\r", progress*100.0)
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println()
|
||||
return "", err
|
||||
} else {
|
||||
fmt.Println("Progress: Finished downloading")
|
||||
}
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
python3IsPython := false
|
||||
if runtime.GOOS != "windows" {
|
||||
if _, err := exec.LookPath("python"); err != nil {
|
||||
if _, err := exec.LookPath("python3"); err == nil {
|
||||
python3IsPython = true
|
||||
} else {
|
||||
return nil, ErrPythonNotInstalled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
youtubeDlPath := searchExecPaths(youtubeDlPaths...)
|
||||
if youtubeDlPath == "" {
|
||||
fmt.Println("Downloading youtube-dl")
|
||||
filename, err := download(true, map[string]map[string]string{
|
||||
"windows": {
|
||||
"amd64": "https://yt-dl.org/downloads/latest/youtube-dl.exe",
|
||||
"386": "https://yt-dl.org/downloads/latest/youtube-dl.exe",
|
||||
},
|
||||
"any": {
|
||||
"any": "https://yt-dl.org/downloads/latest/youtube-dl",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
youtubeDlPath = "./"+filename
|
||||
macosEnableExecutable(youtubeDlPath)
|
||||
if python3IsPython {
|
||||
// Replace first line with `replacement`
|
||||
data, err := os.ReadFile(youtubeDlPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
replacement := []byte("#!/usr/bin/env python3")
|
||||
for i, c := range data {
|
||||
if c == '\n' {
|
||||
data = append(replacement, data[i:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(youtubeDlPath, data, 0777); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Using youtube-dl executable found at", youtubeDlPath)
|
||||
}
|
||||
cfg.Extractors["youtube-dl"]["youtube-dl-path"] = youtubeDlPath
|
||||
|
||||
cfg.FfmpegPath = searchExecPaths(ffmpegPaths...)
|
||||
if cfg.FfmpegPath == "" {
|
||||
targetFile := "ffmpeg"
|
||||
if runtime.GOOS == "windows" {
|
||||
targetFile += ".exe"
|
||||
}
|
||||
fmt.Println("Downloading FFmpeg")
|
||||
filename, err := download(false, map[string]map[string]string{
|
||||
"linux": {
|
||||
"amd64": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
|
||||
"386": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-i686-static.tar.xz",
|
||||
"arm64": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz",
|
||||
"arm": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-armhf-static.tar.xz",
|
||||
},
|
||||
"windows": {
|
||||
"amd64": "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
|
||||
"386": "https://github.com/sudo-nautilus/FFmpeg-Builds-Win32/releases/download/latest/ffmpeg-n5.1-latest-win32-gpl-5.1.zip",
|
||||
},
|
||||
"darwin": {
|
||||
"amd64": "https://evermeet.cx/ffmpeg/getrelease/zip",
|
||||
"arm64": "https://www.osxexperts.net/FFmpeg511ARM.zip",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("Unpacking", targetFile, "from", filename)
|
||||
if err := unarchiveSingleFile(filename, targetFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.Remove(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.FfmpegPath = "./"+targetFile
|
||||
macosEnableExecutable(cfg.FfmpegPath)
|
||||
} else {
|
||||
fmt.Println("Using FFmpeg executable found at", cfg.FfmpegPath)
|
||||
}
|
||||
|
||||
fmt.Println("Writing configuration to", filename)
|
||||
write(filename, cfg)
|
||||
fmt.Println("Almost done. Now just edit", filename, "and set your bot token.")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
191
config/util.go
Normal file
191
config/util.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/ulikunitz/xz"
|
||||
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedOSAndArch = errors.New("no download available for your operating system and hardware architecture")
|
||||
ErrFileNotFoundInArchive = errors.New("file not found in archive")
|
||||
ErrUnsupportedArchive = errors.New("unsupported archive format (supported are .tar, .tar.gz, .tar.xz and .zip")
|
||||
)
|
||||
|
||||
func download(executable bool, urlsByOS map[string]map[string]string, progCallback func(progress float32)) (filename string, err error) {
|
||||
// Find appropriate URL
|
||||
var url string
|
||||
var urlByArch map[string]string
|
||||
var ok bool
|
||||
if urlByArch, ok = urlsByOS[runtime.GOOS]; !ok {
|
||||
urlByArch, ok = urlsByOS["any"]
|
||||
}
|
||||
if ok {
|
||||
if url, ok = urlByArch[runtime.GOARCH]; !ok {
|
||||
url, ok = urlByArch["any"]
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return "", ErrUnsupportedOSAndArch
|
||||
}
|
||||
|
||||
// Initiate request
|
||||
lastPath := url
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Get the filename to save the downloaded file to
|
||||
var savePath string
|
||||
if v := resp.Header.Get("Content-Disposition"); v != "" {
|
||||
disposition, params, err := mime.ParseMediaType(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if disposition == "attachment" {
|
||||
lastPath = params["filename"]
|
||||
}
|
||||
}
|
||||
if savePath == "" {
|
||||
sp := strings.Split(lastPath, "/")
|
||||
savePath = sp[len(sp)-1]
|
||||
}
|
||||
|
||||
// Download resource
|
||||
size, _ := strconv.Atoi(resp.Header.Get("content-length"))
|
||||
var perms uint32 = 0666
|
||||
if executable {
|
||||
perms = 0777
|
||||
}
|
||||
file, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.FileMode(perms))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := 0; ; i += 100_000 {
|
||||
_, err := io.CopyN(file, resp.Body, 100_000)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if progCallback != nil && size != 0 {
|
||||
progCallback(float32(i)/float32(size))
|
||||
}
|
||||
}
|
||||
return savePath, nil
|
||||
}
|
||||
|
||||
func unarchiveSingleFile(archive, target string) error {
|
||||
unzip := func() error {
|
||||
ar, err := zip.OpenReader(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ar.Close()
|
||||
|
||||
found := false
|
||||
for _, file := range ar.File {
|
||||
if !file.FileInfo().IsDir() && filepath.Base(file.Name) == target {
|
||||
found = true
|
||||
dstFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close()
|
||||
if _, err := io.Copy(dstFile, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ErrFileNotFoundInArchive
|
||||
}
|
||||
return nil
|
||||
}
|
||||
untar := func(rd io.Reader) error {
|
||||
ar := tar.NewReader(rd)
|
||||
for {
|
||||
hdr, err := ar.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hdr.Typeflag == tar.TypeReg && filepath.Base(hdr.Name) == target {
|
||||
dstFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.FileMode(hdr.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
if _, err := io.Copy(dstFile, ar); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrFileNotFoundInArchive
|
||||
}
|
||||
match := func(name string, patterns ...string) bool {
|
||||
for _, pattern := range patterns {
|
||||
matches, err := filepath.Match(pattern, archive)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if match(archive, "*.zip") {
|
||||
return unzip()
|
||||
} else if match(archive, "*.tar", "*.tar.[gx]z") {
|
||||
file, err := os.Open(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
var uncompressedFile io.Reader
|
||||
if match(archive, "*.tar") {
|
||||
uncompressedFile = file
|
||||
} else if match(archive, "*.tar.gz") {
|
||||
gz, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gz.Close()
|
||||
uncompressedFile = gz
|
||||
} else if match(archive, "*.tar.xz") {
|
||||
uncompressedFile, err = xz.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return untar(uncompressedFile)
|
||||
} else {
|
||||
return ErrUnsupportedArchive
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user