2021-09-13 18:58:28 +00:00
package youtube
import (
"bytes"
2021-09-14 20:26:46 +00:00
"context"
2021-09-13 18:58:28 +00:00
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
2021-09-14 20:26:46 +00:00
"sync"
"time"
2021-09-13 18:58:28 +00:00
"git.netflux.io/rob/clipper/media"
youtubev2 "github.com/kkdai/youtube/v2"
)
2021-10-08 14:38:35 +00:00
const SizeOfInt16 = 2
2021-09-13 18:58:28 +00:00
2021-10-08 14:38:35 +00:00
const (
2021-09-24 05:37:49 +00:00
rawAudioCodec = "pcm_s16le"
rawAudioFormat = "s16le"
rawAudioSampleRate = 48000
2021-10-08 14:38:35 +00:00
)
2021-09-24 05:37:49 +00:00
2021-10-08 14:38:35 +00:00
const (
2021-09-24 05:37:49 +00:00
thumbnailPrescaleWidth = - 1
thumbnailPrescaleHeight = 120
2021-10-08 14:38:35 +00:00
thumbnailWidth = 177 // 16:9
thumbnailHeight = 100 // "
2021-09-13 18:58:28 +00:00
)
// YoutubeClient wraps the youtube.Client client.
type YoutubeClient interface {
2021-09-14 20:26:46 +00:00
GetVideoContext ( context . Context , string ) ( * youtubev2 . Video , error )
GetStreamContext ( context . Context , * youtubev2 . Video , * youtubev2 . Format ) ( io . ReadCloser , int64 , error )
2021-09-13 18:58:28 +00:00
}
// Downloader downloads a set of Youtube media for a given video ID, including
// separate audio and video files and a JSON metadata file. Additionally, it
// also renders the downloaded audio file as a raw audio file.
type Downloader struct {
youtubeClient YoutubeClient
}
2021-09-24 05:15:40 +00:00
// NewDownloader returns a new *Downloader.
2021-09-13 18:58:28 +00:00
func NewDownloader ( youtubeClient YoutubeClient ) * Downloader {
return & Downloader { youtubeClient : youtubeClient }
}
2021-09-14 20:26:46 +00:00
type audioResult struct {
* media . Audio
err error
}
type videoResult struct {
* media . Video
err error
}
2021-09-13 18:58:28 +00:00
// Download downloads the relevant audio and video files for the provided
// Youtube video ID. If successful, a *media.MediaSet struct containing
// metadata about the downloaded items is returned.
2021-09-14 20:26:46 +00:00
func ( d * Downloader ) Download ( ctx context . Context , videoID string ) ( * media . MediaSet , error ) {
var video * youtubev2 . Video
video , err := d . youtubeClient . GetVideoContext ( ctx , videoID )
2021-09-13 18:58:28 +00:00
if err != nil {
return nil , fmt . Errorf ( "error fetching video: %v" , err )
}
2021-09-25 17:00:19 +00:00
mediaSet := media . NewMediaSet ( videoID )
2021-09-14 20:26:46 +00:00
audioResultChan := make ( chan audioResult , 1 )
videoResultChan := make ( chan videoResult , 1 )
var wg sync . WaitGroup
wg . Add ( 2 )
go func ( ) {
defer close ( audioResultChan )
audio , audioErr := d . downloadAudio ( ctx , video , mediaSet . EncodedAudioPath ( ) , mediaSet . RawAudioPath ( ) )
result := audioResult { audio , audioErr }
audioResultChan <- result
wg . Done ( )
} ( )
go func ( ) {
defer close ( videoResultChan )
2021-10-19 15:37:54 +00:00
video , videoErr := d . downloadVideo ( ctx , video , mediaSet . VideoPath ( ) )
2021-09-14 20:26:46 +00:00
result := videoResult { video , videoErr }
videoResultChan <- result
wg . Done ( )
} ( )
wg . Wait ( )
audioResult := <- audioResultChan
videoResult := <- videoResultChan
if err = audioResult . err ; err != nil {
return nil , fmt . Errorf ( "error downloading audio: %v" , err )
}
if err = videoResult . err ; err != nil {
return nil , fmt . Errorf ( "error downloading video: %v" , err )
}
mediaSet . Audio = * audioResult . Audio
mediaSet . Video = * videoResult . Video
metadataFile , err := os . Create ( mediaSet . MetadataPath ( ) )
if err != nil {
return nil , fmt . Errorf ( "error opening metadata file: %v" , err )
}
if err = json . NewEncoder ( metadataFile ) . Encode ( mediaSet ) ; err != nil {
return nil , fmt . Errorf ( "error encoding metadata: %v" , err )
}
if err = metadataFile . Close ( ) ; err != nil {
return nil , fmt . Errorf ( "error writing metadata file: %v" , err )
}
log . Println ( "finished downloading mediaset" )
2021-09-25 17:00:19 +00:00
return mediaSet , nil
2021-09-14 20:26:46 +00:00
}
func ( d * Downloader ) downloadAudio ( ctx context . Context , video * youtubev2 . Video , outPath , rawOutPath string ) ( * media . Audio , error ) {
2021-09-24 05:15:40 +00:00
if len ( video . Formats ) == 0 {
2021-09-14 20:26:46 +00:00
return nil , errors . New ( "error selecting audio format: no format available" )
2021-09-13 18:58:28 +00:00
}
2021-09-24 05:15:40 +00:00
format := SortAudio ( video . Formats ) [ 0 ]
log . Printf ( "selected audio format: %s" , FormatDebugString ( & format , false ) )
2021-09-13 18:58:28 +00:00
2021-09-24 05:15:40 +00:00
stream , _ , err := d . youtubeClient . GetStreamContext ( ctx , video , & format )
2021-09-13 18:58:28 +00:00
if err != nil {
2021-09-14 20:26:46 +00:00
return nil , fmt . Errorf ( "error fetching audio stream: %v" , err )
2021-09-13 18:58:28 +00:00
}
2021-10-19 15:37:54 +00:00
reader := progressReader { Reader : stream , label : "audio" , exp : int ( format . ContentLength ) }
2021-09-13 18:58:28 +00:00
2021-09-14 20:26:46 +00:00
rawAudioFile , err := os . Create ( rawOutPath )
2021-09-13 18:58:28 +00:00
if err != nil {
return nil , fmt . Errorf ( "error creating raw audio file: %v" , err )
}
2021-09-14 20:26:46 +00:00
encodedAudioFile , err := os . Create ( outPath )
2021-09-13 18:58:28 +00:00
if err != nil {
return nil , fmt . Errorf ( "error creating encoded audio file: %v" , err )
}
2021-10-19 15:37:54 +00:00
streamReader := io . TeeReader ( & reader , encodedAudioFile )
2021-09-13 18:58:28 +00:00
var errOut bytes . Buffer
2021-09-24 05:37:49 +00:00
cmd := exec . CommandContext ( ctx , "ffmpeg" , "-i" , "-" , "-f" , rawAudioFormat , "-ar" , strconv . Itoa ( rawAudioSampleRate ) , "-acodec" , rawAudioCodec , "-" )
2021-09-13 18:58:28 +00:00
cmd . Stdin = streamReader
cmd . Stdout = rawAudioFile
cmd . Stderr = & errOut
if err = cmd . Run ( ) ; err != nil {
log . Println ( errOut . String ( ) )
return nil , fmt . Errorf ( "error processing audio: %v" , err )
}
if err = rawAudioFile . Close ( ) ; err != nil {
return nil , fmt . Errorf ( "error writing raw audio file: %v" , err )
}
2021-09-14 20:26:46 +00:00
rawAudioFile , err = os . Open ( rawOutPath )
2021-09-13 18:58:28 +00:00
if err != nil {
2021-09-14 20:26:46 +00:00
return nil , fmt . Errorf ( "error opening raw audio file: %v" , err )
2021-09-13 18:58:28 +00:00
}
fi , err := rawAudioFile . Stat ( )
if err != nil {
2021-09-14 20:26:46 +00:00
return nil , fmt . Errorf ( "error reading raw audio file info: %v" , err )
2021-09-13 18:58:28 +00:00
}
numFrames := fi . Size ( ) / int64 ( SizeOfInt16 ) / int64 ( format . AudioChannels )
sampleRate , err := strconv . Atoi ( format . AudioSampleRate )
if err != nil {
return nil , fmt . Errorf ( "invalid samplerate: %s" , format . AudioSampleRate )
}
2021-09-14 20:26:46 +00:00
return & media . Audio {
Bytes : fi . Size ( ) ,
Channels : format . AudioChannels ,
Frames : numFrames ,
SampleRate : sampleRate ,
} , nil
}
2021-09-13 18:58:28 +00:00
2021-10-19 03:15:38 +00:00
type progressReader struct {
io . Reader
2021-10-19 15:37:54 +00:00
label string
2021-10-19 03:15:38 +00:00
total , exp int
}
func ( pw * progressReader ) Read ( p [ ] byte ) ( int , error ) {
n , err := pw . Reader . Read ( p )
pw . total += n
2021-10-19 15:37:54 +00:00
log . Printf ( "[ProgressReader] [%s] Read %d of %d (%.02f%%) bytes from the provided reader" , pw . label , pw . total , pw . exp , ( float32 ( pw . total ) / float32 ( pw . exp ) ) * 100.0 )
2021-10-19 03:15:38 +00:00
return n , err
2021-09-14 20:26:46 +00:00
}
2021-10-19 15:37:54 +00:00
func ( d * Downloader ) downloadVideo ( ctx context . Context , video * youtubev2 . Video , outPath string ) ( * media . Video , error ) {
2021-09-24 05:15:40 +00:00
if len ( video . Formats ) == 0 {
return nil , errors . New ( "error selecting audio format: no format available" )
}
format := SortVideo ( video . Formats ) [ 0 ]
log . Printf ( "selected video format: %s" , FormatDebugString ( & format , false ) )
2021-09-14 20:26:46 +00:00
2021-10-08 14:38:35 +00:00
durationMsecs , err := strconv . Atoi ( format . ApproxDurationMs )
if err != nil {
return nil , fmt . Errorf ( "could not parse video duration: %s" , err )
}
2021-09-24 05:15:40 +00:00
stream , _ , err := d . youtubeClient . GetStreamContext ( ctx , video , & format )
2021-09-13 18:58:28 +00:00
if err != nil {
2021-09-14 20:26:46 +00:00
return nil , fmt . Errorf ( "error fetching video stream: %v" , err )
2021-09-13 18:58:28 +00:00
}
2021-10-19 15:37:54 +00:00
reader := progressReader { Reader : stream , label : "video" , exp : int ( format . ContentLength ) }
2021-09-13 18:58:28 +00:00
2021-10-08 14:38:35 +00:00
videoFile , err := os . Create ( outPath )
2021-09-14 20:26:46 +00:00
if err != nil {
2021-10-08 14:38:35 +00:00
return nil , fmt . Errorf ( "error creating video file: %v" , err )
2021-09-13 18:58:28 +00:00
}
2021-09-14 20:26:46 +00:00
2021-10-19 03:15:38 +00:00
if _ , err = io . Copy ( videoFile , & reader ) ; err != nil {
2021-09-14 20:26:46 +00:00
return nil , fmt . Errorf ( "error processing video: %v" , err )
}
return & media . Video {
Bytes : format . ContentLength ,
ThumbnailWidth : thumbnailWidth ,
ThumbnailHeight : thumbnailHeight ,
2021-09-24 05:37:49 +00:00
Duration : time . Duration ( durationMsecs ) * time . Millisecond ,
2021-09-14 20:26:46 +00:00
} , nil
2021-09-13 18:58:28 +00:00
}