├── pkg ├── constants │ └── filename.go └── scrapers │ ├── get_iframe.go │ ├── most_watched.go │ ├── search.go │ ├── select_video.go │ └── episode.go ├── internal ├── entity │ └── anime.go ├── utils │ ├── get_user_input.go │ ├── option_is_valid.go │ ├── get_home_dir.go │ ├── clean_cache.go │ ├── play_video.go │ ├── read_file.go │ ├── greeting.go │ ├── clear.go │ └── create_file.go └── presence │ └── presence.go ├── .github └── workflows │ ├── commit.yml │ └── ci.yml ├── CONTRIBUTING.md ├── go.mod ├── LICENSE ├── disclaimer.md ├── README.md ├── config └── colly.go ├── cmd └── gophimation │ └── main.go └── go.sum /pkg/constants/filename.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | FOLDER_NAME = "gophimation" 5 | USER_SETTINGS_FILE = "/settings.json" 6 | URL_BASE = "https://betteranime.net/" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/entity/anime.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Anime struct { 4 | Name string `json:"name"` 5 | URL string `json:"url"` 6 | } 7 | 8 | type AnimeResponse struct { 9 | Anime Anime `json:"anime"` 10 | Episodes []Anime `json:"episodes"` 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commits 2 | on: [pull_request] 3 | 4 | jobs: 5 | lint-commits: 6 | name: Lint Commits 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | - uses: wagoid/commitlint-github-action@v4 -------------------------------------------------------------------------------- /internal/utils/get_user_input.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | // GetUserInput retrieves user input by displaying a message in the console. 6 | func GetUserInput(message string) int { 7 | var selectedOption int 8 | 9 | fmt.Println(message) 10 | fmt.Scanln(&selectedOption) 11 | 12 | return selectedOption 13 | } 14 | -------------------------------------------------------------------------------- /internal/utils/option_is_valid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/Caixetadev/gophimation/internal/entity" 7 | ) 8 | 9 | // OptionIsValid checks if the option selected by the user is valid for a given list of anime. 10 | func OptionIsValid(anime []entity.Anime, selectedOption int) { 11 | // Checks whether the selected option is invalid. 12 | if selectedOption <= 0 || selectedOption > len(anime) { 13 | log.Fatalf("Digite um número válido") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/utils/get_home_dir.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | 7 | "github.com/Caixetadev/gophimation/pkg/constants" 8 | ) 9 | 10 | // GetCacheDir retrieves the cache directory. It expects a parameter 'folderName' that specifies the desired folder name. 11 | func GetCacheDir(folderName string) string { 12 | usr, err := user.Current() 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | cacheDir := filepath.Join(usr.HomeDir, ".cache", constants.FOLDER_NAME, folderName) 18 | 19 | return cacheDir 20 | } 21 | -------------------------------------------------------------------------------- /internal/utils/clean_cache.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "time" 6 | ) 7 | 8 | // CleanCache clears the cache, expecting a parameter named cacheDuration of type time.Duration 9 | // that determines the maximum duration a cache file is allowed to exist before being deleted. 10 | func CleanCache(cacheDuration time.Duration) { 11 | cacheDir := GetCacheDir("anime") 12 | folders, err := os.ReadDir(cacheDir) 13 | 14 | if err == nil { 15 | for _, folder := range folders { 16 | info, _ := folder.Info() 17 | if time.Since(info.ModTime()) > cacheDuration { 18 | go os.RemoveAll(cacheDir + "/" + folder.Name()) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/utils/play_video.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | ) 10 | 11 | // PlayVideo runs a command that opens a video player to play a video from a specified URL 12 | func PlayVideo(videoUrl, nameEpisode string) { 13 | executablesByOS := map[string]string{ 14 | "windows": "mpv.exe", 15 | "linux": "mpv", 16 | } 17 | 18 | cmd := exec.Command( 19 | executablesByOS[runtime.GOOS], 20 | videoUrl, 21 | "--fs", 22 | "--force-window=immediate", 23 | "--no-terminal", 24 | fmt.Sprintf("--force-media-title=%v", nameEpisode), 25 | "--cache=yes", 26 | ) 27 | cmd.Stdout = os.Stdout 28 | 29 | if err := cmd.Run(); err != nil { 30 | log.Fatalln(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Como contribuir? 2 | Este é um projeto totalmente livre que aceita contribuições via pull requests no GitHub. Este documento tem a responsabilidade de alinhar as contribuições de acordo com os padrões estabelecidos no mesmo. Em caso de dúvidas, [abra uma issue](https://github.com/iuricode/recursos-gratuitos/issues/new). 3 | 4 | ## Primeiros passos 5 | 1. Fork este repositório. 6 | 2. Envie seus commits em português. 7 | 3. Solicite a pull request. 8 | 4. Insira um pequeno resumo dos links adicionados. 9 | 10 | ## Recomendação (Opcional) 11 | Para uma melhor semântica nos commits, recomendamos nosso repositório sobre [COMMITS SEMÂNTICOS](https://github.com/iuricode/padroes-de-commits). Assim ficará mais fácil para avaliar seu pull request. 12 | -------------------------------------------------------------------------------- /internal/utils/read_file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | // ReadFile reads the specified file and unmarshals its JSON content 11 | // into a UserConfig struct. It then calls the Greeting function to 12 | // display a welcome message for the user. 13 | func ReadFile(rootDir string, fileName string) { 14 | fmt.Println(rootDir + fileName) 15 | data, err := os.ReadFile(rootDir + fileName) 16 | 17 | var user UserConfig 18 | 19 | if errUnmarshal := json.Unmarshal(data, &user); errUnmarshal != nil { 20 | log.Fatalln(errUnmarshal) 21 | } 22 | 23 | Clear() 24 | 25 | Greeting(user.Name) 26 | 27 | fmt.Print("\n") 28 | 29 | if err != nil { 30 | log.Panicf("failed reading data from file: %s", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/scrapers/get_iframe.go: -------------------------------------------------------------------------------- 1 | package scrapers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/Caixetadev/gophimation/config" 8 | "github.com/gocolly/colly" 9 | ) 10 | 11 | // GetIframe performs web scraping on an anime page to extract the iframe tag. 12 | func GetIframe(URL string) (string, string) { 13 | c := config.Colly() 14 | 15 | var iframe string 16 | var nameAnimeAndEpisode string 17 | 18 | c.OnHTML(".anime-title", func(h *colly.HTMLElement) { 19 | nameAnimeAndEpisode = fmt.Sprintf("%s - %s", h.ChildText("h2 a"), h.ChildText("h3")) 20 | }) 21 | 22 | c.OnHTML("iframe", func(h *colly.HTMLElement) { 23 | iframe = h.Attr("src") 24 | }) 25 | 26 | if err := c.Visit(URL); err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | return iframe, nameAnimeAndEpisode 31 | } 32 | -------------------------------------------------------------------------------- /internal/utils/greeting.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const MESSAGE = "! Seja bem-vindo de volta ao Gophimation" 9 | 10 | // Greeting is a function that takes a user name and outputs a greeting message based on the time of day. 11 | // It gets the current hour using the time package and uses a switch statement to determine the appropriate message. 12 | // The function then prints the greeting message to the console. 13 | func Greeting(userName string) { 14 | var message string 15 | 16 | switch hour := time.Now().Hour(); { 17 | case hour < 6: 18 | message = "Boa madrugada, " + userName + MESSAGE 19 | case hour < 12: 20 | message = "Bom dia, " + userName + MESSAGE 21 | case hour < 17: 22 | message = "Boa tarde, " + userName + MESSAGE 23 | default: 24 | message = "Boa noite, " + userName + MESSAGE 25 | } 26 | 27 | fmt.Println(message) 28 | } 29 | -------------------------------------------------------------------------------- /internal/presence/presence.go: -------------------------------------------------------------------------------- 1 | package presence 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hugolgst/rich-go/client" 9 | ) 10 | 11 | const CLIENT_ID = "1075841986923352079" 12 | 13 | // Presence sets the presence status of a Discord client with a custom activity 14 | func Presence(animeImage, animeName, state, smallImage string) { 15 | err := client.Login(CLIENT_ID) 16 | if err != nil { 17 | err := errors.New("Aviso: Não foi possivel ativar a conexao com o discord\n") 18 | fmt.Println(err) 19 | } 20 | 21 | now := time.Now() 22 | 23 | err = client.SetActivity(client.Activity{ 24 | State: state, 25 | Details: animeName, 26 | LargeImage: animeImage, 27 | LargeText: animeName, 28 | SmallImage: smallImage, 29 | SmallText: "Gophimation", 30 | Timestamps: &client.Timestamps{ 31 | Start: &now, 32 | }, 33 | }) 34 | 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Caixetadev/gophimation 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gocolly/colly v1.2.0 7 | github.com/hugolgst/rich-go v0.0.0-20210925091458-d59fb695d9c0 8 | ) 9 | 10 | require ( 11 | github.com/PuerkitoBio/goquery v1.8.1 // indirect 12 | github.com/andybalholm/cascadia v1.3.1 // indirect 13 | github.com/antchfx/htmlquery v1.3.0 // indirect 14 | github.com/antchfx/xmlquery v1.3.15 // indirect 15 | github.com/antchfx/xpath v1.2.3 // indirect 16 | github.com/gobwas/glob v0.2.3 // indirect 17 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 18 | github.com/golang/protobuf v1.3.1 // indirect 19 | github.com/kennygrant/sanitize v1.2.4 // indirect 20 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 21 | github.com/temoto/robotstxt v1.1.2 // indirect 22 | golang.org/x/net v0.17.0 // indirect 23 | golang.org/x/text v0.13.0 // indirect 24 | google.golang.org/appengine v1.6.7 // indirect 25 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /internal/utils/clear.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "runtime" 8 | ) 9 | 10 | var clear map[string]func() // create a map for storing clear funcs 11 | 12 | func init() { 13 | clear = make(map[string]func()) // Initialize it 14 | clear["linux"] = func() { 15 | cmd := exec.Command("clear") // Linux example, its tested 16 | cmd.Stdout = os.Stdout 17 | if err := cmd.Run(); err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | clear["windows"] = func() { 22 | cmd := exec.Command("cmd", "/c", "cls") // Windows example, its tested 23 | cmd.Stdout = os.Stdout 24 | if err := cmd.Run(); err != nil { 25 | log.Fatal(err) 26 | } 27 | } 28 | } 29 | 30 | // Clear runs the command to clear the terminal 31 | func Clear() { 32 | value, ok := clear[runtime.GOOS] // runtime.GOOS -> linux, windows, darwin etc. 33 | if ok { // if we defined a clear func for that platform: 34 | value() // we execute it 35 | } else { // unsupported platform 36 | panic("Your platform is unsupported! I can't clear terminal screen :(") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rafael Caixeta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/utils/create_file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | // UserConfig stores user configuration information, including their name. 11 | type UserConfig struct { 12 | Name string `json:"name"` 13 | } 14 | 15 | // CreateFile prompts the user to enter their name, creates a new file 16 | // with the specified name, and writes the user's name to the file as 17 | // JSON data in a UserConfig struct format. 18 | func CreateFile(rootDir string, filename string) { 19 | var name string 20 | 21 | fmt.Println("Qual o seu nome?") 22 | 23 | fmt.Scanln(&name) 24 | 25 | err := os.MkdirAll(rootDir, 0777) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | file, err := os.Create(rootDir + filename) 31 | 32 | data := UserConfig{ 33 | Name: name, 34 | } 35 | 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | defer file.Close() 41 | 42 | user, err := json.Marshal(data) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | err = os.WriteFile(rootDir+filename, user, 0644) 48 | 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | Clear() 58 | } 59 | -------------------------------------------------------------------------------- /pkg/scrapers/most_watched.go: -------------------------------------------------------------------------------- 1 | package scrapers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/Caixetadev/gophimation/config" 9 | "github.com/Caixetadev/gophimation/internal/entity" 10 | "github.com/Caixetadev/gophimation/internal/utils" 11 | "github.com/Caixetadev/gophimation/pkg/constants" 12 | "github.com/gocolly/colly" 13 | ) 14 | 15 | const userPromptMostWatched = "\nColoque um numero para assistir" 16 | 17 | // MostWatched prints the most viewed anime of the week and returns its url 18 | func MostWatched() string { 19 | var selectedOption int 20 | 21 | c := config.Colly() 22 | 23 | var anime []entity.Anime 24 | 25 | setCollyCallbacksMostWatched(c, &anime) 26 | 27 | if err := c.Visit(constants.URL_BASE); err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | selectedOption = utils.GetUserInput(userPromptMostWatched) 32 | 33 | utils.OptionIsValid(anime, selectedOption) 34 | 35 | utils.Clear() 36 | 37 | return anime[selectedOption-1].URL 38 | } 39 | 40 | func setCollyCallbacksMostWatched(c *colly.Collector, anime *[]entity.Anime) { 41 | c.OnHTML(".highlights .highlight-card .highlight-body", func(h *colly.HTMLElement) { 42 | fmt.Printf("[%02d] - %v\n", h.Index+1, h.ChildText(".highlight-title h3")) 43 | 44 | *anime = append(*anime, entity.Anime{URL: strings.TrimPrefix(h.ChildAttr("a", "href"), constants.URL_BASE)}) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /disclaimer.md: -------------------------------------------------------------------------------- 1 | # Disclaimer 2 | 3 | **Project**: gophimation 4 | 5 | The core aim of this project is to correlate automation and efficiency to extract information available on the internet. All content accessible through the project is hosted by external non-affiliated sources. 6 | 7 | Any content served through this project is publicly accessible. If your site is listed in this project, the code is virtually public. Take necessary measures to counter the exploits used to extract content on your site. 8 | 9 | Think of this project as a regular browser, but a bit more straightforward and specific. While a typical browser makes hundreds of requests to get everything from a site, this project makes requests associated only with obtaining the content provided by the sites. 10 | 11 | This project should be used at the user's own risk, in accordance with the laws and regulations of their government. 12 | 13 | The project has no control over the content it is serving, and the use of copyrighted content from providers will not be the responsibility of the developer. It is at the user's own risk. 14 | 15 | # DMCA and Copyright Infringements 16 | A browser is a tool, and the maliciousness of the tool is directly based on the user. This project uses client-side content access mechanisms. Therefore, copyright infringements or DMCA claims related to this project should be forwarded to the associated site by the associated notifier of such claims. As of the writing, this applies to betteranime. 17 | -------------------------------------------------------------------------------- /pkg/scrapers/search.go: -------------------------------------------------------------------------------- 1 | package scrapers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/Caixetadev/gophimation/config" 10 | "github.com/Caixetadev/gophimation/internal/entity" 11 | "github.com/Caixetadev/gophimation/internal/utils" 12 | "github.com/Caixetadev/gophimation/pkg/constants" 13 | "github.com/gocolly/colly" 14 | ) 15 | 16 | // Search does the search for the anime 17 | func Search() string { 18 | c := config.Colly() 19 | 20 | searchTerm := strings.Join(os.Args[1:], "+") 21 | 22 | URL := fmt.Sprintf("%spesquisa?titulo=%s&searchTerm=%s", constants.URL_BASE, searchTerm, searchTerm) 23 | 24 | var selectedOption int 25 | 26 | var anime []entity.Anime 27 | 28 | setCollyCallbacksSearch(c, &anime) 29 | 30 | if err := c.Visit(URL); err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | if len(anime) == 0 { 35 | log.Fatal("Não foi possivel achar o anime") 36 | } 37 | 38 | selectedOption = utils.GetUserInput("\nColoque um numero para assistir") 39 | 40 | utils.OptionIsValid(anime, selectedOption) 41 | 42 | utils.Clear() 43 | 44 | return anime[selectedOption-1].URL 45 | } 46 | 47 | func setCollyCallbacksSearch(c *colly.Collector, anime *[]entity.Anime) { 48 | c.OnHTML(".list-animes article", func(h *colly.HTMLElement) { 49 | fmt.Printf("[%02d] - %s\n", h.Index+1, h.ChildAttr("a", "title")) 50 | 51 | *anime = append(*anime, entity.Anime{URL: strings.TrimPrefix(h.ChildAttr("a", "href"), constants.URL_BASE)}) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/scrapers/select_video.go: -------------------------------------------------------------------------------- 1 | package scrapers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/Caixetadev/gophimation/config" 10 | "github.com/Caixetadev/gophimation/pkg/constants" 11 | "github.com/gocolly/colly" 12 | ) 13 | 14 | type PlayerInfo struct { 15 | Name string `json:"name"` 16 | Url string `json:"url"` 17 | } 18 | 19 | // SelectVideo does the search for the url of the video 20 | func SelectVideo(ep string) *PlayerInfo { 21 | var urlPlayer []PlayerInfo 22 | 23 | c := config.Colly() 24 | 25 | iframeURL, nameAnimeAndEpisode := GetIframe(constants.URL_BASE + ep) 26 | 27 | setCollyCallbacksPlayer(c, &urlPlayer, nameAnimeAndEpisode) 28 | 29 | if err := c.Visit(iframeURL); err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | return &PlayerInfo{Name: urlPlayer[0].Name, Url: urlPlayer[0].Url} 34 | } 35 | 36 | func setCollyCallbacksPlayer(c *colly.Collector, player *[]PlayerInfo, nameAnimeAndEpisode string) { 37 | c.OnRequest(func(r *colly.Request) { 38 | r.Headers.Set("Referer", constants.URL_BASE) 39 | }) 40 | 41 | c.OnHTML("script:nth-of-type(4)", func(h *colly.HTMLElement) { 42 | re := regexp.MustCompile(`"file":"([^"]+)"`) 43 | match := re.FindStringSubmatch(h.Text) 44 | 45 | if len(match) > 1 { 46 | *player = append( 47 | *player, 48 | PlayerInfo{Name: nameAnimeAndEpisode, Url: strings.ReplaceAll(match[1], "\\", "")}, 49 | ) 50 | } 51 | }) 52 | 53 | c.OnError(func(r *colly.Response, err error) { 54 | fmt.Println(err) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | - main 9 | pull_request: 10 | permissions: 11 | contents: read 12 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 13 | # pull-requests: read 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/setup-go@v3 20 | with: 21 | go-version: "1.17" 22 | - uses: actions/checkout@v3 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v3 25 | with: 26 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 27 | version: v1.29 28 | 29 | # Optional: working directory, useful for monorepos 30 | # working-directory: somedir 31 | 32 | # Optional: golangci-lint command line arguments. 33 | # args: --issues-exit-code=0 34 | 35 | # Optional: show only new issues if it's a pull request. The default value is `false`. 36 | # only-new-issues: true 37 | 38 | # Optional: if set to true then the all caching functionality will be complete disabled, 39 | # takes precedence over all other caching options. 40 | # skip-cache: true 41 | 42 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 43 | # skip-pkg-cache: true 44 | 45 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 46 | # skip-build-cache: true 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gophimation 2 | 3 | 4 | 5 | ![GitHub repo size](https://img.shields.io/github/repo-size/Caixetadev/gophimation?style=for-the-badge) 6 | ![GitHub language count](https://img.shields.io/github/languages/count/Caixetadev/gophimation?style=for-the-badge) 7 | ![GitHub forks](https://img.shields.io/github/forks/Caixetadev/gophimation?style=for-the-badge) 8 | ![Bitbucket open issues](https://img.shields.io/github/issues/Caixetadev/gophimation?style=for-the-badge) 9 | ![Bitbucket open pull requests](https://img.shields.io/bitbucket/pr-raw/Caixetadev/gophimation?style=for-the-badge) 10 | 11 | https://user-images.githubusercontent.com/87894998/224532763-f82b90f2-06c5-4b44-8c96-c3eb2eab7244.mp4 12 | 13 | > Gophimation is a command-line tool built in Golang that allows you to watch your favorite anime directly from the terminal, without the distraction of ads and other unwanted interruptions. 14 | 15 | ### Adjustments and Improvements 16 | 17 | The project is still in development, and upcoming updates will focus on the following tasks: 18 | - [ ] Make mpv work on Windows 19 | - [ ] Add more player options 20 | 21 | ## 💻 Prerequisites 22 | 23 | Before you begin, make sure you have met the following requirements: 24 | * mpv 25 | 26 | ## 🚀 Installing Gophimation 27 | 28 | To install Gophimation, follow these steps: 29 | 30 | Install gophimation [HERE](https://github.com/Caixetadev/gophimation/releases) 31 | 32 | Linux: 33 | ``` 34 | cd Downloads 35 | sudo chmod +x gophimation 36 | sudo mv gophimation /usr/bin 37 | ``` 38 | 39 | ## ☕ Using Gophimation 40 | 41 | To search for an anime: 42 | ``` 43 | gophimation animename 44 | ``` 45 | 46 | to search for a random anime: 47 | ``` 48 | gophimation random 49 | ``` 50 | 51 | to search for the most watched anime of the week: 52 | ``` 53 | gophimation 54 | ``` 55 | 56 | ## 😄 Be one of the contributors 57 | 58 | Want to be part of this project? Click [HERE](CONTRIBUTING.md) and read how to contribute. 59 | 60 | ## 📝 License 61 | 62 | This project is under license. See the [LICENÇA](LICENSE) file for more details 63 | 64 | [⬆ Go to top](#nome-do-projeto)
65 | -------------------------------------------------------------------------------- /config/colly.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "net/url" 9 | "os/user" 10 | 11 | "github.com/Caixetadev/gophimation/pkg/constants" 12 | "github.com/gocolly/colly" 13 | ) 14 | 15 | func CollyPastebin(userHomeDir string) string { 16 | c := colly.NewCollector( 17 | colly.CacheDir(fmt.Sprintf("%s/.cache/gophimation/anime", userHomeDir)), 18 | colly.AllowedDomains("pastebin.com"), 19 | colly.UserAgent( 20 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", 21 | ), 22 | ) 23 | 24 | var cookie string 25 | c.OnHTML(".content .post-view .de1", func(e *colly.HTMLElement) { 26 | cookie = e.Text 27 | }) 28 | 29 | if err := c.Visit("https://pastebin.com/9iNGXsDt"); err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | return cookie 34 | } 35 | 36 | func Colly() *colly.Collector { 37 | user, err := user.Current() 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | c := colly.NewCollector( 43 | colly.CacheDir(fmt.Sprintf("%s/.cache/gophimation/anime", user.HomeDir)), 44 | colly.AllowedDomains(constants.URL_BASE), 45 | colly.UserAgent( 46 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", 47 | ), 48 | ) 49 | 50 | cookieJar, err := cookiejar.New(nil) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | c.SetCookieJar(cookieJar) 56 | 57 | url, err := url.Parse(constants.URL_BASE) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | cookiePastebin := CollyPastebin(user.HomeDir) 63 | 64 | cookie := &http.Cookie{ 65 | Name: "betteranime_session", 66 | Value: cookiePastebin, 67 | Domain: "betteranime.net", 68 | Path: "/", 69 | } 70 | 71 | cookie2 := &http.Cookie{ 72 | Name: "BetterQuality", 73 | Value: "eyJpdiI6ImVqbEcwT0dQZWNNNjFuK0NwZUVjMnc9PSIsInZhbHVlIjoib3RQZFF0TEZGcTZwM2pjRFJ1aU8yOWRLOW5ORFh4M1pkSzdEblZ0T2IrMmxTSGgwaHNCUHVrQTZ1MDBEbkRkZy93aHIyak9xVWh1Wmc5K05BRUNqYUMrZzIvNzY4elpwNDRUMWplN2ZOMXNnd3k0QWgwb3p3SFZYYWF5S0g3RjAiLCJtYWMiOiJiYWViNTA2NTA4NTY1NTRiZmY0Yjg1Y2U2MzI4ZTdlZGYxNDUzNmU3NGMwZGRmMTM5MTQ4OTJmMGNjODQ2MjIwIiwidGFnIjoiIn0%3D", 74 | Domain: "betteranime.net", 75 | Path: "/", 76 | } 77 | 78 | cookieJar.SetCookies(url, []*http.Cookie{cookie, cookie2}) 79 | 80 | return c 81 | } 82 | -------------------------------------------------------------------------------- /pkg/scrapers/episode.go: -------------------------------------------------------------------------------- 1 | package scrapers 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/Caixetadev/gophimation/config" 9 | "github.com/Caixetadev/gophimation/internal/entity" 10 | "github.com/Caixetadev/gophimation/internal/presence" 11 | "github.com/Caixetadev/gophimation/internal/utils" 12 | "github.com/Caixetadev/gophimation/pkg/constants" 13 | "github.com/gocolly/colly" 14 | ) 15 | 16 | const userPromptEpisode = "\nColoque um numero para assistir" 17 | 18 | // SelectEpisode performs web scraping on the episodes page, allowing the user to choose the desired episode. 19 | func SelectEpisode(URL string) (*string, int, []entity.Anime) { 20 | c := config.Colly() 21 | 22 | var selectedOption int 23 | var episodes []entity.Anime 24 | var nameAnime string 25 | var image string 26 | 27 | setCollyCallbacksEpisodes(c, &nameAnime, &episodes, &image, URL) 28 | 29 | if err := c.Visit(constants.URL_BASE + URL); err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | selectedOption = utils.GetUserInput(userPromptEpisode) 34 | 35 | utils.OptionIsValid(episodes, selectedOption) 36 | 37 | utils.Clear() 38 | 39 | fmt.Println("Carregando...") 40 | 41 | updatePresence(image, nameAnime, selectedOption) 42 | 43 | if selectedOption == len(episodes) { 44 | return nil, selectedOption - 1, episodes 45 | } 46 | 47 | return &episodes[(selectedOption+1)-1].URL, selectedOption - 1, episodes 48 | } 49 | 50 | func setCollyCallbacksEpisodes( 51 | c *colly.Collector, 52 | nameAnime *string, 53 | episodes *[]entity.Anime, 54 | image *string, 55 | URL string, 56 | ) { 57 | c.OnHTML(".infos_left .anime-info", func(h *colly.HTMLElement) { 58 | *nameAnime = h.ChildText("h2") 59 | 60 | if URL == "random" { 61 | fmt.Printf("O anime random é: %s\n\n", *nameAnime) 62 | } 63 | }) 64 | 65 | c.OnHTML("#episodesList .list-group-item-action", func(h *colly.HTMLElement) { 66 | fmt.Printf("[%02d] - %v\n", h.Index+1, h.ChildText("a h3")) 67 | 68 | *episodes = append( 69 | *episodes, 70 | entity.Anime{ 71 | Name: h.ChildText("a h3"), 72 | URL: strings.TrimPrefix(h.ChildAttr("a", "href"), constants.URL_BASE), 73 | }, 74 | ) 75 | }) 76 | 77 | c.OnHTML("main.container", func(h *colly.HTMLElement) { 78 | *image = h.ChildAttr(".infos-img img", "src") 79 | }) 80 | } 81 | 82 | func updatePresence(image string, nameAnime string, selectedOption int) { 83 | go presence.Presence( 84 | "https:"+image, 85 | nameAnime, 86 | fmt.Sprintf("Episódio %02d", selectedOption), 87 | "https://www.stickersdevs.com.br/wp-content/uploads/2022/01/gopher-adesivo-sticker.png", 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /cmd/gophimation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/Caixetadev/gophimation/internal/entity" 10 | "github.com/Caixetadev/gophimation/internal/presence" 11 | "github.com/Caixetadev/gophimation/internal/utils" 12 | "github.com/Caixetadev/gophimation/pkg/constants" 13 | "github.com/Caixetadev/gophimation/pkg/scrapers" 14 | ) 15 | 16 | func init() { 17 | go utils.CleanCache(time.Hour * 3) 18 | go presence.Presence( 19 | "https://www.stickersdevs.com.br/wp-content/uploads/2022/01/gopher-adesivo-sticker.png", 20 | "Explorando Animes", 21 | "Encontre seu próximo anime favorito <3", 22 | "", 23 | ) 24 | 25 | rootDir := utils.GetCacheDir("user") 26 | 27 | _, error := os.Stat(rootDir) 28 | 29 | if os.IsNotExist(error) { 30 | utils.CreateFile(rootDir, constants.USER_SETTINGS_FILE) 31 | } else { 32 | utils.ReadFile(rootDir, constants.USER_SETTINGS_FILE) 33 | } 34 | } 35 | 36 | func getSelectedVideo(episodes []entity.Anime, index int) { 37 | videoSelected := scrapers.SelectVideo(episodes[index].URL) 38 | utils.PlayVideo(videoSelected.Url, videoSelected.Name) 39 | } 40 | 41 | func fetchAnimeData(animeSelected string) (int, []entity.Anime) { 42 | nextEpisode, currentEpIndex, episodes := scrapers.SelectEpisode(animeSelected) 43 | getSelectedVideo(episodes, currentEpIndex) 44 | if nextEpisode != nil { 45 | go scrapers.SelectVideo(*nextEpisode) 46 | } 47 | 48 | return currentEpIndex, episodes 49 | } 50 | 51 | func app() { 52 | var watchedEpisodes []entity.Anime 53 | var currentEpisodeIndex int 54 | var searchResult string 55 | 56 | switch { 57 | case len(os.Args) > 1 && os.Args[1] == "--delete-cache": 58 | go utils.CleanCache(time.Second * 1) 59 | animeMostWatched := scrapers.MostWatched() 60 | currentEpisodeIndex, watchedEpisodes = fetchAnimeData(animeMostWatched) 61 | searchResult = animeMostWatched 62 | case len(os.Args) > 1: 63 | animeSearch := scrapers.Search() 64 | currentEpisodeIndex, watchedEpisodes = fetchAnimeData(animeSearch) 65 | searchResult = animeSearch 66 | default: 67 | animeMostWatched := scrapers.MostWatched() 68 | currentEpisodeIndex, watchedEpisodes = fetchAnimeData(animeMostWatched) 69 | searchResult = animeMostWatched 70 | } 71 | 72 | utils.Clear() 73 | 74 | for { 75 | utils.Clear() 76 | 77 | var optionSelected string 78 | 79 | options := []string{ 80 | "[n] - Play Next Episode\n", 81 | "[b] - Go Back to Previous Episode\n", 82 | "[r] - Replay Current Episode\n", 83 | "[s] - Return to Episode List\n", 84 | "[q] - Quit\n", 85 | } 86 | 87 | fmt.Println(strings.Join(options, "")) 88 | 89 | fmt.Scanln(&optionSelected) 90 | 91 | switch strings.ToLower(optionSelected) { 92 | case "n": 93 | currentEpisodeIndex++ 94 | 95 | getSelectedVideo(watchedEpisodes, currentEpisodeIndex) 96 | 97 | if currentEpisodeIndex+1 < len(watchedEpisodes) { 98 | go scrapers.SelectVideo(watchedEpisodes[currentEpisodeIndex+1].URL) 99 | } 100 | case "r": 101 | getSelectedVideo(watchedEpisodes, currentEpisodeIndex) 102 | case "s": 103 | fmt.Println(searchResult) 104 | nextEpisode, currentEpIndex, episodes := scrapers.SelectEpisode(searchResult) 105 | fmt.Println(episodes) 106 | getSelectedVideo(episodes, currentEpIndex) 107 | if nextEpisode != nil { 108 | go scrapers.SelectVideo(*nextEpisode) 109 | } 110 | 111 | currentEpisodeIndex = currentEpIndex 112 | case "b": 113 | currentEpisodeIndex-- 114 | 115 | getSelectedVideo(watchedEpisodes, currentEpisodeIndex) 116 | case "q": 117 | return 118 | default: 119 | utils.Clear() 120 | fmt.Print("Invalid option. Please try again.\n\n") 121 | } 122 | } 123 | } 124 | 125 | func main() { 126 | app() 127 | } 128 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= 2 | github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= 3 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 4 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 5 | github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= 6 | github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= 7 | github.com/antchfx/xmlquery v1.3.15 h1:aJConNMi1sMha5G8YJoAIF5P+H+qG1L73bSItWHo8Tw= 8 | github.com/antchfx/xmlquery v1.3.15/go.mod h1:zMDv5tIGjOxY/JCNNinnle7V/EwthZ5IT8eeCGJKRWA= 9 | github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes= 10 | github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= 11 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 14 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 15 | github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= 16 | github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= 17 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 19 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 20 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 21 | github.com/hugolgst/rich-go v0.0.0-20210925091458-d59fb695d9c0 h1:IkZfZBWufGFLvci1vXLiUu8PWVtG6wlz920CMtHobMo= 22 | github.com/hugolgst/rich-go v0.0.0-20210925091458-d59fb695d9c0/go.mod h1:nGaW7CGfNZnhtiFxMpc4OZdqIexGXjUlBnlmpZmjEKA= 23 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 24 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= 28 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 31 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 32 | github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= 33 | github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= 34 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 37 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 38 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 39 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 40 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 41 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 42 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 43 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 44 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 45 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 46 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 47 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 58 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 59 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 60 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 61 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 62 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 63 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 64 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 65 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 66 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 67 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 68 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 69 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 70 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 71 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 72 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 73 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 75 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 76 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= 77 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 78 | --------------------------------------------------------------------------------