Remove unused youtube module

This commit is contained in:
Rob Watson 2021-11-12 13:41:59 +01:00
parent 5ab37a068a
commit cd2686a5b2
6 changed files with 26 additions and 473 deletions

View File

@ -13,7 +13,6 @@ import (
"time"
"git.netflux.io/rob/clipper/media"
"git.netflux.io/rob/clipper/youtube"
youtubev2 "github.com/kkdai/youtube/v2"
)
@ -57,7 +56,7 @@ func main() {
fmt.Println("In descending order of preference:")
for n, f := range formats {
fmt.Printf("%d: %s\n", n+1, youtube.FormatDebugString(&f, verbose))
fmt.Printf("%d: %s\n", n+1, formatDebugString(&f, verbose))
}
}
@ -95,3 +94,27 @@ func downloadAll(formats youtubev2.FormatList) {
wg.Wait()
}
func formatDebugString(format *youtubev2.Format, includeURL bool) string {
url := "hidden"
if includeURL {
url = format.URL
}
return fmt.Sprintf(
"iTag = %d, mime_type = %s, quality = %s, quality_label = %s, bitrate = %d, fps = %d, width = %d, height = %d, content_length = %d, duration = %v, audio_channels = %d, audio_sample_rate = %s, audio_quality = %s, url = %s",
format.ItagNo,
format.MimeType,
format.Quality,
format.QualityLabel,
format.Bitrate,
format.FPS,
format.Width,
format.Height,
format.ContentLength,
format.ApproxDurationMs,
format.AudioChannels,
format.AudioSampleRate,
format.AudioQuality,
url,
)
}

View File

@ -1,83 +0,0 @@
package server
// // getMediaSet is a handler that responds with a MediaSet.
// func getMediaSet(c echo.Context) error {
// videoID := c.Param("id")
// mediaSet := media.NewMediaSet(videoID)
// if mediaSet.Exists() {
// if err := mediaSet.Load(); err != nil {
// log.Printf("error loading MediaSet: %v", err)
// return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch media set")
// }
// return c.JSON(http.StatusOK, mediaSet)
// }
// var youtubeClient youtubev2.Client
// downloader := youtube.NewDownloader(&youtubeClient)
// mediaSet, err := downloader.Download(c.Request().Context(), videoID)
// if err != nil {
// log.Printf("error downloading MediaSet: %v", err)
// return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch media set")
// }
// return c.JSON(http.StatusOK, mediaSet)
// }
// // getThumbnails is a handler that responds with a MediaSet thumbnail grid.
// func getThumbnails(c echo.Context) error {
// videoID := c.Param("id")
// mediaSet := media.NewMediaSet(videoID)
// if err := mediaSet.Load(); err != nil {
// log.Printf("error loading MediaSet: %v", err)
// return echo.NewHTTPError(http.StatusInternalServerError, "could not load media set")
// }
// return c.File(mediaSet.ThumbnailPath())
// }
// // getVideo is a handler that responds with the video file for a MediaSet
// func getVideo(c echo.Context) error {
// videoID := c.Param("id")
// mediaSet := media.NewMediaSet(videoID)
// if err := mediaSet.Load(); err != nil {
// log.Printf("error loading MediaSet: %v", err)
// return echo.NewHTTPError(http.StatusInternalServerError, "could not load media set")
// }
// return c.File(mediaSet.VideoPath())
// }
// // getPeaks is a handler that returns a two-dimensional array of peaks, with
// // the number of bins matching the provided parameter.
// func getPeaks(c echo.Context) error {
// videoID := c.Param("id")
// start, err := strconv.ParseInt(c.QueryParam("start"), 0, 64)
// if err != nil {
// return echo.NewHTTPError(http.StatusBadRequest, "invalid start parameter provided")
// }
// end, err := strconv.ParseInt(c.QueryParam("end"), 0, 64)
// if err != nil {
// return echo.NewHTTPError(http.StatusBadRequest, "invalid end parameter provided")
// }
// numBins, err := strconv.Atoi(c.QueryParam("bins"))
// if err != nil {
// return echo.NewHTTPError(http.StatusBadRequest, "invalid bins parameter provided")
// }
// mediaSet := media.NewMediaSet(videoID)
// if err = mediaSet.Load(); err != nil {
// log.Printf("error loading MediaSet: %v", err)
// return echo.NewHTTPError(http.StatusInternalServerError, "could not load media set")
// }
// peaks, err := mediaSet.Peaks(start, end, numBins)
// if err != nil {
// log.Printf("error generating peaks: %v", err)
// return echo.NewHTTPError(http.StatusInternalServerError, "could not generate peaks")
// }
// return json.NewEncoder(c.Response()).Encode(peaks)
// }

View File

