forked from go/golangs_learn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
327 lines
8.6 KiB
327 lines
8.6 KiB
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
ytDownloader "github.com/kkdai/youtube/v2"
|
|
"github.com/wailsapp/wails/v2"
|
|
|
|
. "ytd/constants"
|
|
. "ytd/db"
|
|
. "ytd/models"
|
|
)
|
|
|
|
type Yt struct {
|
|
Name string
|
|
WailsRuntime *wails.Runtime
|
|
AppConfig *AppConfig
|
|
AppStats *AppStats
|
|
NewEntries chan GenericEntry
|
|
dir string
|
|
client ytDownloader.Client
|
|
ctx context.Context
|
|
}
|
|
|
|
type YtEntry struct {
|
|
Type string
|
|
Track GenericTrack
|
|
Playlist GenericPlaylist
|
|
}
|
|
|
|
func (yt *Yt) SetWailsRuntime(runtime *wails.Runtime) {
|
|
yt.WailsRuntime = runtime
|
|
}
|
|
|
|
func (yt *Yt) SetContext(ctx context.Context) {
|
|
yt.ctx = ctx
|
|
}
|
|
|
|
func (yt *Yt) SetAppConfig(config *AppConfig) {
|
|
yt.AppConfig = config
|
|
}
|
|
|
|
func (yt *Yt) SetAppStats(stats *AppStats) {
|
|
yt.AppStats = stats
|
|
}
|
|
|
|
func (yt *Yt) GetName() string {
|
|
return "youtube"
|
|
}
|
|
|
|
func (yt *Yt) Initialize() error {
|
|
fmt.Println("Initializing yt client...")
|
|
yt.client = ytDownloader.Client{Debug: true}
|
|
return nil
|
|
}
|
|
|
|
func (yt *Yt) SetDir(dir string) {
|
|
yt.dir = dir
|
|
if _, err := os.Stat(yt.dir); os.IsNotExist(err) {
|
|
fmt.Printf("Creating %s directory for youtube plugin", yt.dir)
|
|
os.MkdirAll(yt.dir, os.ModePerm)
|
|
}
|
|
}
|
|
|
|
func (yt *Yt) GetDir() string {
|
|
return yt.dir
|
|
}
|
|
|
|
func (yt *Yt) IsTrackFileExists(track GenericTrack, fileType string) bool {
|
|
if _, err := os.Stat(fmt.Sprintf("%s/%s.%s", yt.dir, track.ID, fileType)); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (yt *Yt) Fetch(url string, isFromClipboard bool) *GenericEntry {
|
|
fmt.Printf("Fetching from yt %s...", url)
|
|
|
|
if isPlaylist := strings.Contains(url, "playlist?"); isPlaylist {
|
|
fmt.Println("Fetching playlist info...")
|
|
ytEntry := &GenericEntry{Source: yt.Name, Type: "playlist"}
|
|
playlist, err := yt.client.GetPlaylist(url)
|
|
if err != nil {
|
|
fmt.Printf("yt.client.GetPlaylist(url) error: %s", err)
|
|
return nil
|
|
}
|
|
|
|
var playlistTracks []string
|
|
playlistEntry := NewGenericPlaylist(playlist.ID, playlist.Title, len(playlist.Videos), playlistTracks)
|
|
ytEntry.Playlist = playlistEntry
|
|
ytEntry.Playlist.Url = url
|
|
for _, v := range playlist.Videos {
|
|
ytEntry.Playlist.TracksIds = append(ytEntry.Playlist.TracksIds, v.ID)
|
|
}
|
|
err = DbWriteEntry(ytEntry.Playlist.ID, ytEntry)
|
|
if err != nil {
|
|
fmt.Printf("Error while saving playlist: %s", err)
|
|
}
|
|
yt.WailsRuntime.Events.Emit("ytd:playlist", ytEntry) // notify frontend
|
|
for k, v := range playlist.Videos {
|
|
fmt.Printf("(%d) %s - '%s'\n", k+1, v.Author, v.Title)
|
|
video, err := yt.client.VideoFromPlaylistEntry(v)
|
|
if err != nil {
|
|
fmt.Printf("yt.client.VideoFromPlaylistEntry(v) error: %s", err)
|
|
continue
|
|
}
|
|
|
|
track := NewGenericTrack(video.ID, video.Title, video.Author, video.ID, url)
|
|
for _, thumbnail := range video.Thumbnails {
|
|
track.Thumbnails = append(track.Thumbnails, thumbnail.URL)
|
|
}
|
|
ytEntry := &GenericEntry{Source: yt.Name, Type: "track", Track: track}
|
|
ytEntry.Track.Status = TrackStatusProcessing
|
|
ytEntry.Track.Duration = video.Duration.Minutes()
|
|
ytEntry.Track.PlaylistID = playlist.ID
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry) // write to rigth bucket
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry) // notify frontend
|
|
|
|
err = yt.downloadTrack(video, ytEntry)
|
|
if err != nil {
|
|
fmt.Printf("yt.downloadTrack(video, video.ID) error: %s \n", err)
|
|
ytEntry.Track.Status = TrackStatusFailed
|
|
ytEntry.Track.StatusError = err.Error()
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
continue
|
|
}
|
|
ytEntry.Track.Status = TrackStatusDownladed
|
|
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
}
|
|
return ytEntry
|
|
}
|
|
|
|
return yt.fetchTrack(url, isFromClipboard)
|
|
}
|
|
|
|
func (yt *Yt) GetFilename() error {
|
|
return nil
|
|
}
|
|
|
|
func (yt *Yt) Supports(address string) bool {
|
|
u, err := url.Parse(address)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.Contains(u.Hostname(), "youtube")
|
|
}
|
|
|
|
func (yt *Yt) fetchTrack(url string, isFromClipboard bool) *GenericEntry {
|
|
video, err := yt.client.GetVideoContext(yt.ctx, url)
|
|
if err != nil {
|
|
fmt.Printf("yt.client.GetVideo(url) error: %s \n", err)
|
|
ytEntry := GenericEntry{Source: yt.Name, Type: "track", Track: NewFailedTrack(url, err)}
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
return nil
|
|
}
|
|
|
|
ytEntry := yt.addTrack(url, video)
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry) // write to rigth bucket
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry) // notify frontend
|
|
|
|
if yt.AppConfig.ClipboardWatch && !yt.AppConfig.DownloadOnCopy && isFromClipboard {
|
|
return nil
|
|
}
|
|
|
|
if yt.AppStats.DownloadingCount >= yt.AppConfig.MaxParrallelDownloads {
|
|
fmt.Println("MaxParrallelDownloads has been reached")
|
|
return nil
|
|
}
|
|
|
|
yt.AppStats.IncDndCount()
|
|
err = yt.downloadTrack(video, ytEntry)
|
|
yt.AppStats.DecDndCount()
|
|
if err != nil {
|
|
ytEntry.Track.Status = TrackStatusFailed
|
|
ytEntry.Track.StatusError = err.Error()
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
return nil
|
|
}
|
|
ytEntry.Track.Status = TrackStatusDownladed
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
return ytEntry
|
|
}
|
|
|
|
func (yt *Yt) StartDownload(ytEntry *GenericEntry) GenericEntry {
|
|
// update AppState.Entries
|
|
video, err := yt.client.GetVideo(ytEntry.Track.Url)
|
|
if err != nil {
|
|
fmt.Printf("StartDownload error: %s \n", err)
|
|
ytEntry.Track.Status = TrackStatusFailed
|
|
ytEntry.Track.StatusError = err.Error()
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
return *ytEntry
|
|
}
|
|
|
|
yt.AppStats.IncDndCount()
|
|
err = yt.downloadTrack(video, ytEntry)
|
|
yt.AppStats.DecDndCount()
|
|
if err != nil {
|
|
ytEntry.Track.Status = TrackStatusFailed
|
|
ytEntry.Track.StatusError = err.Error()
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
return *ytEntry
|
|
}
|
|
ytEntry.Track.Status = TrackStatusDownladed
|
|
DbWriteEntry(ytEntry.Track.ID, ytEntry)
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
return *ytEntry
|
|
}
|
|
|
|
func (yt *Yt) downloadTrack(video *ytDownloader.Video, ytEntry *GenericEntry) error {
|
|
audioFormats := video.Formats.Type("audio/webm")
|
|
|
|
if len(audioFormats) == 0 {
|
|
return TrackWithoutAudioFormat
|
|
}
|
|
|
|
stream, size, err := yt.client.GetStream(video, &audioFormats[0])
|
|
ytEntry.Track.FileSize = size
|
|
ytEntry.Track.Status = TrackStatusDownloading
|
|
yt.WailsRuntime.Events.Emit("ytd:track", ytEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return yt.saveTrack(stream, video.ID, size)
|
|
}
|
|
|
|
func (yt *Yt) saveTrack(stream io.ReadCloser, filename string, filesize int64) error {
|
|
file, err := os.Create(fmt.Sprintf("%s/%s.webm", yt.dir, filename))
|
|
if err != nil {
|
|
fmt.Printf("Cannot create file for track %s | %s\n", filename, err)
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
done := make(chan int64)
|
|
progress := make(chan uint)
|
|
go downloadProgress(done, progress, fmt.Sprintf("%s/%s.webm", yt.dir, filename), filesize)
|
|
go func() { // read from progress channel as soon as downloadProgress writes to progress chan
|
|
// loop until channel progress is closed by downloadProgress goroutine
|
|
for p := range progress {
|
|
fmt.Printf("Received progress: %d for video with ID %s\n", p, filename)
|
|
yt.WailsRuntime.Events.Emit(
|
|
"ytd:track:progress",
|
|
struct {
|
|
Id string `json:"id"`
|
|
Progress uint `json:"progress"`
|
|
}{Id: filename, Progress: p},
|
|
)
|
|
}
|
|
}()
|
|
|
|
w, err := io.Copy(file, stream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
done <- w
|
|
|
|
return nil
|
|
}
|
|
|
|
func (yt *Yt) addTrack(url string, video *ytDownloader.Video) *GenericEntry {
|
|
if video == nil {
|
|
track := GenericTrack{Url: url, Status: TrackStatusPending}
|
|
return &GenericEntry{Source: yt.Name, Type: "track", Track: track}
|
|
}
|
|
track := NewGenericTrack(video.ID, video.Title, video.Author, video.ID, url)
|
|
for _, thumbnail := range video.Thumbnails {
|
|
track.Thumbnails = append(track.Thumbnails, thumbnail.URL)
|
|
}
|
|
ytEntry := &GenericEntry{Source: yt.Name, Type: "track", Track: track}
|
|
ytEntry.Track.Status = TrackStatusProcessing
|
|
ytEntry.Track.Duration = video.Duration.Minutes()
|
|
return ytEntry
|
|
}
|
|
|
|
func downloadProgress(done chan int64, progress chan<- uint, path string, total int64) {
|
|
|
|
var stop bool = false
|
|
|
|
for {
|
|
select {
|
|
case <-done:
|
|
stop = true
|
|
default:
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
fi, err := file.Stat()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
size := fi.Size()
|
|
|
|
if size == 0 {
|
|
size = 1
|
|
}
|
|
|
|
var percent float64 = float64(size) / float64(total) * 100
|
|
progress <- uint(percent)
|
|
}
|
|
|
|
if stop {
|
|
close(progress)
|
|
break
|
|
}
|
|
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
|