Refactored downloader

This commit is contained in:
2021-12-22 01:22:33 +01:00
parent 4b9583b08f
commit 23ca1168df
21 changed files with 544 additions and 177 deletions

View File

@@ -1,5 +0,0 @@
package downloader
type Downloader interface {
Download(link string, updateEvent func(progress string)) error
}

View File

@@ -1,130 +0,0 @@
package yt_downloader
import (
"crypto/sha256"
"downloader/internal/core/ports/downloader"
"downloader/pkg/files"
"fmt"
"go.uber.org/zap"
"io"
"io/fs"
"log"
"os"
"os/exec"
"regexp"
"strings"
"time"
)
type YtDownloader struct {
outputDirectory string
tempDirectory string
checkFrequencyMs time.Duration
logger *zap.SugaredLogger
}
func New(logger *zap.SugaredLogger) downloader.Downloader {
return &YtDownloader{
outputDirectory: "/home/hermansen/Downloads/yt",
tempDirectory: "/tmp/downloader",
checkFrequencyMs: 5000,
logger: logger,
}
}
func init() {
_, err := exec.Command("youtube-dl", "--version").Output()
if err != nil {
log.Fatal("Youtube download (youtube-dl) isn't installed on the device")
}
}
func (y *YtDownloader) Download(link string, updateEvent func(progress string)) error {
baseDir := fmt.Sprintf("%s/%x",
y.tempDirectory,
sha256.Sum256([]byte(link)))
err := os.MkdirAll(baseDir, os.ModePerm)
err = os.MkdirAll(y.outputDirectory, os.ModePerm)
if err != nil {
y.logger.Error(err)
return err
}
filePath := fmt.Sprintf("%s/%s", baseDir, "%(title)s-%(id)s.%(ext)s")
command := exec.Command("youtube-dl",
"-R 3",
"-o",
filePath,
link,
)
var stdout io.ReadCloser
stdout, err = command.StdoutPipe()
go func() {
for true {
bytes := make([]byte, 1024)
_, err = stdout.Read(bytes)
if err != nil {
return
}
output := string(bytes)
compile := regexp.MustCompile(`[a-z\[\]\s]+([\d\.]+).[a-z\s]+([\d\.]+)([a-zA-Z]+)[a-z\s]+`)
if err != nil {
y.logger.Error(err)
return
}
res := compile.FindAllStringSubmatch(output, -1)
if len(res) != 0 && strings.Contains(res[0][0], "download") && len(res[0]) >= 2 {
progress := res[0][1]
y.logger.Debugw("progress",
"percentage", progress,
"link", link)
updateEvent(progress)
}
time.Sleep(time.Millisecond * y.checkFrequencyMs)
}
}()
err = command.Start()
if err != nil {
y.logger.Warn(err)
return err
}
err = command.Wait()
if err != nil {
return err
}
var dir []fs.DirEntry
dir, err = os.ReadDir(baseDir)
if err != nil {
y.logger.Error("Could not read directory")
return err
}
for _, fileInfo := range dir {
oldPath := fmt.Sprintf("%s/%s", baseDir, fileInfo.Name())
newPath := fmt.Sprintf("%s/%s", y.outputDirectory, fileInfo.Name())
if err := files.MoveFile(oldPath, newPath); err != nil {
return err
} else {
y.logger.Infow("moved file",
"fileName", fileInfo.Name())
if err := os.Remove(baseDir); err != nil {
y.logger.Warn("could not cleanup",
"path", oldPath)
}
}
}
return nil
}

View File

@@ -0,0 +1,10 @@
package downloadhandler
type OnDownloadEventHandler interface {
OnTickEvent(downloadId string, progress string)
}
type DownloadHandler interface {
OnProgress(eventHandler OnDownloadEventHandler)
Download(link string, outputDir string, downloadId string) error
}

View File