@ -11,7 +11,6 @@ import (
pbMediaSet "git.netflux.io/rob/clipper/generated/pb/media_set"
"git.netflux.io/rob/clipper/media"
"git.netflux.io/rob/clipper/youtube"
"github.com/google/uuid"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"google.golang.org/grpc"
@ -60,7 +59,7 @@ type Options struct {
BindAddr string
Timeout time.Duration
Store media.Store
YoutubeClient youtube.YoutubeClient
YoutubeClient media.YoutubeClient
S3Client media.S3Client
}

View File

@ -1,81 +0,0 @@
package youtube
import (
"fmt"
"sort"
"strings"
youtubev2 "github.com/kkdai/youtube/v2"
)
func FormatDebugString(format *youtubev2.Format, includeURL bool) string {
url := "hidden"
if includeURL {
url = format.URL
}
return fmt.Sprintf(
"iTag = %d, mime_type = %s, quality = %s, quality_label = %s, bitrate = %d, fps = %d, width = %d, height = %d, content_length = %d, duration = %v, audio_channels = %d, audio_sample_rate = %s, audio_quality = %s, url = %s",
format.ItagNo,
format.MimeType,
format.Quality,
format.QualityLabel,
format.Bitrate,
format.FPS,
format.Width,
format.Height,
format.ContentLength,
format.ApproxDurationMs,
format.AudioChannels,
format.AudioSampleRate,
format.AudioQuality,
url,
)
}
// SortAudio returns the provided formats ordered in descending preferred
// order. The ideal candidate is opus-encoded stereo audio in a webm container,
// with the lowest available bitrate.
func SortAudio(inFormats youtubev2.FormatList) youtubev2.FormatList {
var formats youtubev2.FormatList
for _, format := range inFormats {
if format.FPS == 0 && format.AudioChannels > 0 {
formats = append(formats, format)
}
}
sort.SliceStable(formats, func(i, j int) bool {
isOpusI := strings.Contains(formats[i].MimeType, "opus")
isOpusJ := strings.Contains(formats[j].MimeType, "opus")
if isOpusI && isOpusJ {
isStereoI := formats[i].AudioChannels == 2
isStereoJ := formats[j].AudioChannels == 2
if isStereoI && isStereoJ {
return formats[i].ContentLength < formats[j].ContentLength
}
return isStereoI
}
return isOpusI
})
return formats
}
// SortVideo returns the provided formats ordered in descending preferred
// order. The ideal candidate is video in an mp4 container with a low
// bitrate, with audio channels (needed to allow synced playback on the
// website).
func SortVideo(inFormats youtubev2.FormatList) youtubev2.FormatList {
var formats youtubev2.FormatList
for _, format := range inFormats {
if format.FPS > 0 && format.ContentLength > 0 {
formats = append(formats, format)
}
}
sort.SliceStable(formats, func(i, j int) bool {
isMP4I := strings.Contains(formats[i].MimeType, "mp4")
isMP4J := strings.Contains(formats[j].MimeType, "mp4")
if isMP4I && isMP4J {
return formats[i].ContentLength < formats[j].ContentLength
}
return isMP4I
})
return formats
}

View File

@ -1,235 +0,0 @@
package youtube
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"sync"
"time"
"git.netflux.io/rob/clipper/media"
youtubev2 "github.com/kkdai/youtube/v2"
)
const SizeOfInt16 = 2
const (
rawAudioCodec = "pcm_s16le"
rawAudioFormat = "s16le"
rawAudioSampleRate = 48000
)
const (
thumbnailPrescaleWidth = -1
thumbnailPrescaleHeight = 120
thumbnailWidth = 177 // 16:9
thumbnailHeight = 100 // "
)
// 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
}
// NewDownloader returns a new *Downloader.
func NewDownloader(youtubeClient YoutubeClient) *Downloader {
return &Downloader{youtubeClient: youtubeClient}
}
type audioResult struct {
*media.Audio
err error
}
type videoResult struct {
*media.Video
err error
}
// 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.
func (d *Downloader) Download(ctx context.Context, videoID string) (*media.MediaSet, error) {
var video *youtubev2.Video
video, err := d.youtubeClient.GetVideoContext(ctx, videoID)
if err != nil {
return nil, fmt.Errorf("error fetching video: %v", err)
}
mediaSet := media.NewMediaSet(videoID)
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)
video, videoErr := d.downloadVideo(ctx, video, mediaSet.VideoPath())
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")
return mediaSet, nil
}
func (d *Downloader) downloadAudio(ctx context.Context, video *youtubev2.Video, outPath, rawOutPath string) (*media.Audio, error) {
if len(video.Formats) == 0 {
return nil, errors.New("error selecting audio format: no format available")
}
format := SortAudio(video.Formats)[0]
log.Printf("selected audio format: %s", FormatDebugString(&format, false))
stream, _, err := d.youtubeClient.GetStreamContext(ctx, video, &format)
if err != nil {
return nil, fmt.Errorf("error fetching audio stream: %v", err)
}
reader := progressReader{Reader: stream, label: "audio", exp: int(format.ContentLength)}
rawAudioFile, err := os.Create(rawOutPath)
if err != nil {
return nil, fmt.Errorf("error creating raw audio file: %v", err)
}
encodedAudioFile, err := os.Create(outPath)
if err != nil {
return nil, fmt.Errorf("error creating encoded audio file: %v", err)
}
streamReader := io.TeeReader(&reader, encodedAudioFile)
var errOut bytes.Buffer
cmd := exec.CommandContext(ctx, "ffmpeg", "-i", "-", "-f", rawAudioFormat, "-ar", strconv.Itoa(rawAudioSampleRate), "-acodec", rawAudioCodec, "-")
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)
}
rawAudioFile, err = os.Open(rawOutPath)
if err != nil {
return nil, fmt.Errorf("error opening raw audio file: %v", err)
}
fi, err := rawAudioFile.Stat()
if err != nil {
return nil, fmt.Errorf("error reading raw audio file info: %v", err)
}
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)
}
return &media.Audio{
Bytes: fi.Size(),
Channels: format.AudioChannels,
Frames: numFrames,
SampleRate: sampleRate,
}, nil
}
type progressReader struct {
io.Reader
label string
total, exp int
}
func (pw *progressReader) Read(p []byte) (int, error) {
n, err := pw.Reader.Read(p)
pw.total += n
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)
return n, err
}
func (d *Downloader) downloadVideo(ctx context.Context, video *youtubev2.Video, outPath string) (*media.Video, error) {
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))
durationMsecs, err := strconv.Atoi(format.ApproxDurationMs)
if err != nil {
return nil, fmt.Errorf("could not parse video duration: %s", err)
}
stream, _, err := d.youtubeClient.GetStreamContext(ctx, video, &format)
if err != nil {
return nil, fmt.Errorf("error fetching video stream: %v", err)
}
reader := progressReader{Reader: stream, label: "video", exp: int(format.ContentLength)}
videoFile, err := os.Create(outPath)
if err != nil {
return nil, fmt.Errorf("error creating video file: %v", err)
}
if _, err = io.Copy(videoFile, &reader); err != nil {
return nil, fmt.Errorf("error processing video: %v", err)
}
return &media.Video{
Bytes: format.ContentLength,
ThumbnailWidth: thumbnailWidth,
ThumbnailHeight: thumbnailHeight,
Duration: time.Duration(durationMsecs) * time.Millisecond,
}, nil
}

