diff --git a/backend/media/media_set.go b/backend/media/media_set.go deleted file mode 100644 index 8937c6a..0000000 --- a/backend/media/media_set.go +++ /dev/null @@ -1,55 +0,0 @@ -package media - -import ( - "fmt" - "time" - - "github.com/google/uuid" -) - -const SizeOfInt16 = 2 - -type Audio struct { - ContentLength int64 - Channels int - // ApproxFrames is used during initial processing when a precise frame count - // cannot be determined. Prefer Frames in all other cases. - ApproxFrames int64 - Frames int64 - SampleRate int - YoutubeItag int - MimeType string -} - -type Video struct { - ContentLength int64 - Duration time.Duration - // not sure if this are needed any more? - ThumbnailWidth int - ThumbnailHeight int - YoutubeItag int - MimeType string -} - -// MediaSet represents the media and metadata associated with a single media -// resource (for example, a YouTube video). -type MediaSet struct { - Audio Audio `json:"audio"` - Video Video `json:"video"` - ID uuid.UUID `json:"id"` - YoutubeID string `json:"youtube_id"` - - exists bool -} - -// New builds a new MediaSet with the given ID. -func NewMediaSet(youtubeID string) *MediaSet { - return &MediaSet{YoutubeID: youtubeID} -} - -// TODO: pass io.Readers/Writers instead of strings. -func (m *MediaSet) RawAudioPath() string { return fmt.Sprintf("cache/%s.raw", m.YoutubeID) } -func (m *MediaSet) EncodedAudioPath() string { return fmt.Sprintf("cache/%s.m4a", m.YoutubeID) } -func (m *MediaSet) VideoPath() string { return fmt.Sprintf("cache/%s.mp4", m.YoutubeID) } -func (m *MediaSet) ThumbnailPath() string { return fmt.Sprintf("cache/%s.jpg", m.YoutubeID) } -func (m *MediaSet) MetadataPath() string { return fmt.Sprintf("cache/%s.json", m.YoutubeID) } diff --git a/backend/media/service.go b/backend/media/service.go index 060df60..fa6d8f9 100644 --- a/backend/media/service.go +++ b/backend/media/service.go @@ -15,7 +15,6 @@ import ( "git.netflux.io/rob/clipper/config" "git.netflux.io/rob/clipper/generated/store" "github.com/aws/aws-sdk-go-v2/aws" - signerv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/google/uuid" "github.com/jackc/pgx/v4" @@ -40,69 +39,6 @@ const ( thumbnailHeight = 100 // " ) -// progressReader is a reader that prints progress logs as it reads. -type progressReader struct { - io.Reader - - label string - total, exp int64 - logger *zap.SugaredLogger -} - -func newProgressReader(reader io.Reader, label string, exp int64, logger *zap.SugaredLogger) *progressReader { - return &progressReader{ - Reader: reader, - exp: exp, - logger: logger.Named(fmt.Sprintf("ProgressReader %s", label)), - } -} - -func (r *progressReader) Read(p []byte) (int, error) { - n, err := r.Reader.Read(p) - r.total += int64(n) - - r.logger.Debugf("Read %d of %d (%.02f%%) bytes from the provided reader", r.total, r.exp, (float32(r.total)/float32(r.exp))*100.0) - - return n, err -} - -// Store wraps a database store. -type Store interface { - GetMediaSet(context.Context, uuid.UUID) (store.MediaSet, error) - GetMediaSetByYoutubeID(context.Context, string) (store.MediaSet, error) - CreateMediaSet(context.Context, store.CreateMediaSetParams) (store.MediaSet, error) - SetRawAudioUploaded(context.Context, store.SetRawAudioUploadedParams) (store.MediaSet, error) - SetEncodedAudioUploaded(context.Context, store.SetEncodedAudioUploadedParams) (store.MediaSet, error) - SetVideoUploaded(context.Context, store.SetVideoUploadedParams) (store.MediaSet, error) - SetVideoThumbnailUploaded(context.Context, store.SetVideoThumbnailUploadedParams) (store.MediaSet, error) -} - -// S3API provides an API to AWS S3. -type S3API struct { - S3Client - S3PresignClient -} - -// S3Client wraps the AWS S3 service client. -type S3Client interface { - GetObject(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) (*s3.GetObjectOutput, error) - CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput, ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) - UploadPart(context.Context, *s3.UploadPartInput, ...func(*s3.Options)) (*s3.UploadPartOutput, error) - AbortMultipartUpload(context.Context, *s3.AbortMultipartUploadInput, ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) - CompleteMultipartUpload(context.Context, *s3.CompleteMultipartUploadInput, ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) -} - -// S3PresignClient wraps the AWS S3 Presign client. -type S3PresignClient interface { - PresignGetObject(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) (*signerv4.PresignedHTTPRequest, error) -} - -// 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 exposes logical flows handling MediaSets. type MediaSetService struct { store Store @@ -123,7 +59,9 @@ func NewMediaSetService(store Store, youtubeClient YoutubeClient, s3API S3API, c } // Get fetches the metadata for a given MediaSet source. If it does not exist -// in the local DB, it will attempt to create it. +// in the local DB, it will attempt to create it. After the resource has been +// created, other endpoints (e.g. GetAudio) can be called to fetch media from +// Youtube and store it in S3. func (s *MediaSetService) Get(ctx context.Context, youtubeID string) (*MediaSet, error) { var ( mediaSet *MediaSet @@ -132,13 +70,13 @@ func (s *MediaSetService) Get(ctx context.Context, youtubeID string) (*MediaSet, mediaSet, err = s.findMediaSet(ctx, youtubeID) if err != nil { - return nil, fmt.Errorf("error getting existing media set: %v", err) + return nil, fmt.Errorf("error finding existing media set: %v", err) } if mediaSet == nil { mediaSet, err = s.createMediaSet(ctx, youtubeID) if err != nil { - return nil, fmt.Errorf("error getting new media set: %v", err) + return nil, fmt.Errorf("error creating new media set: %v", err) } } @@ -281,11 +219,11 @@ func (s *MediaSetService) GetVideo(ctx context.Context, id uuid.UUID) (GetVideoP return nil, fmt.Errorf("error getting media set: %v", err) } - // TODO: use mediaSet func to fetch s3Key - s3Key := fmt.Sprintf("media_sets/%s/video.mp4", mediaSet.ID) - if mediaSet.VideoS3UploadedAt.Valid { - input := s3.GetObjectInput{Bucket: aws.String(s.config.S3Bucket), Key: aws.String(s3Key)} + input := s3.GetObjectInput{ + Bucket: aws.String(s.config.S3Bucket), + Key: aws.String(mediaSet.VideoS3Key.String), + } request, signErr := s.s3.PresignGetObject(ctx, &input, s3.WithPresignExpires(getVideoExpiresIn)) if signErr != nil { return nil, fmt.Errorf("error generating presigned URL: %v", signErr) @@ -309,6 +247,9 @@ func (s *MediaSetService) GetVideo(ctx context.Context, id uuid.UUID) (GetVideoP return nil, fmt.Errorf("error fetching stream: %v", err) } + // TODO: use mediaSet func to fetch s3Key + s3Key := fmt.Sprintf("media_sets/%s/video.mp4", mediaSet.ID) + videoGetter := newVideoGetter(s.s3, s.store, s.logger) return videoGetter.GetVideo( ctx, @@ -654,3 +595,29 @@ func (s *MediaSetService) getThumbnailFromYoutube(ctx context.Context, mediaSet return VideoThumbnail{Width: int(thumbnail.Width), Height: int(thumbnail.Height), Data: imageData}, nil } + +// progressReader is a reader that prints progress logs as it reads. +type progressReader struct { + io.Reader + + label string + total, exp int64 + logger *zap.SugaredLogger +} + +func newProgressReader(reader io.Reader, label string, exp int64, logger *zap.SugaredLogger) *progressReader { + return &progressReader{ + Reader: reader, + exp: exp, + logger: logger.Named(fmt.Sprintf("ProgressReader %s", label)), + } +} + +func (r *progressReader) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + r.total += int64(n) + + r.logger.Debugf("Read %d of %d (%.02f%%) bytes from the provided reader", r.total, r.exp, (float32(r.total)/float32(r.exp))*100.0) + + return n, err +} diff --git a/backend/media/types.go b/backend/media/types.go new file mode 100644 index 0000000..7a24da6 --- /dev/null +++ b/backend/media/types.go @@ -0,0 +1,86 @@ +package media + +import ( + "context" + "io" + "time" + + "git.netflux.io/rob/clipper/generated/store" + signerv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" + youtubev2 "github.com/kkdai/youtube/v2" +) + +// An int16 has two bytes. +const SizeOfInt16 = 2 + +// MediaSet represents the media and metadata associated with a single media +// resource (for example, a YouTube video). +type MediaSet struct { + Audio Audio + Video Video + ID uuid.UUID + YoutubeID string +} + +// Audio contains the metadata for the audio part of the media set. +type Audio struct { + ContentLength int64 + Channels int + // ApproxFrames is used during initial processing when a precise frame count + // cannot be determined. Prefer Frames in all other cases. + ApproxFrames int64 + Frames int64 + SampleRate int + YoutubeItag int + MimeType string +} + +// Video contains the metadata for the video part of the media set. +type Video struct { + ContentLength int64 + Duration time.Duration + // not sure if this are needed any more? + ThumbnailWidth int + ThumbnailHeight int + YoutubeItag int + MimeType string +} + +// Store wraps a database store. +type Store interface { + GetMediaSet(context.Context, uuid.UUID) (store.MediaSet, error) + GetMediaSetByYoutubeID(context.Context, string) (store.MediaSet, error) + CreateMediaSet(context.Context, store.CreateMediaSetParams) (store.MediaSet, error) + SetRawAudioUploaded(context.Context, store.SetRawAudioUploadedParams) (store.MediaSet, error) + SetEncodedAudioUploaded(context.Context, store.SetEncodedAudioUploadedParams) (store.MediaSet, error) + SetVideoUploaded(context.Context, store.SetVideoUploadedParams) (store.MediaSet, error) + SetVideoThumbnailUploaded(context.Context, store.SetVideoThumbnailUploadedParams) (store.MediaSet, error) +} + +// S3API provides an API to AWS S3. +type S3API struct { + S3Client + S3PresignClient +} + +// S3Client wraps the AWS S3 service client. +type S3Client interface { + GetObject(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) (*s3.GetObjectOutput, error) + CreateMultipartUpload(context.Context, *s3.CreateMultipartUploadInput, ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) + UploadPart(context.Context, *s3.UploadPartInput, ...func(*s3.Options)) (*s3.UploadPartOutput, error) + AbortMultipartUpload(context.Context, *s3.AbortMultipartUploadInput, ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) + CompleteMultipartUpload(context.Context, *s3.CompleteMultipartUploadInput, ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) +} + +// S3PresignClient wraps the AWS S3 Presign client. +type S3PresignClient interface { + PresignGetObject(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) (*signerv4.PresignedHTTPRequest, error) +} + +// 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) +}