Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add thumbnail download command #328

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"math/rand"
"net/http"
"net/url"
"strconv"
"sync/atomic"

"log/slog"
)

const (
Expand All @@ -25,7 +24,8 @@ const (
)

var (
ErrNoFormat = errors.New("no video format provided")
ErrNoFormat = errors.New("no video format provided")
ErrNoThumbnail = errors.New("no thumbnail found")
)

// DefaultClient type to use. No reason to change but you could if you wanted to.
Expand Down Expand Up @@ -72,6 +72,30 @@ func (c *Client) GetVideoContext(ctx context.Context, url string) (*Video, error
return c.videoFromID(ctx, id)
}

// GetThumbnail returns the thumbnail image for a video.
func (c *Client) GetThumbnail(thumbnails Thumbnails) (io.ReadCloser, string, error) {
return c.GetThumbnailContext(context.Background(), thumbnails)
}

// GetThumbnailContext returns the thumbnail image for a video with a context.
func (c *Client) GetThumbnailContext(ctx context.Context, thumbnails Thumbnails) (io.ReadCloser, string, error) {
c.assureClient()

for _, thumbnail := range thumbnails {
resp, err := c.httpGet(ctx, thumbnail.URL)
if errors.Is(err, ErrUnexpectedStatusCode(http.StatusNotFound)) {
continue
}
if err != nil {
return nil, "", err
}

return resp.Body, resp.Header.Get("Content-Type"), nil
}

return nil, "", ErrNoThumbnail
}

func (c *Client) videoFromID(ctx context.Context, id string) (*Video, error) {
c.assureClient()

Expand Down
2 changes: 1 addition & 1 deletion cmd/youtubedr/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func download(id string) error {
func checkFFMPEG() error {
fmt.Println("check ffmpeg is installed....")
if err := exec.Command("ffmpeg", "-version").Run(); err != nil {
ffmpegCheck = fmt.Errorf("please check ffmpegCheck is installed correctly")
ffmpegCheck = fmt.Errorf("please check that ffmpeg is installed correctly")
}

return ffmpegCheck
Expand Down
135 changes: 135 additions & 0 deletions cmd/youtubedr/thumbnail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package main

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"

"github.com/kkdai/youtube/v2"
"github.com/spf13/cobra"
)

var thumbnailCmd = &cobra.Command{
Use: "thumbnail",
Short: "Downloads a thumbnail from youtube",
Example: `youtubedr thumbnail -n maxresdefault -o thumbnail.png https://www.youtube.com/watch\?v\=TGqoAUaivOY`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
exitOnError(downloadThumbnail(args[0]))
},
}

var (
thumbnailName string
noExtend bool
)

func init() {
rootCmd.AddCommand(thumbnailCmd)

thumbnailCmd.Flags().StringVarP(&outputFile, "filename", "o", "", "the output file, the default is generated by the video title")
thumbnailCmd.Flags().StringVarP(&outputDir, "directory", "d", ".", "the output directory")
thumbnailCmd.Flags().StringVarP(&thumbnailName, "name", "n", "", "the thumbnail name (ex. \"maxresdefault\")")
thumbnailCmd.Flags().BoolVarP(&noExtend, "known-only", "k", false, "only try thumbnails received in video response (lower quality)")
}

func downloadThumbnail(url string) error {
downloader := getDownloader()
video, err := downloader.GetVideo(url)

if err != nil {
return err
}

thumbnails := video.Thumbnails
if !noExtend {
thumbnails = thumbnails.Extended(video.ID)
}
if thumbnailName != "" {
thumbnails = thumbnails.FilterName(thumbnailName)
}
thumbnails.Sort()

if outputFile == "" {
return downloadAnyFormat(video.ID, thumbnails)
}

ext := filepath.Ext(outputFile)
switch ext {
case ".jpg", ".jpeg":
thumbnails = thumbnails.FilterExt(".jpg")
case ".webp":
thumbnails = thumbnails.FilterExt(".webp")
default:
return downloadAndEncode(thumbnails)
}

return downloadAnyFormat(video.ID, thumbnails)
}

func downloadAnyFormat(videoID string, thumbnails youtube.Thumbnails) error {
image, mimeType, err := downloader.GetThumbnail(thumbnails)

if err != nil {
return err
}

defer image.Close()

var f *os.File

if outputFile == "" {
switch mimeType {
case "image/jpeg":
f, err = os.Create(filepath.Join(outputDir, fmt.Sprintf("%s.jpg", videoID)))
case "image/webp":
f, err = os.Create(filepath.Join(outputDir, fmt.Sprintf("%s.webp", videoID)))
default:
return fmt.Errorf("unknown content type: %s", mimeType)
}
} else {
f, err = os.Create(filepath.Join(outputDir, outputFile))
}

if err != nil {
return err
}

defer f.Close()

if _, err := io.Copy(f, image); err != nil {
return err
}

return nil
}

func downloadAndEncode(thumbnails youtube.Thumbnails) error {
if err := checkFFMPEG(); err != nil {
return err
}

image, _, err := downloader.GetThumbnail(thumbnails)
if err != nil {
return err
}

defer image.Close()

//nolint:gosec
ffmpeg := exec.Command("ffmpeg",
"-i", "-",
"-update", "true",
"-frames:v", "1",
filepath.Join(outputDir, outputFile),
"-loglevel", "warning",
)

ffmpeg.Stdin = image
ffmpeg.Stdout = os.Stdout
ffmpeg.Stderr = os.Stderr

return ffmpeg.Run()
}
2 changes: 0 additions & 2 deletions response_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@ func (f *Format) LanguageDisplayName() string {
return f.AudioTrack.DisplayName
}

type Thumbnails []Thumbnail

type Thumbnail struct {
URL string
Width uint
Expand Down
Loading
Loading