View File

@ -1,70 +0,0 @@
package youtube
import (
"context"
"errors"
"fmt"
"io"
"log"
"strconv"
"time"
"git.netflux.io/rob/clipper/media"
youtubev2 "github.com/kkdai/youtube/v2"
)
// YoutubeClient wraps the youtube.Client client.
type YoutubeClient interface {
GetVideoContext(context.Context, string) (*youtubev2.Video, error)
GetStreamContext(context.Context, *youtubev2.Video, *youtubev2.Format) (io.ReadCloser, int64, error)
}
// MediaSetService implements a MediaSetService for Youtube videos.
type MediaSetService struct {
youtubeClient YoutubeClient
}
// not used
func (s *MediaSetService) GetMediaSet(ctx context.Context, id string) (*media.MediaSet, error) {
var video *youtubev2.Video
video, err := s.youtubeClient.GetVideoContext(ctx, id)
if err != nil {
return nil, fmt.Errorf("error fetching video: %v", err)
}
if len(video.Formats) == 0 {
return nil, errors.New("no format available")
}
audioFormat := SortAudio(video.Formats)[0]
videoFormat := SortVideo(video.Formats)[0]
durationMsecs, err := strconv.Atoi(videoFormat.ApproxDurationMs)
if err != nil {
log.Printf("GetMediaSet: invalid duration %s", videoFormat.ApproxDurationMs)
return nil, errors.New("error parsing format")
}
duration := time.Duration(durationMsecs) * time.Millisecond
sampleRate, err := strconv.Atoi(videoFormat.AudioSampleRate)
if err != nil {
log.Printf("GetMediaSet: invalid samplerate %s", videoFormat.AudioSampleRate)
return nil, errors.New("error parsing format")
}
return &media.MediaSet{
YoutubeID: "",
Audio: media.Audio{
Bytes: audioFormat.ContentLength,
Channels: audioFormat.AudioChannels,
Frames: 0,
SampleRate: sampleRate,
},
Video: media.Video{
Bytes: videoFormat.ContentLength,
Duration: duration,
ThumbnailWidth: videoFormat.Width,
ThumbnailHeight: videoFormat.Height,
},
}, nil
}