@@ -0,0 +1,98 @@
package downloadhandler
import (
"fmt"
"github.com/ovh/configstore"
"go.uber.org/zap"
"log"
"os/exec"
"regexp"
"strings"
"time"
)
type YoutubeDlDownloader struct {
progressUpdate bool
checkFrequencyMs time.Duration
logger *zap.SugaredLogger
onDownloadEventHandler OnDownloadEventHandler
}
func NewYoutubeDlDownloader(logger *zap.SugaredLogger) DownloadHandler {
var checkFrequencyMs int64
var featureProgressUpdate bool
var err error
checkFrequencyMs, err = configstore.GetItemValueInt("download_frequency_update_ms")
featureProgressUpdate, err = configstore.GetItemValueBool("download_progress_update")
if err != nil {
panic(err)
}
_, err = exec.Command("youtube-dl", "--version").Output()
if err != nil {
log.Fatal("Youtube download (youtube-dl) isn't installed on the device")
}
return &YoutubeDlDownloader{
checkFrequencyMs: time.Duration(checkFrequencyMs),
progressUpdate: featureProgressUpdate,
logger: logger,
onDownloadEventHandler: nil,
}
}
func (y *YoutubeDlDownloader) OnProgress(eventHandler OnDownloadEventHandler) {
y.onDownloadEventHandler = eventHandler
}
func (y *YoutubeDlDownloader) Download(link string, outputDir string, downloadId string) error {
filePath := fmt.Sprintf("%s/%s", outputDir, "%(title)s-%(id)s.%(ext)s")
command := exec.Command("youtube-dl",
"-R 3",
"-o",
filePath,
link,
)
stdout, err := command.StdoutPipe()
if y.progressUpdate {
go func() {
for true {
bytes := make([]byte, 1024)
_, err = stdout.Read(bytes)
if err != nil {
return
}
output := string(bytes)
if err = y.getParameters(output, link, downloadId); err != nil {
return
}
time.Sleep(time.Millisecond * y.checkFrequencyMs)
}
}()
}
return command.Run()
}
func (y *YoutubeDlDownloader) getParameters(output string, link string, downloadId string) error {
compile := regexp.MustCompile(`[a-z\[\]\s]+([\d\.]+).[a-z\s]+([\d\.]+)([a-zA-Z]+)[a-z\s]+`)
res := compile.FindAllStringSubmatch(output, -1)
if len(res) != 0 && strings.Contains(res[0][0], "download") && len(res[0]) >= 2 {
progress := res[0][1]
y.logger.Debugw("progress",
"percentage", progress,
"link", link)
if y.onDownloadEventHandler != nil {
y.onDownloadEventHandler.OnTickEvent(downloadId, progress)
}
}
return nil
}

View File

@@ -0,0 +1,98 @@
package downloadhandler
import (
"fmt"
"github.com/ovh/configstore"
"go.uber.org/zap"
"log"
"os/exec"
"regexp"
"strings"
"time"
)
type YtDlpDownloader struct {
progressUpdate bool
checkFrequencyMs time.Duration
logger *zap.SugaredLogger
onDownloadEventHandler OnDownloadEventHandler
}
func NewYtDlpDownloader(logger *zap.SugaredLogger) DownloadHandler {
var checkFrequencyMs int64
var featureProgressUpdate bool
var err error
checkFrequencyMs, err = configstore.GetItemValueInt("download_frequency_update_ms")
featureProgressUpdate, err = configstore.GetItemValueBool("download_progress_update")
if err != nil {
panic(err)
}
_, err = exec.Command("yt-dlp", "--version").Output()
if err != nil {
log.Fatal("Youtube download (youtube-dl) isn't installed on the device")
}
return &YtDlpDownloader{
checkFrequencyMs: time.Duration(checkFrequencyMs),
progressUpdate: featureProgressUpdate,
logger: logger,
onDownloadEventHandler: nil,
}
}
func (y *YtDlpDownloader) OnProgress(eventHandler OnDownloadEventHandler) {
y.onDownloadEventHandler = eventHandler
}
func (y *YtDlpDownloader) Download(link string, outputDir string, downloadId string) error {
filePath := fmt.Sprintf("%s/%s", outputDir, "%(title)s-%(id)s.%(ext)s")
command := exec.Command("yt-dlp",
"-R 3",
"-o",
filePath,
link,
)
stdout, err := command.StdoutPipe()
if y.progressUpdate {
go func() {
for true {
bytes := make([]byte, 1024)
_, err = stdout.Read(bytes)
if err != nil {
return
}
output := string(bytes)
if err = y.getParameters(output, link, downloadId); err != nil {
return
}
time.Sleep(time.Millisecond * y.checkFrequencyMs)
}
}()
}
return command.Run()
}
func (y *YtDlpDownloader) getParameters(output string, link string, downloadId string) error {
compile := regexp.MustCompile(`[a-z\[\]\s]+([\d\.]+).[a-z\s\~]+([\d\.]+)([a-zA-Z]+)[a-z\s]+`)
res := compile.FindAllStringSubmatch(output, -1)
if len(res) != 0 && strings.Contains(res[0][0], "download") && len(res[0]) >= 2 {
progress := res[0][1]
y.logger.Debugw("progress",
"percentage", progress,
"link", link)
if y.onDownloadEventHandler != nil {
y.onDownloadEventHandler.OnTickEvent(downloadId, progress)
}
}
return nil
}

