Browse Source

Some more CLI tests

terorie 9 months ago
parent
commit
0e47e3a0ba

+ 5
- 2
README.md View File

@@ -2,6 +2,8 @@
2 2
 
3 3
 > YT metadata extractor inspired by [`youtube-ma` by _CorentinB_][youtube-ma]
4 4
 
5
+__Warning: Very WIP rn!__
6
+
5 7
 ##### Build
6 8
 
7 9
 Install and compile the Go project with `go get github.com/terorie/yt-mango`!
@@ -15,14 +17,15 @@ If you don't have a Go toolchain, grab an executable from the Releases tab
15 17
     - _/apiclassic_: HTML API implementation (parsing using [goquery][goquery])
16 18
     - _/apijson_: JSON API implementation (parsing using [fastjson][fastjson])
17 19
 - _/net_: HTTP utilities (asnyc HTTP implementation)
20
+- _/cmd_: Cobra CLI
21
+- _/util_: I don't have a better place for these
18 22
 
19 23
 - _/pretty_: (not yet used) Terminal color utilities
20 24
 - _/controller_: (not yet implemented) worker management
21 25
     - _/db_: (not yet implemented) MongoDB connection
22 26
     - _???_: (not yet implemented) Redis queue
23
-- _/classic_: Extractor calling the HTML `/watch` API
24
-- _/watchapi_: Extractor calling the JSON `/watch` API
25 27
 
26 28
  [youtube-ma]: https://github.com/CorentinB/youtube-ma
27 29
  [goquery]: https://github.com/PuerkitoBio/goquery
28 30
  [fastjson]: https://github.com/valyala/fastjson
31
+ [cobra]: https://github.com/spf13/cobra

+ 15
- 12
api/api.go View File

@@ -4,6 +4,7 @@ import (
4 4
 	"github.com/terorie/yt-mango/data"
5 5
 	"net/http"
6 6
 	"github.com/terorie/yt-mango/apijson"
7
+	"github.com/terorie/yt-mango/apiclassic"
7 8
 )
8 9
 
9 10
 type API struct {
@@ -25,23 +26,25 @@ var Main *API = nil
25 26
 
26 27
 // TODO: Remove when everything is implemented
27 28
 var TempAPI = API{
28
-	GrabVideo: apijson.GrabVideo,
29
-	ParseVideo: apijson.ParseVideo,
29
+	GrabVideo: apiclassic.GrabVideo,
30
+	ParseVideo: apiclassic.ParseVideo,
31
+
32
+	GrabChannel: apiclassic.GrabChannel,
33
+	ParseChannel: apiclassic.ParseChannel,
30 34
 
31 35
 	GrabChannelPage: apijson.GrabChannelPage,
32 36
 	ParseChannelVideoURLs: apijson.ParseChannelVideoURLs,
33 37
 }
34 38
 
35
-/*var ClassicAPI = API{
36
-	GetVideo: apiclassic.GetVideo,
37
-	GetVideoSubtitleList: apiclassic.GetVideoSubtitleList,
38
-	GetChannel: apiclassic.GetChannel,
39
-	GetChannelVideoURLs: apiclassic.GetChannelVideoURLs,
39
+var ClassicAPI = API{
40
+	GrabVideo: apiclassic.GrabVideo,
41
+	ParseVideo: apiclassic.ParseVideo,
42
+
43
+	GrabChannel: apiclassic.GrabChannel,
44
+	ParseChannel: apiclassic.ParseChannel,
40 45
 }
41 46
 
42 47
 var JsonAPI = API{
43
-	GetVideo: apijson.GetVideo,
44
-	GetVideoSubtitleList: apiclassic.GetVideoSubtitleList,
45
-	GetChannel: apijson.GetChannel,
46
-	GetChannelVideoURLs: apijson.GetChannelVideoURLs,
47
-}*/
48
+	GrabChannelPage: apijson.GrabChannelPage,
49
+	ParseChannelVideoURLs: apijson.ParseChannelVideoURLs,
50
+}

+ 45
- 10
api/ids.go View File

@@ -2,37 +2,41 @@ package api
2 2
 
