feat: initial commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package lib
|
||||
|
||||
import "rul.sh/go-ytmp3/utils"
|
||||
|
||||
type Task struct {
|
||||
Url string `json:"url"`
|
||||
Slug string `json:"slug"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
IsPending bool `json:"is_pending"`
|
||||
Result string `json:"result"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
var tasks = []*Task{}
|
||||
var queue = []*Task{}
|
||||
|
||||
func NewTask(task Task) *Task {
|
||||
task.IsPending = true
|
||||
|
||||
queue = append(queue, &task)
|
||||
tasks = append(tasks, &task)
|
||||
if len(tasks) > 20 {
|
||||
tasks = tasks[1:]
|
||||
}
|
||||
|
||||
return &task
|
||||
}
|
||||
|
||||
func GetTasks() []*Task {
|
||||
return tasks
|
||||
}
|
||||
|
||||
type TaskScheduler struct {
|
||||
Ch chan bool
|
||||
}
|
||||
|
||||
func InitTaskScheduler() *TaskScheduler {
|
||||
ch := make(chan bool)
|
||||
outDir := utils.GetEnv("OUT_DIR", "/tmp")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
default:
|
||||
if len(queue) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
task := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
video, err := YtGetVideo(task.Url)
|
||||
if err != nil {
|
||||
task.Error = err
|
||||
task.IsPending = false
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := Yt2Mp3(&video, Yt2Mp3Options{
|
||||
OutDir: outDir,
|
||||
Slug: task.Slug,
|
||||
Thumbnail: task.Thumbnail,
|
||||
Title: task.Title,
|
||||
Artist: task.Artist,
|
||||
Album: task.Album,
|
||||
})
|
||||
|
||||
task.IsPending = false
|
||||
task.Error = err
|
||||
task.Result = result
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &TaskScheduler{Ch: ch}
|
||||
}
|
||||
|
||||
func (s *TaskScheduler) Stop() {
|
||||
s.Ch <- true
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gosimple/slug"
|
||||
ffmpeg "github.com/u2takey/ffmpeg-go"
|
||||
"github.com/wader/goutubedl"
|
||||
"golang.org/x/image/webp"
|
||||
"rul.sh/go-ytmp3/utils"
|
||||
)
|
||||
|
||||
func fetchVideo(video *goutubedl.Result, out string, ch chan error) {
|
||||
dl, err := video.Download(context.Background(), "best")
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
defer dl.Close()
|
||||
|
||||
f, err := os.Create(out)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
fmt.Println("Downloading...")
|
||||
if _, err := io.Copy(f, dl); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
ch <- nil
|
||||
}
|
||||
|
||||
func resizeImage(imgName string, src io.Reader, dst *os.File, width int, height int) error {
|
||||
var img image.Image
|
||||
var err error
|
||||
|
||||
ext := imgName[strings.LastIndex(imgName, "."):]
|
||||
|
||||
switch ext {
|
||||
case "webp":
|
||||
img, err = webp.Decode(src)
|
||||
default:
|
||||
img, _, err = image.Decode(src)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
img = imaging.Fill(img, width, height, imaging.Center, imaging.Lanczos)
|
||||
|
||||
return jpeg.Encode(dst, img, nil)
|
||||
}
|
||||
|
||||
func fetchThumbnail(video *goutubedl.Result, thumbnail string, ch chan error) {
|
||||
if video.Info.Thumbnail == "" {
|
||||
ch <- fmt.Errorf("no thumbnail found")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Downloading thumbnail...")
|
||||
|
||||
f, err := os.Create(thumbnail)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
resp, err := http.Get(video.Info.Thumbnail)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := resizeImage(video.Info.Thumbnail, resp.Body, f, 512, 512); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
ch <- nil
|
||||
}
|
||||
|
||||
type ConvertOptions struct {
|
||||
Video string
|
||||
Thumbnail string
|
||||
Title string
|
||||
Artist string
|
||||
Album string
|
||||
Output string
|
||||
}
|
||||
|
||||
func convertToMp3(data ConvertOptions, ch chan error) {
|
||||
fmt.Println("Converting...")
|
||||
|
||||
input := []*ffmpeg.Stream{ffmpeg.Input(data.Video).Audio()}
|
||||
args := ffmpeg.KwArgs{
|
||||
"format": "mp3",
|
||||
"id3v2_version": "3",
|
||||
"write_id3v1": "1",
|
||||
"metadata": []string{
|
||||
fmt.Sprintf("title=%s", data.Title),
|
||||
fmt.Sprintf("artist=%s", data.Artist),
|
||||
fmt.Sprintf("album=%s", data.Album),
|
||||
},
|
||||
}
|
||||
|
||||
if data.Thumbnail != "" {
|
||||
input = append(input, ffmpeg.Input(data.Thumbnail).Video())
|
||||
}
|
||||
|
||||
if err := ffmpeg.Output(input, data.Output, args).OverWriteOutput().Run(); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
ch <- nil
|
||||
}
|
||||
|
||||
func YtGetVideo(url string) (goutubedl.Result, error) {
|
||||
return goutubedl.New(context.Background(), url, goutubedl.Options{})
|
||||
}
|
||||
|
||||
type Yt2Mp3Options struct {
|
||||
OutDir string
|
||||
Slug string
|
||||
Thumbnail string
|
||||
Title string
|
||||
Artist string
|
||||
Album string
|
||||
}
|
||||
|
||||
func Yt2Mp3(video *goutubedl.Result, options Yt2Mp3Options) (string, error) {
|
||||
if video == nil {
|
||||
return "", fmt.Errorf("no video found")
|
||||
}
|
||||
|
||||
tmpDir := utils.GetEnv("TMP_DIR", "/tmp")
|
||||
|
||||
title := video.Info.Title
|
||||
artist := video.Info.Channel
|
||||
album := video.Info.Album
|
||||
|
||||
if video.Info.Artist != "" {
|
||||
artist = video.Info.Artist
|
||||
}
|
||||
|
||||
videoSlug := options.Slug
|
||||
if len(options.Slug) == 0 {
|
||||
videoSlug = slug.Make(title)
|
||||
}
|
||||
|
||||
videoSrc := fmt.Sprintf("%s/%s.mp4", tmpDir, videoSlug)
|
||||
thumbnail := fmt.Sprintf("%s/%s.jpg", tmpDir, videoSlug)
|
||||
out := fmt.Sprintf("%s/%s.mp3", options.OutDir, videoSlug)
|
||||
|
||||
if err := os.MkdirAll(options.OutDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
videoCh := make(chan error)
|
||||
thumbCh := make(chan error)
|
||||
|
||||
go fetchVideo(video, videoSrc, videoCh)
|
||||
go fetchThumbnail(video, thumbnail, thumbCh)
|
||||
|
||||
err := <-videoCh
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = <-thumbCh
|
||||
if err != nil {
|
||||
thumbnail = ""
|
||||
}
|
||||
|
||||
fmt.Println(artist, album)
|
||||
|
||||
convertCh := make(chan error)
|
||||
|
||||
go convertToMp3(ConvertOptions{
|
||||
Video: videoSrc,
|
||||
Thumbnail: thumbnail,
|
||||
Title: title,
|
||||
Artist: artist,
|
||||
Album: album,
|
||||
Output: out,
|
||||
}, convertCh)
|
||||
|
||||
err = <-convertCh
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user