View File

@@ -0,0 +1,39 @@
package local
import (
"downloader/internal/core/ports/filehandler/mover"
"downloader/pkg/files"
"fmt"
"go.uber.org/zap"
"os"
)
type Mover struct {
logger *zap.SugaredLogger
}
func New(logger *zap.SugaredLogger) mover.Mover {
return &Mover{logger: logger}
}
func (m *Mover) Move(sourceDirectory string, destinationDirectory string) error {
dir, err := os.ReadDir(sourceDirectory)
if err != nil {
m.logger.Error("Could not read directory")
return err
}
for _, fileInfo := range dir {
oldPath := fmt.Sprintf("%s/%s", sourceDirectory, fileInfo.Name())
newPath := fmt.Sprintf("%s/%s", destinationDirectory, fileInfo.Name())
if err := files.MoveFile(oldPath, newPath); err != nil {
return err
} else {
m.logger.Infow("moved file",
"fileName", fileInfo.Name())
}
}
return nil
}

View File

@@ -0,0 +1,5 @@
package mover
type Mover interface {
Move(sourceDirectory string, destinationDirectory string) error
}

View File

@@ -0,0 +1,36 @@
package destinationhandler
import (
"downloader/internal/core/ports/filehandler/mover"
"github.com/ovh/configstore"
"os"
)
type DestinationHandler interface {
Prepare() error
Move(sourcePath string) error
}
type localDestinationHandler struct {
outputDirectory string
mover mover.Mover
}
func New(mover mover.Mover) DestinationHandler {
outputDirectory, err := configstore.GetItemValue("download_output")
if err != nil {
panic(err)
}
return &localDestinationHandler{
outputDirectory: outputDirectory,
mover: mover,
}
}
func (l localDestinationHandler) Prepare() error {
return os.MkdirAll(l.outputDirectory, os.ModePerm)
}
func (l localDestinationHandler) Move(sourcePath string) error {
return l.mover.Move(sourcePath, l.outputDirectory)
}

View File

@@ -0,0 +1,40 @@
package fileorchestrator
import (
"downloader/internal/core/ports/fileorchestrator/destinationhandler"
"downloader/internal/core/ports/fileorchestrator/sourcehandler"
"go.uber.org/zap"
)
type FileOrchestrator struct {
logger *zap.SugaredLogger
sourceHandler sourcehandler.SourceHandler
destinationHandler destinationhandler.DestinationHandler
}
func New(logger *zap.SugaredLogger, sourceHandler sourcehandler.SourceHandler, destinationHandler destinationhandler.DestinationHandler) *FileOrchestrator {
return &FileOrchestrator{
logger: logger,
sourceHandler: sourceHandler,
destinationHandler: destinationHandler,
}
}
func (o *FileOrchestrator) Begin(link string) (string, error) {
basePath, err := o.sourceHandler.Prepare(link)
err = o.destinationHandler.Prepare()
return basePath, err
}
func (o *FileOrchestrator) CleanUp(sourceDirectory string) {
err := o.sourceHandler.CleanUp(sourceDirectory)
if err != nil {
o.logger.Errorw("could not cleanup",
"path", sourceDirectory)
}
}
func (o *FileOrchestrator) Move(sourcePath string) error {
return o.destinationHandler.Move(sourcePath)
}