3 3
 import (
4 4
 	"regexp"
5
-	"os"
6 5
 	"strings"
7 6
 	"log"
8 7
 	"net/url"
9 8
 )
10 9
 
10
+// FIXME: API package should be abstract, no utility code in here
11
+
11 12
 var matchChannelID = regexp.MustCompile("^([\\w\\-]|(%3[dD]))+$")
13
+var matchVideoID = regexp.MustCompile("^[\\w\\-]+$")
12 14
 
13
-func GetChannelID(chanURL string) (string, error) {
15
+// Input: Channel ID or link to YT channel page
16
+// Output: Channel ID or "" on error
17
+func GetChannelID(chanURL string) string {
14 18
 	if !matchChannelID.MatchString(chanURL) {
15 19
 		// Check if youtube.com domain
16 20
 		_url, err := url.Parse(chanURL)
17 21
 		if err != nil || (_url.Host != "www.youtube.com" && _url.Host != "youtube.com") {
18 22
 			log.Fatal("Not a channel ID:", chanURL)
19
-			os.Exit(1)
23
+			return ""
20 24
 		}
21 25
 
22 26
 		// Check if old /user/ URL
23 27
 		if strings.HasPrefix(_url.Path, "/user/") {
24 28
 			// TODO Implement extraction of channel ID
25
-			log.Fatal("New /channel/ link is required!\n" +
26
-				"The old /user/ links do not work.")
27
-			os.Exit(1)
29
+			log.Print("New /channel/ link is required!\n" +
30
+				"The old /user/ links do not work:", chanURL)
31
+			return ""
28 32
 		}
29 33
 
30 34
 		// Remove /channel/ path
31 35
 		channelID := strings.TrimPrefix(_url.Path, "/channel/")
32 36
 		if len(channelID) == len(_url.Path) {
33 37
 			// No such prefix to be removed
34
-			log.Fatal("Not a channel ID:", channelID)
35
-			os.Exit(1)
38
+			log.Print("Not a channel ID:", channelID)
39
+			return ""
36 40
 		}
37 41
 
38 42
 		// Remove rest of path from channel ID
@@ -41,9 +45,40 @@ func GetChannelID(chanURL string) (string, error) {
41 45
 			channelID = channelID[:slashIndex]
42 46
 		}
43 47
 
44
-		return channelID, nil
48
+		return channelID
45 49
 	} else {
46 50
 		// It's already a channel ID
47
-		return chanURL, nil
51
+		return chanURL
52
+	}
53
+}
54
+
55
+func GetVideoID(vidURL string) string {
56
+	if !matchVideoID.MatchString(vidURL) {
57
+		// Check if youtube.com domain
58
+		_url, err := url.Parse(vidURL)
59
+		if err != nil || (_url.Host != "www.youtube.com" && _url.Host != "youtube.com") {
60
+			log.Fatal("Not a video ID:", vidURL)
61
+			return ""
62
+		}
63
+
64
+		// TODO Support other URLs (/v or /embed)
65
+
66
+		// Check if watch path
67
+		if !strings.HasPrefix(_url.Path, "/watch") {
68
+			log.Fatal("Not a watch URL:", vidURL)
69
+			return ""
70
+		}
71
+
72
+		// Parse query string
73
+		query := _url.Query()
74
+		videoID := query.Get("v")
75
+		if videoID == "" {
76
+			log.Fatal("Invalid watch URL:", vidURL)
77
+			return ""
78
+		}
79
+
80
+		return videoID
81
+	} else {
82
+		return vidURL
48 83
 	}
49 84
 }

+ 0
- 38
apiclassic/get.go View File

@@ -1,38 +0,0 @@
1
-package apiclassic
2
-
3
-import (
4
-	"github.com/terorie/yt-mango/data"
5
-	"errors"
6
-)
7
-
8
-func GetVideo(v *data.Video) error {
9
-	if len(v.ID) == 0 { return errors.New("no video ID") }
10
-
11
-	// Download the doc tree
12
-	doc, err := GrabVideo(v.ID)
13
-	if err != nil { return err }
14
-
15
-	// Parse it
16
-	p := parseInfo{v, doc}
17
-	err = p.parse()
18
-	if err != nil { return err }
19
-
20
-	return nil
21
-}
22
-
23
-func GetVideoSubtitleList(v *data.Video) (err error) {
24
-	tracks, err := GrabSubtitleList(v.ID)
25
-	if err != nil { return }
26
-	for _, track := range tracks.Tracks {
27
-		v.Subtitles = append(v.Subtitles, track.LangCode)
28
-	}
29
-	return
30
-}
31
-
32
-func GetChannel(c *data.Channel) error {
33
-	return errors.New("not implemented")
34
-}
35
-
36
-func GetChannelVideoURLs(channelID string, page uint) ([]string, error) {
37
-	return nil, errors.New("not implemented")
38
-}

+ 15
- 16
apiclassic/grab.go View File

@@ -4,31 +4,22 @@ import (
4 4
 	"net/http"
5 5
 	"errors"
6 6
 	"encoding/xml"
7
-	"github.com/PuerkitoBio/goquery"
8 7
 	"github.com/terorie/yt-mango/net"
8
+	"fmt"
9 9
 )
10 10
 
11
-const mainURL = "https://www.youtube.com/watch?has_verified=1&bpctr=6969696969&v="
11
+const videoURL = "https://www.youtube.com/watch?has_verified=1&bpctr=6969696969&v="
12 12
 const subtitleURL = "https://video.google.com/timedtext?type=list&v="
13
+const channelURL = "https://www.youtube.com/channel/%s/about"
13 14
 
14
-// Grabs a HTML video page and returns the document tree
15
-func GrabVideo(videoID string) (doc *goquery.Document, err error) {
16
-	req, err := http.NewRequest("GET", mainURL + videoID, nil)
17
-	if err != nil { return }
15
+func GrabVideo(videoID string) *http.Request {
16
+	req, err := http.NewRequest("GET", videoURL + videoID, nil)
17
+	if err != nil { panic(err) }
18 18
 	setHeaders(&req.Header)
19 19
 
20
-	res, err := net.Client.Do(req)
21
-	if err != nil { return }
22
-	if res.StatusCode != 200 { return nil, errors.New("HTTP failure") }
23
-
24
-	defer res.Body.Close()
25
-	doc, err = goquery.NewDocumentFromReader(res.Body)
26
-	if err != nil { return nil, err }
27
-
28
-	return
20
+	return req
29 21
 }
30 22
 
31
-// Grabs and parses a subtitle list
32 23
 func GrabSubtitleList(videoID string) (tracks *XMLSubTrackList, err error) {
33 24
 	req, err := http.NewRequest("GET", subtitleURL + videoID, nil)
34 25
 	if err != nil { return }
@@ -46,6 +37,14 @@ func GrabSubtitleList(videoID string) (tracks *XMLSubTrackList, err error) {
46 37
 	return
47 38
 }
48 39
 
40
+func GrabChannel(channelID string) *http.Request {
41
+	req, err := http.NewRequest("GET", fmt.Sprintf(channelURL, channelID), nil)
42
+	if err != nil { panic(err) }
43
+	setHeaders(&req.Header)
44
+
45
+	return req
46
+}
47
+
49 48
 func setHeaders(h *http.Header) {
50 49
 	h.Add("Host", "www.youtube.com")
51 50
 	h.Add("User-Agent", "yt-mango/0.1")

+ 61
- 0
apiclassic/parsechannel.go View File

@@ -0,0 +1,61 @@
1
+package apiclassic
2
+
3
+import (
4
+	"github.com/terorie/yt-mango/data"
5
+	"net/http"
6
+	"errors"
7
+	"github.com/PuerkitoBio/goquery"
8
+	"strconv"
9
+)
10
+
11
+func ParseChannel(c *data.Channel, res *http.Response) (err error) {
12
+	if res.StatusCode != 200 { return errors.New("HTTP failure") }
13
+
14
+	defer res.Body.Close()
15
+	doc, err := goquery.NewDocumentFromReader(res.Body)
16
+	if err != nil { return }
17
+
18
+	p := parseChannelInfo{c, doc}
19
+	return p.parse()
20
+}
21
+
22
+type parseChannelInfo struct {
23
+	c *data.Channel
24
+	doc *goquery.Document
25
+}
26
+
27
+func (p *parseChannelInfo) parse() error {
28
+	if err := p.parseMetas();
29
+		err != nil { return err }
30
+	return nil
31
+}
32
+
33
+func (p *parseChannelInfo) parseMetas() error {
34
+	p.doc.Find("head").RemoveFiltered("#watch-container")
35
+	enumMetas(p.doc.Find("head").Find("meta"), func(tag metaTag)bool {
36
+		content := tag.content
37
+		switch tag.typ {
38
+		case metaProperty:
39
+			switch tag.name {
40
+			case "og:title":
41
+				p.c.Name = content
42
+			}
43
+		case metaItemProp:
44
+			switch tag.name {
45
+			case "paid":
46
+				if val, err := strconv.ParseBool(content);
47
+					err == nil { p.c.Paid = val }
48
+			}
49
+		}
50
+		return false
51
+	})
52
+	return nil
53
+}
54
+
55
+func (p *parseChannelInfo) parseAbout() error {
56
+	p.doc.Find(".about-stats").Find(".about-stat").Each(func(_ int, s *goquery.Selection) {
57
+		text := s.Text()
58
+		println(text)
59
+	})
60
+	return nil
61
+}

+ 3
- 3
apiclassic/parsedescription.go View File

@@ -4,13 +4,13 @@ import (
4 4
 	"errors"
5 5
 	"golang.org/x/net/html"
6 6
 	"bytes"
7
-	"github.com/terorie/yt-mango/net"
8 7
 	"strings"
8
+	"github.com/terorie/yt-mango/util"
9 9
 )
10 10
 
11 11
 const descriptionSelector = "#eow-description"
12 12
 
13
-func (p *parseInfo) parseDescription() error {
13
+func (p *parseVideoInfo) parseDescription() error {
14 14
 	// Find description root
15 15
 	descNode := p.doc.Find(descriptionSelector).First()
16 16
 	if len(descNode.Nodes) == 0 { return errors.New("could not find description") }
@@ -24,7 +24,7 @@ func (p *parseInfo) parseDescription() error {
24 24
 		case html.TextNode:
25 25
 			// FIXME: "&amp;lt;" gets parsed to => "<"
26 26
 			// Write text to buffer, escaping markdown
27
-			err := net.MarkdownTextEscape.ToBuffer(c.Data, &buffer)
27
+			err := util.MarkdownTextEscape.ToBuffer(c.Data, &buffer)
28 28
 			if err != nil { return err }
29 29
 		case html.ElementNode:
30 30
 			switch c.Data {

+ 47
- 0
apiclassic/parsemetas.go View File

@@ -0,0 +1,47 @@
1
+package apiclassic
2
+
3
+import "github.com/PuerkitoBio/goquery"
4
+
5
+type metaType uint8
6
+const (
7
+	metaUnknown = metaType(iota)
8
+	metaProperty
9
+	metaItemProp
10
+)
11
+
12
+type metaTag struct {
13
+	typ metaType
14
+	name string
15
+	content string
16
+}
17
+
18
+func enumMetas(s *goquery.Selection, next func(metaTag)bool) {
19
+	// For each <meta>
20
+	s.EachWithBreak(func(i int, s *goquery.Selection) bool {
21
+		tag := metaTag{ metaUnknown, "", "" }
22
+		listAttrs: for _, attr := range s.Nodes[0].Attr {
23
+			switch attr.Key {
24
+				case "property":
25
+					tag.typ = metaProperty
26
+					tag.name = attr.Val
27
+					break listAttrs
28
+				case "itemprop":
29
+					tag.typ = metaItemProp
30
+					tag.name = attr.Val
31
+					break listAttrs
32
+				case "content":
33
+					tag.content = attr.Val
34
+					break listAttrs
35
+			}
36
+
37
+			if tag.typ == metaUnknown { continue }
38
+			if len(tag.content) == 0 { continue }
39
+
40
+			// Callback tag
41
+			if !next(tag) {
42
+				return true
43
+			}
44
+		}
45
+		return false
46
+	})
47
+}

apiclassic/parse.go → apiclassic/parsevideo.go View File

@@ -9,6 +9,7 @@ import (
9 9
 	"regexp"
10 10
 	"github.com/valyala/fastjson"
11 11
 	"strings"
12
+	"net/http"
12 13
 )
13 14
 
14 15
 const likeBtnSelector = ".like-button-renderer-like-button-unclicked"
@@ -19,12 +20,23 @@ const channelNameSelector = ".yt-uix-sessionlink"
19 20
 
20 21
 var playerConfigErr = errors.New("failed to parse player config")
21 22
 
22
-type parseInfo struct {
23
+func ParseVideo(v *data.Video, res *http.Response) (err error) {
24
+	if res.StatusCode != 200 { return errors.New("HTTP failure") }
25
+
26
+	defer res.Body.Close()
27
+	doc, err := goquery.NewDocumentFromReader(res.Body)
28
+	if err != nil { return }
29
+
30
+	p := parseVideoInfo{v, doc}
31
+	return p.parse()
32
+}
33
+
34
+type parseVideoInfo struct {
23 35
 	v *data.Video
24 36
 	doc *goquery.Document
25 37
 }
26 38
 
27
-func (p *parseInfo) parse() error {
39
+func (p *parseVideoInfo) parse() error {
28 40
 	if err := p.parseLikeDislike();
29 41
 		err != nil { return err }
30 42
 	if err := p.parseViewCount();
@@ -40,7 +52,7 @@ func (p *parseInfo) parse() error {
40 52
 	return nil
41 53
 }
42 54
 
43
-func (p *parseInfo) parseLikeDislike() error {
55
+func (p *parseVideoInfo) parseLikeDislike() error {
44 56
 	likeText := p.doc.Find(likeBtnSelector).First().Text()
45 57
 	dislikeText := p.doc.Find(dislikeBtnSelector).First().Text()
46 58
 
@@ -57,7 +69,7 @@ func (p *parseInfo) parseLikeDislike() error {
57 69
 	return nil
58 70
 }
59 71
 
60
-func (p *parseInfo) parseViewCount() error {
72
+func (p *parseVideoInfo) parseViewCount() error {
61 73
 	viewCountText := p.doc.Find(viewCountSelector).First().Text()
62 74
 	viewCount, err := extractNumber(viewCountText)
63 75
 	if err != nil { return err }
@@ -65,7 +77,7 @@ func (p *parseInfo) parseViewCount() error {
65 77
 	return nil
66 78
 }
67 79
 
68
-func (p *parseInfo) parseUploader() error {
80
+func (p *parseVideoInfo) parseUploader() error {
69 81
 	userInfo := p.doc.Find(userInfoSelector)
70 82
 	userLinkNode := userInfo.Find(".yt-uix-sessionlink")
71 83
 
@@ -81,30 +93,12 @@ func (p *parseInfo) parseUploader() error {
81 93
 	return nil
82 94
 }
83 95
 
84
-func (p *parseInfo) parseMetas() error {
85
-	metas := p.doc.Find("meta")
86
-	// For each <meta>
87
-	for _, node := range metas.Nodes {
88
-		// Attributes
89
-		var content string
90
-		var itemprop string
91
-		var prop string
92
-
93
-		// Parse attributes
94
-		for _, attr := range node.Attr {
95
-			switch attr.Key {
96
-			case "property": prop = attr.Val
97
-			case "itemprop": itemprop = attr.Val
98
-			case "content": content = attr.Val
99
-			}
100
-		}
101
-
102
-		// Content not set
103
-		if len(content) == 0 { continue }
104
-
105
-		// <meta property …
106
-		if len(prop) != 0 {
107
-			switch prop {
96
+func (p *parseVideoInfo) parseMetas() (err error) {
97
+	enumMetas(p.doc.Selection, func(tag metaTag)bool {
98
+		content := tag.content
99
+		switch tag.typ {
100
+		case metaProperty:
101
+			switch tag.name {
108 102
 			case "og:title":
109 103
 				p.v.Title = content
110 104
 			case "og:video:tag":
@@ -114,11 +108,8 @@ func (p *parseInfo) parseMetas() error {
114 108
 			case "og:image":
115 109
 				p.v.Thumbnail = content
116 110
 			}
117
-			continue
118
-		}
119
-		// <meta itemprop …
120
-		if len(itemprop) != 0 {
121
-			switch itemprop {
111
+		case metaItemProp:
112
+			switch tag.name {
122 113
 			case "datePublished":
123 114
 				if val, err := time.Parse("2006-01-02", content);
124 115
 					err == nil { p.v.UploadDate = val }
@@ -130,19 +121,20 @@ func (p *parseInfo) parseMetas() error {
130 121
 				if val, err := parseDuration(content); err == nil {
131 122
 					p.v.Duration = val
132 123
 				} else {
133
-					return err
124
+					return false
134 125
 				}
135 126
 			case "isFamilyFriendly":
136 127
 				if val, err := strconv.ParseBool(content);
137 128
 					err == nil { p.v.FamilyFriendly = val }
138 129
 			}
139
-			continue
140 130
 		}
141
-	}
142
-	return nil
131
+		return true
132
+	})
133
+
134
+	return err
143 135
 }
144 136
 
145
-func (p *parseInfo) parsePlayerConfig() error {
137
+func (p *parseVideoInfo) parsePlayerConfig() error {
146 138
 	var json string
147 139
 
148 140
 	p.doc.Find("script").EachWithBreak(func(_ int, s *goquery.Selection) bool {

+ 0
- 1
apijson/grab.go View File

@@ -8,7 +8,6 @@ const videoURL = "https://www.youtube.com/watch?pbj=1&v="
8 8
 const channelURL = "https://www.youtube.com/browse_ajax?ctoken="
9 9
 
10 10
 func GrabVideo(videoID string) *http.Request {
11
-	// Prepare request
12 11
 	req, err := http.NewRequest("GET", videoURL + videoID, nil)
13 12
 	if err != nil { panic(err) }
14 13
 	setHeaders(&req.Header)

+ 1
- 0
cmd/channel.go View File

@@ -14,4 +14,5 @@ var Channel = cobra.Command{
14 14
 func init() {
15 15
 	channelDumpCmd.Flags().BoolVarP(&force, "force", "f", false, "Overwrite the output file if it already exists")
16 16
 	Channel.AddCommand(&channelDumpCmd)
17
+	Channel.AddCommand(&channelDetailCmd)
17 18
 }

+ 43
- 0
cmd/channeldetail.go View File

@@ -0,0 +1,43 @@
1
+package cmd
2
+
3
+import (
4
+	"github.com/spf13/cobra"
5
+	"github.com/terorie/yt-mango/api"
6
+	"os"
7
+	"log"
8
+	"github.com/terorie/yt-mango/net"
9
+	"github.com/terorie/yt-mango/data"
10
+	"fmt"
11
+	"encoding/json"
12
+)
13
+
14
+var channelDetailCmd = cobra.Command{
15
+	Use: "detail <channel ID>",
16
+	Short: "Get detail about a channel",
17
+	Args: cobra.ExactArgs(1),
18
+	Run: doChannelDetail,
19
+}
20
+
21
+func doChannelDetail(_ *cobra.Command, args []string) {
22
+	channelID := args[0]
23
+
24
+	channelID = api.GetChannelID(channelID)
25
+	if channelID == "" {
26
+		os.Exit(1)
27
+	}
28
+
29
+	channelReq := api.Main.GrabChannel(channelID)
30
+
31
+	res, err := net.Client.Do(channelReq)
32
+	if err != nil {
33
+		log.Fatal(err)
34
+		os.Exit(1)
35
+	}
36
+
37
+	var c data.Channel
38
+	api.Main.ParseChannel(&c, res)
39
+
40
+	bytes, err := json.MarshalIndent(&c, "", "\t")
41
+	if err != nil { panic(err) }
42
+	fmt.Println(string(bytes))
43
+}

+ 2
- 5
cmd/channeldump.go View File

@@ -56,11 +56,8 @@ func doChannelDump(_ *cobra.Command, args []string) {
56 56
 	}
57 57
 	channelDumpContext.printResults = printResults
58 58
 
59
-	channelID, err := api.GetChannelID(channelID)
60
-	if err != nil {
61
-		log.Print(err)
62
-		os.Exit(1)
63
-	}
59
+	channelID = api.GetChannelID(channelID)
60
+	if channelID == "" { os.Exit(1) }
64 61
 
65 62
 	log.Printf("Starting work on channel ID \"%s\".", channelID)
66 63
 	channelDumpContext.startTime = time.Now()

+ 31
- 4
cmd/videodetail.go View File

@@ -1,14 +1,41 @@
1 1
 package cmd
2 2
 
3
-import "github.com/spf13/cobra"
3
+import (
4
+	"github.com/spf13/cobra"
5
+	"github.com/terorie/yt-mango/api"
6
+	"os"
7
+	"github.com/terorie/yt-mango/net"
8
+	"github.com/terorie/yt-mango/data"
9
+	"log"
10
+	"fmt"
11
+	"encoding/json"
12
+)
4 13
 
5 14
 var videoDetailCmd = cobra.Command{
6 15
 	Use: "detail <video ID> [file]",
7 16
 	Short: "Get details about a video",
17
+	Args: cobra.ExactArgs(1),
8 18
 	Run: func(cmd *cobra.Command, args []string) {
19
+		videoID := args[0]
9 20
 
10
-	},
11
-}
21
+		videoID = api.GetVideoID(videoID)
22
+		if videoID == "" {
23
+			os.Exit(1)
24
+		}
25
+
26
+		videoReq := api.Main.GrabVideo(videoID)
12 27
 
13
-func init()  {
28
+		res, err := net.Client.Do(videoReq)
29
+		if err != nil {
30
+			log.Fatal(err)
31
+			os.Exit(1)
32
+		}
33
+
34
+		var v data.Video
35
+		api.Main.ParseVideo(&v, res)
36
+
37
+		bytes, err := json.MarshalIndent(&v, "", "\t")
38
+		if err != nil { panic(err) }
39
+		fmt.Println(string(bytes))
40
+	},
14 41
 }

+ 2
- 0
data/channel.go View File

@@ -3,4 +3,6 @@ package data
3 3
 type Channel struct {
4 4
 	ID string `json:"id"`
5 5
 	Name string `json:"name"`
6
+	Paid bool `json:"paid"`
7
+	Thumbnail string `json:"thumbnail"`
6 8
 }

+ 2
- 2
main.go View File

@@ -39,8 +39,8 @@ func main() {
39 39
 
40 40
 			switch forceAPI {
41 41
 			case "": api.Main = &api.TempAPI
42
-			//case "classic": api.Main = &api.ClassicAPI
43
-			//case "json": api.Main = &api.JsonAPI
42
+			case "classic": api.Main = &api.ClassicAPI
43
+			case "json": api.Main = &api.JsonAPI
44 44
 			default:
45 45
 				fmt.Fprintln(os.Stderr, "Invalid API specified.\n" +
46 46
 					"Valid options are: \"classic\" and \"json\"")

api/escape.go → util/escape.go View File

@@ -1,4 +1,4 @@
1
-package api
1
+package util
2 2
 
3 3
 import "bytes"
4 4
 

api/markdown.go → util/markdown.go View File

@@ -1,4 +1,4 @@
1
-package api
1
+package util
2 2
 
3 3
 var MarkdownTextEscape EscapeMap
4 4
 var MarkdownLinkEscape EscapeMap

+ 2
- 0
version/get.go View File

@@ -1,5 +1,7 @@
1 1
 package version
2 2
 
3
+// TODO Refactor: Dedicating a single package is too much
4
+
3 5
 func Get() string {
4 6
 	return "v0.1 -- dev"
5 7
 }

Loading…
Cancel
Save