diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..69022e2
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,11 @@
+version: 2
+jobs:
+ build:
+ working_directory: /go/src/s32x.com/anirip
+ docker:
+ - image: circleci/golang:1.11.3
+ steps:
+ - checkout
+ - run:
+ name: Run unit tests
+ command: make test
\ No newline at end of file
diff --git a/common/error.go b/common/error.go
deleted file mode 100644
index e1a419f..0000000
--- a/common/error.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package common /* import "s32x.com/anirip/common" */
-
-import "fmt"
-
-// Error represents a generic anirip error
-type Error struct {
- Msg string
- Err error
-}
-
-// NewError generates a new generic anirip error
-func NewError(msg string, err error) *Error {
- return &Error{Msg: msg, Err: err}
-}
-
-// Error implements the error interface
-func (e *Error) Error() string {
- if e.Err != nil {
- return fmt.Sprintf("Error: %v - %v", e.Msg, e.Err)
- }
- return fmt.Sprintf("Error: %v", e.Msg)
-}
diff --git a/common/httpclient.go b/common/httpclient.go
index b91b805..e49bbd5 100644
--- a/common/httpclient.go
+++ b/common/httpclient.go
@@ -1,7 +1,6 @@
package common /* import "s32x.com/anirip/common" */
import (
- "errors"
"fmt"
"io"
"io/ioutil"
@@ -32,14 +31,15 @@ type HTTPClient struct {
// NewHTTPClient generates a new HTTPClient Requester that
// contains a random user-agent to emulate browser requests
-func NewHTTPClient() (*HTTPClient, error) {
+func NewHTTPClient() *HTTPClient {
// Create the client and attach a cookie jar
client := &http.Client{}
client.Jar, _ = cookiejar.New(nil)
+
return &HTTPClient{
Client: client,
UserAgent: randomUA(),
- }, nil
+ }
}
// randomUA retrieves a list of user-agents and returns a
@@ -81,7 +81,7 @@ func (c *HTTPClient) Get(url string, header http.Header) (*http.Response, error)
// Assemble our request and attach all headers and cookies
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("creating GET request: %w", err)
}
if header != nil {
req.Header = header
@@ -97,7 +97,7 @@ func (c *HTTPClient) request(req *http.Request) (*http.Response, error) {
// Executes the request
res, err := c.Client.Do(req)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("processing request: %w", err)
}
// If the server is in IUAM mode, solve the challenge and retry
@@ -107,11 +107,11 @@ func (c *HTTPClient) request(req *http.Request) (*http.Response, error) {
var rb []byte
rb, err = ioutil.ReadAll(res.Body)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("reading IUAM response: %w", err)
}
return c.bypassCF(req, rb)
}
- return res, err
+ return res, nil
}
// bypass attempts to re-execute a standard request after first bypassing
@@ -121,7 +121,7 @@ func (c *HTTPClient) bypassCF(req *http.Request, body []byte) (*http.Response, e
r1, _ := regexp.Compile(`setTimeout\(function\(\){\s+(var s,t,o,p,b,r,e,a,k,i,n,g,f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n`)
r1Match := r1.FindSubmatch(body)
if len(r1Match) != 2 {
- return nil, errors.New("Failed to match on IUAM challenge")
+ return nil, fmt.Errorf("failed to match on IUAM challenge")
}
js := string(r1Match[1])
@@ -154,7 +154,7 @@ func (c *HTTPClient) bypassCF(req *http.Request, body []byte) (*http.Response, e
passMatch := pass.FindSubmatch(body)
if !(len(vcMatch) == 2 && len(passMatch) == 2) {
- return nil, errors.New("Failed to extract Cloudflare IUAM challenge")
+ return nil, fmt.Errorf("failed to extract IUAM challenge")
}
// Assemble the CFClearence request
@@ -168,7 +168,7 @@ func (c *HTTPClient) bypassCF(req *http.Request, body []byte) (*http.Response, e
// Execute, populate cookies after 5 seconds and re-execute prior request
time.Sleep(4000 * time.Millisecond)
if _, err := c.Get(u.String(), nil); err != nil {
- return nil, err
+ return nil, fmt.Errorf("getting IUAM request: %w", err)
}
return c.request(req)
}
diff --git a/common/log/log.go b/common/log/log.go
index 843650c..93dcacc 100644
--- a/common/log/log.go
+++ b/common/log/log.go
@@ -17,4 +17,4 @@ func Warn(format string, a ...interface{}) { color.Yellow(prefix+format, a...) }
func Success(format string, a ...interface{}) { color.Green(prefix+format, a...) }
// Error logs errors
-func Error(err error) { color.Red(prefix + err.Error()) }
+func Error(err error) { color.Red(prefix + "Error: " + err.Error()) }
diff --git a/common/video.go b/common/video.go
index 3668888..8fc8fc5 100644
--- a/common/video.go
+++ b/common/video.go
@@ -1,6 +1,7 @@
package common /* import "s32x.com/anirip/common" */
import (
+ "fmt"
"os/exec"
"path/filepath"
)
@@ -25,21 +26,22 @@ func (p *VideoProcessor) DumpHLS(url string) error {
"-c", "copy", "incomplete.episode.mkv")
cmd.Dir = p.tempDir
if err := cmd.Run(); err != nil {
- return err
+ return fmt.Errorf("running download command: %w", err)
}
// Rename the file since it's no longer incomplete
// and return
- return Rename(p.tempDir+pathSep+"incomplete.episode.mkv",
- p.tempDir+pathSep+"episode.mkv", 10)
+ if err := Rename(p.tempDir+pathSep+"incomplete.episode.mkv",p.tempDir+pathSep+"episode.mkv", 10); err != nil {
+ return fmt.Errorf("renaming incomplete episode: %w", err)
+ }
+ return nil
}
// MergeSubtitles merges the VIDEO.mkv and the VIDEO.ass
func (p *VideoProcessor) MergeSubtitles(audioLang, subtitleLang string) error {
Delete(p.tempDir, "unmerged.episode.mkv")
- if err := Rename(p.tempDir+pathSep+"episode.mkv",
- p.tempDir+pathSep+"unmerged.episode.mkv", 10); err != nil {
- return err
+ if err := Rename(p.tempDir+pathSep+"episode.mkv", p.tempDir+pathSep+"unmerged.episode.mkv", 10); err != nil {
+ return fmt.Errorf("renaming unmerged episode: %w", err)
}
cmd := new(exec.Cmd)
if subtitleLang == "" {
@@ -65,7 +67,7 @@ func (p *VideoProcessor) MergeSubtitles(audioLang, subtitleLang string) error {
}
cmd.Dir = p.tempDir
if err := cmd.Run(); err != nil {
- return err
+ return fmt.Errorf("running download command: %w", err)
}
Delete(p.tempDir, "subtitles.episode.ass")
Delete(p.tempDir, "unmerged.episode.mkv")
diff --git a/crunchyroll/episode.go b/crunchyroll/episode.go
index aeda6e6..0f80f47 100644
--- a/crunchyroll/episode.go
+++ b/crunchyroll/episode.go
@@ -2,6 +2,7 @@ package crunchyroll /* import "s32x.com/anirip/crunchyroll" */
import (
"bytes"
+ "fmt"
"io/ioutil"
"net/http"
"net/url"
@@ -53,13 +54,13 @@ func (e *Episode) GetEpisodeInfo(client *common.HTTPClient, quality string) erro
// client.Header.Add("Referer", "http://www.crunchyroll.com/"+strings.Split(e.Path, "/")[1])
resp, err := client.Get(e.URL, nil)
if err != nil {
- return common.NewError("There was an error requesting the episode doc", err)
+ return fmt.Errorf("getting episode page: %w", err)
}
// Creates the document that will be used to scrape for episode metadata
doc, err := goquery.NewDocumentFromResponse(resp)
if err != nil {
- return common.NewError("There was an error reading the episode doc", err)
+ return fmt.Errorf("generating episode document: %w", err)
}
// Request querystring
@@ -87,13 +88,13 @@ func (e *Episode) GetEpisodeInfo(client *common.HTTPClient, quality string) erro
header.Add("X-Requested-With", "ShockwaveFlash/22.0.0.192")
resp, err = client.Post("http://www.crunchyroll.com/xml/?"+queryString, header, reqBody)
if err != nil {
- return common.NewError("There was an error retrieving the manifest", err)
+ return fmt.Errorf("getting manifest page: %w", err)
}
// Gets the xml string from the received xml response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
- return common.NewError("There was an error reading the xml response", err)
+ return fmt.Errorf("reading manifest page: %w", err)
}
// Checks for an unsupported region first
@@ -101,7 +102,7 @@ func (e *Episode) GetEpisodeInfo(client *common.HTTPClient, quality string) erro
xmlString := string(body)
if strings.Contains(xmlString, "") && strings.Contains(xmlString, "
") {
if strings.SplitN(strings.SplitN(xmlString, "", 2)[1], "
", 2)[0] == "4" {
- return common.NewError("This video is not available in your region", err)
+ return fmt.Errorf("video not avaliable in your region: %w", err)
}
}
@@ -111,7 +112,7 @@ func (e *Episode) GetEpisodeInfo(client *common.HTTPClient, quality string) erro
if strings.Contains(xmlString, "") && strings.Contains(xmlString, "") {
eFile = strings.SplitN(strings.SplitN(xmlString, "", 2)[1], "", 2)[0]
} else {
- return common.NewError("No hosts were found for the episode", err)
+ return fmt.Errorf("no episode hosts found: %w", err)
}
e.Title = strings.Replace(strings.Replace(doc.Find("#showmedia_about_name").First().Text(), "“", "", -1), "”", "", -1)
@@ -122,7 +123,10 @@ func (e *Episode) GetEpisodeInfo(client *common.HTTPClient, quality string) erro
// Download downloads entire episode to our temp directory
func (e *Episode) Download(vp *common.VideoProcessor) error {
- return vp.DumpHLS(e.StreamURL)
+ if err := vp.DumpHLS(e.StreamURL); err != nil {
+ return fmt.Errorf("dumping HLS stream: %w", err)
+ }
+ return nil
}
// GetFilename returns the Episodes filename
diff --git a/crunchyroll/session.go b/crunchyroll/session.go
index 92406a2..ce355a2 100644
--- a/crunchyroll/session.go
+++ b/crunchyroll/session.go
@@ -2,6 +2,7 @@ package crunchyroll /* import "s32x.com/anirip/crunchyroll" */
import (
"bytes"
+ "fmt"
"net/http"
"net/url"
"strings"
@@ -16,12 +17,13 @@ func Login(c *common.HTTPClient, user, pass string) error {
// Perform preflight request to retrieve the login page
res, err := c.Get("https://www.crunchyroll.com/login", nil)
if err != nil {
- return err
+ return fmt.Errorf("getting login page: %w", err)
}
+
defer res.Body.Close()
doc, err := goquery.NewDocumentFromResponse(res)
if err != nil {
- return err
+ return fmt.Errorf("generating login document: %w", err)
}
// Scrape the login token
@@ -29,12 +31,12 @@ func Login(c *common.HTTPClient, user, pass string) error {
// Sets the credentials and attempts to generate new cookies
if err := createSession(c, user, pass, token); err != nil {
- return err
+ return fmt.Errorf("creating session: %w", err)
}
// Validates the session created and returns
if err := validateSession(c); err != nil {
- return err
+ return fmt.Errorf("validating session: %w", err)
}
log.Info("Successfully logged in!")
return nil
@@ -56,7 +58,7 @@ func createSession(c *common.HTTPClient, user, pass, token string) error {
head.Add("Referer", "https://www.crunchyroll.com/login")
head.Add("Content-Type", "application/x-www-form-urlencoded")
if _, err := c.Post("https://www.crunchyroll.com/login", head, body); err != nil {
- return common.NewError("Failed to execute authentication request", err)
+ return fmt.Errorf("posting auth request: %w", err)
}
return nil
}
@@ -66,17 +68,17 @@ func createSession(c *common.HTTPClient, user, pass, token string) error {
func validateSession(c *common.HTTPClient) error {
resp, err := c.Get("http://www.crunchyroll.com/", nil)
if err != nil {
- return common.NewError("Failed to execute session validation request", err)
+ return fmt.Errorf("getting validation page: %w", err)
}
doc, err := goquery.NewDocumentFromResponse(resp)
if err != nil {
- return common.NewError("Failed to parse session validation page", err)
+ return fmt.Errorf("generating validation document: %w", err)
}
user := strings.TrimSpace(doc.Find("li.username").First().Text())
if resp.StatusCode == 200 && user != "" {
return nil
}
- return common.NewError("Failed to verify session", nil)
+ return fmt.Errorf("could not verify session")
}
diff --git a/crunchyroll/show.go b/crunchyroll/show.go
index e98ccc8..3713078 100644
--- a/crunchyroll/show.go
+++ b/crunchyroll/show.go
@@ -1,6 +1,7 @@
package crunchyroll /* import "s32x.com/anirip/crunchyroll" */
import (
+ "fmt"
"strconv"
"strings"
@@ -21,13 +22,13 @@ type Show struct {
func (s *Show) Scrape(client *common.HTTPClient, showURL string) error {
res, err := client.Get(showURL, nil)
if err != nil {
- return common.NewError("There was an error retrieving show page", err)
+ return fmt.Errorf("getting show page: %w", err)
}
// Creates the goquery document for scraping
showDoc, err := goquery.NewDocumentFromResponse(res)
if err != nil {
- return common.NewError("There was an error while accessing the show page", err)
+ return fmt.Errorf("generating show document: %w", err)
}
// Sets Title, Path and URL on our show object
diff --git a/crunchyroll/subtitle.go b/crunchyroll/subtitle.go
index 034ce4c..794de93 100644
--- a/crunchyroll/subtitle.go
+++ b/crunchyroll/subtitle.go
@@ -3,10 +3,10 @@ package crunchyroll /* import "s32x.com/anirip/crunchyroll" */
import (
"bytes"
"encoding/json"
- "errors"
+ "fmt"
"io/ioutil"
"os"
- "strings"
+ "regexp"
"s32x.com/anirip/common"
)
@@ -34,45 +34,41 @@ type configStruct struct {
// DownloadSubtitles entirely downloads subtitles to our temp directory
func (episode *Episode) DownloadSubtitles(client *common.HTTPClient, language string, tempDir string) (string, error) {
- // Remove stale temp file to avoid conflicts in func
- os.Remove(tempDir + string(os.PathSeparator) + "subtitles.episode.ass")
-
- // Subtitle language (The only two I know fo are enUS and jaJP)
- subLang := "enUS"
+ subOutput := tempDir + string(os.PathSeparator) + "subtitles.episode.ass"
+ // Remove stale temp file to avoid conflicts in writing
+ os.Remove(subOutput)
// Fetch html page for the episode
- var body []byte
- if res, err := client.Get(episode.URL, nil); err == nil {
- defer res.Body.Close()
- if body, err = ioutil.ReadAll(res.Body); err != nil {
- return "", err
- }
- } else {
- return "", err
+ res, err := client.Get(episode.URL, nil)
+ if err != nil {
+ return "", fmt.Errorf("getting episode page: %w", err)
}
- // Find the vilos config table and split the area after it.
- // Then, trim away the "vilos.config.media = " to produce a json table in string form
- stringBody := string(body)
- mediaConfigIndex := strings.Index(stringBody, "vilos.config.media")
- newResult := strings.SplitAfter(stringBody[mediaConfigIndex:], "}]}")
- jsonReady := strings.TrimPrefix(newResult[0], "vilos.config.media = ")
+ defer res.Body.Close()
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return "", fmt.Errorf("reading episode response: %w", err)
+ }
- // Parse json string to configStruct
- var subStruct configStruct
- if err := json.Unmarshal([]byte(jsonReady), &subStruct); err != nil {
- return "", err
+ jsonResult := regexp.MustCompile("vilos.config.media = (.*);\n").FindSubmatch(body)
+
+ if jsonResult == nil {
+ return "", fmt.Errorf("finding vilos config")
}
+ var subStruct configStruct
+ if err := json.Unmarshal(jsonResult[1], &subStruct); err != nil {
+ return "", fmt.Errorf("unmarshaling json: %w", err)
+ }
// Verify that there are actually subtitles
if len(subStruct.Subtitles) == 0 {
- return "", errors.New("No subtitle files found(?)")
+ return "", fmt.Errorf("no subtitles found (?)")
}
// Determine the best subtitle (I know it currently defaults to enUS but it can be fixed later)
chosenSubtitle := subStruct.Subtitles[0]
for _, sub := range subStruct.Subtitles {
- if sub.Language == subLang {
+ if sub.Language == language {
chosenSubtitle = sub
break
}
@@ -81,7 +77,7 @@ func (episode *Episode) DownloadSubtitles(client *common.HTTPClient, language st
// Fetch the download page for the chosen subtitle (the page that's returned is the decrypted subtitles in ass format)
subResp, err := client.Get(chosenSubtitle.DownloadURL, nil)
if err != nil {
- return "", err
+ return "", fmt.Errorf("getting download url: %w", err)
}
// Read the subtitles and output them to the subtitles.episode.ass file in the temp directory
@@ -89,8 +85,8 @@ func (episode *Episode) DownloadSubtitles(client *common.HTTPClient, language st
buf := new(bytes.Buffer)
buf.ReadFrom(subResp.Body)
- if err := ioutil.WriteFile(tempDir + string(os.PathSeparator) + "subtitles.episode.ass", buf.Bytes(), 0777); err != nil {
- return "", err
+ if err := ioutil.WriteFile(subOutput, buf.Bytes(), os.ModePerm); err != nil {
+ return "", fmt.Errorf("writing file: %w", err)
}
- return subLang, nil
+ return language, nil
}
diff --git a/main.go b/main.go
index e826e36..cbc340d 100644
--- a/main.go
+++ b/main.go
@@ -42,8 +42,8 @@ func main() {
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "language, l",
- Value: "eng",
- Usage: "language code for the subtitles (not all are supported) ex: eng, esp",
+ Value: "en-US",
+ Usage: "language code for the subtitles (not all are supported) ex: en-US, ja-JP",
},
cli.StringFlag{
Name: "quality, q",
@@ -75,15 +75,11 @@ func download(showURL, user, pass, quality, subLang string) {
_, err := os.Stat(tempDir)
if err != nil {
log.Info("Generating new temporary directory")
- os.Mkdir(tempDir, 0777)
+ os.Mkdir(tempDir, os.ModePerm)
}
// Generate the HTTP client that will be used through whole lifecycle
- client, err := common.NewHTTPClient()
- if err != nil {
- log.Error(err)
- return
- }
+ client := common.NewHTTPClient()
// Logs the user in and stores their session data in the clients jar
log.Info("Logging into Crunchyroll...")
@@ -105,11 +101,11 @@ func download(showURL, user, pass, quality, subLang string) {
vp := common.NewVideoProcessor(tempDir)
// Creates a new show directory which will store all seasons
- os.Mkdir(show.GetTitle(), 0777)
+ os.Mkdir(show.GetTitle(), os.ModePerm)
for _, season := range show.GetSeasons() {
// Creates a new season directory that will store all episodes
- os.Mkdir(show.GetTitle()+string(os.PathSeparator)+seasonMap[season.GetNumber()], 0777)
+ os.Mkdir(show.GetTitle()+string(os.PathSeparator)+seasonMap[season.GetNumber()], os.ModePerm)
for _, episode := range season.GetEpisodes() {
// Retrieves more fine grained episode metadata