Skip to content
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
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (c *Client) GetPlaylist(url string) (*Playlist, error) {
// for these videos. Playlist entries cannot be downloaded, as they lack all the required metadata, but
// can be used to enumerate all IDs, Authors, Titles, etc.
func (c *Client) GetPlaylistContext(ctx context.Context, url string) (*Playlist, error) {
id, err := extractPlaylistID(url)
id, err := ExtractPlaylistID(url)
if err != nil {
return nil, fmt.Errorf("extractPlaylistID failed: %w", err)
}
Expand Down
54 changes: 49 additions & 5 deletions cmd/youtubedr/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,65 @@ import (
"fmt"
"log"
"os/exec"
"strconv"
"strings"

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

var (
errNotID = fmt.Errorf("cannot detect ID in given input")
)

// downloadCmd represents the download command
var downloadCmd = &cobra.Command{
Use: "download",
Short: "Downloads a video from youtube",
Short: "Downloads a video or a playlist from youtube",
Example: `youtubedr -o "Campaign Diary".mp4 https://www.youtube.com/watch\?v\=XbNghLqsVwU`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
exitOnError(download(args[0]))
if playlistID, err := youtube.ExtractPlaylistID(args[0]); err != nil {
videoID, err := youtube.ExtractVideoID(args[0])
if err != nil {
exitOnError(errNotID)
}
log.Printf(
"download video %s to directory %s\n",
videoID,
outputDir,
)
exitOnError(download(videoID))
} else {
playlist, err := getDownloader().GetPlaylist(playlistID)
if err != nil {
exitOnError(err)
}
log.Printf(
"download %d videos from playlist %s to directory %s\n",
len(playlist.Videos),
playlist.ID,
outputDir,
)
var errors []error
outputFileOrigin := outputFile
for i, v := range playlist.Videos {
if len(outputFileOrigin) != 0 {
index := v.Index
if len(index) == 0 {
// In case video does not contain index
index = strconv.Itoa(i)
}
outputFile = fmt.Sprintf("%s-%s", outputFile, index)
}
if err := download(v.ID); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
exitOnErrors(errors)
}
}
},
}

Expand All @@ -30,7 +76,7 @@ var (
func init() {
rootCmd.AddCommand(downloadCmd)

downloadCmd.Flags().StringVarP(&outputFile, "filename", "o", "", "The output file, the default is genated by the video title.")
downloadCmd.Flags().StringVarP(&outputFile, "filename", "o", "", "The output file, the default is generated from the video title.")
downloadCmd.Flags().StringVarP(&outputDir, "directory", "d", ".", "The output directory.")
addQualityFlag(downloadCmd.Flags())
addMimeTypeFlag(downloadCmd.Flags())
Expand All @@ -42,8 +88,6 @@ func download(id string) error {
return err
}

log.Println("download to directory", outputDir)

if strings.HasPrefix(outputQuality, "hd") {
if err := checkFFMPEG(); err != nil {
return err
Expand Down
9 changes: 9 additions & 0 deletions cmd/youtubedr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,12 @@ func exitOnError(err error) {
os.Exit(1)
}
}

func exitOnErrors(errors []error) {
for _, err := range errors {
fmt.Fprintln(os.Stderr, err)
}
if len(errors) != 0 {
os.Exit(1)
}
}
21 changes: 16 additions & 5 deletions playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ type Playlist struct {

type PlaylistEntry struct {
ID string
Index string
Title string
Author string
Duration time.Duration
}

func extractPlaylistID(url string) (string, error) {
func ExtractPlaylistID(url string) (string, error) {
if playlistIDRegex.Match([]byte(url)) {
return url, nil
}
Expand Down Expand Up @@ -128,10 +129,11 @@ func (p *Playlist) UnmarshalJSON(b []byte) (err error) {

type videosJSONExtractor struct {
Renderer *struct {
ID string `json:"videoId"`
Title withRuns `json:"title"`
Author withRuns `json:"shortBylineText"`
Duration string `json:"lengthSeconds"`
ID string `json:"videoId"`
Index indexText `json:"index"`
Title withRuns `json:"title"`
Author withRuns `json:"shortBylineText"`
Duration string `json:"lengthSeconds"`
} `json:"playlistVideoRenderer"`
}

Expand All @@ -142,6 +144,7 @@ func (vje videosJSONExtractor) PlaylistEntry() *PlaylistEntry {
}
return &PlaylistEntry{
ID: vje.Renderer.ID,
Index: vje.Renderer.Index.String(),
Title: vje.Renderer.Title.String(),
Author: vje.Renderer.Author.String(),
Duration: time.Second * time.Duration(ds),
Expand All @@ -154,9 +157,17 @@ type withRuns struct {
} `json:"runs"`
}

type indexText struct {
SimpleText string `json:"simpleText"`
}

func (wr withRuns) String() string {
if len(wr.Runs) > 0 {
return wr.Runs[0].Text
}
return ""
}

func (it indexText) String() string {
return it.SimpleText
}
2 changes: 1 addition & 1 deletion playlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestYoutube_extractPlaylistID(t *testing.T) {

for _, v := range tests {
t.Run(v.name, func(t *testing.T) {
id, err := extractPlaylistID(v.url)
id, err := ExtractPlaylistID(v.url)

assert.Equal(t, v.expectedError, err)
assert.Equal(t, v.expectedID, id)
Expand Down