View File

@@ -0,0 +1,39 @@
package sourcehandler
import (
"crypto/sha256"
"fmt"
"github.com/ovh/configstore"
"os"
)
type SourceHandler interface {
Prepare(link string) (string, error)
CleanUp(sourcePath string) error
}
type localSourceHandler struct {
outputTmpDirectory string
}
func New() SourceHandler {
outputTmpDirectory, err := configstore.GetItemValue("download_tmp_output")
if err != nil {
panic(err)
}
return &localSourceHandler{
outputTmpDirectory: outputTmpDirectory,
}
}
func (l localSourceHandler) Prepare(link string) (string, error) {
path := fmt.Sprintf("%s/%x",
l.outputTmpDirectory,
sha256.Sum256([]byte(link)))
err := os.MkdirAll(path, os.ModePerm)
return path, err
}
func (l localSourceHandler) CleanUp(sourcePath string) error {
return os.Remove(sourcePath)
}

View File

@@ -3,9 +3,8 @@ package _default
import (
"downloader/internal/core/entities"
"downloader/internal/core/ports/download_request"
"downloader/internal/core/ports/downloader"
"downloader/internal/core/services/download"
"fmt"
"downloader/internal/core/services/downloader"
"go.uber.org/zap"
)
@@ -26,10 +25,7 @@ func (l localBackgroundService) Run(download *entities.Download) error {
download.Status = "started"
_ = l.repository.Update(download)
err := l.downloader.Download(download.Link, func(progress string) {
download.Status = fmt.Sprintf("in-progress: %s", progress)
_ = l.repository.Update(download)
})
err := l.downloader.Download(download.Link, download.ID)
download.Status = "done"
if err != nil {

View File

@@ -0,0 +1,32 @@
package handlers
import (
"downloader/internal/core/ports/download_request"
"downloader/internal/core/ports/downloadhandler"
"fmt"
"go.uber.org/zap"
)
type onDownloadEventHandler struct {
repository download_request.Repository
logger *zap.SugaredLogger
}
func New(repository download_request.Repository, logger *zap.SugaredLogger) downloadhandler.OnDownloadEventHandler {
return &onDownloadEventHandler{
repository: repository,
logger: logger,
}
}
func (o *onDownloadEventHandler) OnTickEvent(downloadId string, progress string) {
download, err := o.repository.GetById(downloadId)
if err != nil {
o.logger.Warnw("could not finish updating progress as not download id available",
"downloadId", downloadId,
"progress", progress)
return
}
download.Status = fmt.Sprintf("in-progress: %s", progress)
_ = o.repository.Update(download)
}

View File

@@ -0,0 +1,54 @@
package downloader
import (
"downloader/internal/core/ports/downloadhandler"
"downloader/internal/core/ports/fileorchestrator"
"errors"
"go.uber.org/zap"
)
type Downloader interface {
Download(link string, downloadId string) error
}
type downloader struct {
logger *zap.SugaredLogger
downloadHandler downloadhandler.DownloadHandler
fileOrchestrator *fileorchestrator.FileOrchestrator
ondownloadhandler downloadhandler.OnDownloadEventHandler
}
func New(
logger *zap.SugaredLogger,
orchestrator *fileorchestrator.FileOrchestrator,
downloadHandler downloadhandler.DownloadHandler,
downloadEventHandler downloadhandler.OnDownloadEventHandler,
) Downloader {
return &downloader{
logger: logger,
downloadHandler: downloadHandler,
fileOrchestrator: orchestrator,
ondownloadhandler: downloadEventHandler,
}
}
func (y *downloader) Download(link string, downloadId string) error {
basePath, err := y.fileOrchestrator.Begin(link)
if err != nil {
return errors.New("could not prepare for this link")
}
defer y.fileOrchestrator.CleanUp(basePath)
y.downloadHandler.OnProgress(y.ondownloadhandler)
err = y.downloadHandler.Download(link, basePath, downloadId)
if err != nil {
return errors.New("could not download file")
}
err = y.fileOrchestrator.Move(basePath)
if err != nil {
return errors.New("could not move to final destination")
}
return nil
}