├── .gitignore
├── LICENSE
├── README.md
├── cmd
├── file.go
├── root.go
└── url.go
├── go.mod
├── go.sum
├── main.go
├── resources
└── demo.gif
├── types
└── types.go
└── utils
├── print.go
├── search.go
├── spinner.go
└── timestamp.go
/.gitignore:
--------------------------------------------------------------------------------
1 | samples/
2 | TODO.md
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Cade Cuddy
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
🥫 sauce - CLI anime identification
2 |
3 | sauce uses trace.moe to identify the anime in an image & serves you its essential details so you can determine if it's worth the watch.
4 |
5 |
6 |
7 |
8 |
9 | never find yourself asking "sauce?" ever again!
10 |
11 | #### This project was inspired by [what-anime-cli](https://github.com/irevenko/what-anime-cli/) by [irevenko](https://github.com/irevenko). I felt there was more utility in looking up the identified anime's MyAnimeList stats, so that was the approach I took with sauce.
12 |
13 | ## 🔧 Installation
14 |
15 | Install with [Go](https://go.dev/) install:
16 | ```bash
17 | go install github.com/cadecuddy/sauce@latest
18 | ```
19 |
20 | ## 💻 Usage
21 | **Known supported image types: jpg, png, webp, gif**
22 |
23 | ### 🔗 search by image url
24 | `sauce url `
25 | ```bash
26 | sauce url https://findthis.jp/anime.png
27 | ```
28 |
29 | ### 📂 search by image file
30 | `sauce file `
31 | ```bash
32 | sauce file demon-slayer.png
33 | ```
34 |
35 | ## 📟 Environment Setup (contributors)
36 |
37 | `git clone https://github.com/cadecuddy/sauce.git`
38 | `cd sauce`
39 |
40 | If you find any bugs or want to add any cool features feel free to leave a PR!
41 |
42 | ## 🤝 made with
43 | * [trace.moe](https://soruly.github.io/trace.moe-api/#/) - anime identification
44 | * [jikan-go](https://github.com/darenliang/jikan-go) - MyAnimeList data
45 |
--------------------------------------------------------------------------------
/cmd/file.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "mime/multipart"
8 | "net/http"
9 | "os"
10 |
11 | "github.com/cadecuddy/sauce/utils"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | const FILE_SEARCH = "https://api.trace.moe/search"
16 |
17 | // fileCmd represents the file command
18 | var fileCmd = &cobra.Command{
19 | Use: "file",
20 | Short: "Search for anime source using a path to local file.",
21 | Long: `Search for anime source using a path to local file.
22 |
23 | File searches support most visual file mediums under 25MB including:
24 | - png
25 | - jpeg/jpg
26 | - gif
27 | - webp
28 | - many more!
29 | `,
30 | Run: func(cmd *cobra.Command, args []string) {
31 | fileSearch(args[0])
32 | },
33 | Args: cobra.MinimumNArgs(1),
34 | }
35 |
36 | // Searches for anime based on file path input
37 | func fileSearch(filepath string) {
38 | file, err := os.Open(filepath)
39 | if err != nil {
40 | fmt.Println("❌ Invalid file")
41 | return
42 | }
43 | defer file.Close()
44 |
45 | s := utils.ConfigSpinner()
46 | s.Start()
47 |
48 | // form file upload via https://gist.github.com/andrewmilson/19185aab2347f6ad29f5
49 | buffer := &bytes.Buffer{}
50 | writer := multipart.NewWriter(buffer)
51 | part, _ := writer.CreateFormFile("file", filepath)
52 | io.Copy(part, file)
53 | writer.Close()
54 |
55 | res, err := http.Post(URL_SEARCH, writer.FormDataContentType(), buffer)
56 | identifiedAnime, malData := utils.HandleResponse(res, err, s)
57 |
58 | s.Stop()
59 |
60 | // do things
61 | utils.PrintSauce(identifiedAnime, malData.Data)
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // rootCmd represents the base command when called without any subcommands
10 | var rootCmd = &cobra.Command{
11 | Use: "sauce",
12 | Short: "🥫 sauce is a CLI tool to find the source of anime screenshots, gifs, clips, etc.",
13 | }
14 |
15 | // Execute adds all child commands to the root command and sets flags appropriately.
16 | // This is called by main.main(). It only needs to happen once to the rootCmd.
17 | func Execute() {
18 | rootCmd.SetUsageTemplate(`Usage:
19 | sauce [command]
20 |
21 | Available Commands:
22 | file Search using a path to local file.
23 | url Search using a url to media.
24 | help Help about any command.
25 |
26 | Use "sauce [command] --help" for more information about a command.
27 | `)
28 | rootCmd.AddCommand(urlCmd)
29 | rootCmd.AddCommand(fileCmd)
30 | err := rootCmd.Execute()
31 | if err != nil {
32 | os.Exit(1)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cmd/url.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 |
8 | "github.com/cadecuddy/sauce/utils"
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | const URL_SEARCH string = "https://api.trace.moe/search?anilistInfo&url="
13 |
14 | // urlCmd represents the url command
15 | var urlCmd = &cobra.Command{
16 | Use: "url",
17 | Short: "Search for anime source using the url to the media.",
18 | Long: `Search for anime source using the url to the media.`,
19 | Run: func(cmd *cobra.Command, args []string) {
20 | urlSearch(args[0])
21 | },
22 | Args: cobra.MinimumNArgs(1),
23 | }
24 |
25 | // Makes a search via URL to the unidentified anime media.
26 | //
27 | // Checks the URL and once verififed, queries the trace.moe API,
28 | // getting the core information of the show.
29 | func urlSearch(linkToMedia string) {
30 | // validate URL
31 | _, err := url.ParseRequestURI(linkToMedia)
32 | if err != nil {
33 | fmt.Println("❌ Invalid URL")
34 | return
35 | }
36 | s := utils.ConfigSpinner()
37 | s.Start()
38 |
39 | // make GET request to trace.moe API
40 | resp, err := http.Get(URL_SEARCH + linkToMedia)
41 | identifiedAnime, malData := utils.HandleResponse(resp, err, s)
42 |
43 | s.Stop()
44 |
45 | // use highest similarity accuracy
46 | utils.PrintSauce(identifiedAnime, malData.Data)
47 | }
48 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cadecuddy/sauce
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/briandowns/spinner v1.19.0
7 | github.com/darenliang/jikan-go v1.2.3
8 | github.com/dustin/go-humanize v1.0.0
9 | github.com/fatih/color v1.13.0
10 | github.com/spf13/cobra v1.5.0
11 | )
12 |
13 | require (
14 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
15 | github.com/mattn/go-colorable v0.1.9 // indirect
16 | github.com/mattn/go-isatty v0.0.14 // indirect
17 | github.com/spf13/pflag v1.0.5 // indirect
18 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
19 | )
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=
2 | github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
4 | github.com/darenliang/jikan-go v1.2.3 h1:Nw6ykJU47QW3rwiIBWHyy1cBNM1Cxsz0AVCdqIN278A=
5 | github.com/darenliang/jikan-go v1.2.3/go.mod h1:rv7ksvNqc1b0UK7mf1Uc3swPToJXd9EZQLz5C38jk9Q=
6 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
7 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
8 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
9 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
10 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
11 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
12 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
13 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
14 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
15 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
16 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
17 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
18 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
19 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
20 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
21 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
22 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
23 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
24 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
25 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
26 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
28 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
29 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
31 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
32 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 CADE CUDDY
3 | MADE WITH: trace.moe API, JIKAN API
4 | */
5 | package main
6 |
7 | import "github.com/cadecuddy/sauce/cmd"
8 |
9 | func main() {
10 | cmd.Execute()
11 | }
12 |
--------------------------------------------------------------------------------
/resources/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cadecuddy/sauce/b90ecf0c6a5274bfd1efe6991bc638560131397b/resources/demo.gif
--------------------------------------------------------------------------------
/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | // Holds unmarshalled trace.moe JSON response data
4 | type MoeResponse struct {
5 | Framecount int `json:"frameCount"`
6 | Error string `json:"error"`
7 | Result []Result `json:"result"`
8 | }
9 |
10 | // Body of relevant data when a successful response goes through
11 | type Result struct {
12 | Anilist AnilistData `json:"anilist"`
13 | Filename string `json:"filename"`
14 | Episode int `json:"episode"`
15 | From float64 `json:"from"`
16 | To float64 `json:"to"`
17 | Similarity float64 `json:"similarity"`
18 | Video string `json:"video"`
19 | Image string `json:"image"`
20 | }
21 |
22 | // Anilist information related to show/episode
23 | type AnilistData struct {
24 | ID int `json:"id"`
25 | MalID int `json:"idMal"`
26 | Title struct {
27 | Native string `json:"native"`
28 | Romaji string `json:"romaji"`
29 | English string `json:"english"`
30 | } `json:"title"`
31 | Synonyms []string `json:"synonyms"`
32 | IsAdult bool `json:"isAdult"`
33 | }
34 |
--------------------------------------------------------------------------------
/utils/print.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/cadecuddy/sauce/types"
8 | "github.com/darenliang/jikan-go"
9 | "github.com/dustin/go-humanize"
10 | "github.com/fatih/color"
11 | )
12 |
13 | const (
14 | PREFIX_LENGTH int = 12
15 | BORDER_MAX = 28
16 | )
17 |
18 | // Prints the output of the result of the search to the terminal
19 | func PrintSauce(res types.Result, malData jikan.AnimeBase) {
20 | b := color.New(color.Bold)
21 | color.New(color.FgGreen).Add(color.Bold).Printf("✅ sauce found : [%f similarity]\n", res.Similarity)
22 |
23 | // regulate size & print top flower border
24 | size := getBorderSize(res.Anilist.Title.Romaji, res.Anilist.Title.English)
25 | if size >= BORDER_MAX {
26 | size = BORDER_MAX
27 | }
28 | println(strings.Repeat("🌸", size))
29 | println()
30 |
31 | formatTitle(res, size)
32 | formatType(res, malData)
33 | formatScore(malData.Score)
34 | b.Print("🏆 Ranking: ")
35 | color.New(color.FgHiMagenta).Printf("#%s\n", humanize.Comma(int64(malData.Rank)))
36 | b.Print("📕 Source: ")
37 | color.Red(" %s", malData.Source)
38 | // Movies won't have their year load from MAL Data
39 | if malData.Year != 0 {
40 | b.Print("📅 Year: ")
41 | color.Cyan(" %s %d", strings.Title(malData.Season), malData.Year)
42 | }
43 | formatGenre(malData.Genres)
44 | b.Print("🎬 Studio: ")
45 | color.New(color.FgGreen).Println(malData.Studios[0].Name)
46 |
47 | // Print bottom border
48 | println()
49 | println(strings.Repeat("🌸", size))
50 | }
51 |
52 | // Helper function that crudely calculates the total flower border size.
53 | //
54 | // Maximum border size is
55 | func getBorderSize(nativeTitle string, englishTitle string) int {
56 | var borderLength = PREFIX_LENGTH
57 |
58 | if nativeTitle == englishTitle {
59 | borderLength += len(nativeTitle)
60 | } else {
61 | borderLength += len(nativeTitle) + len(englishTitle) + 3
62 | }
63 |
64 | return int(float32(borderLength))
65 | }
66 |
67 | // Prints the Anime's native title as well as an English
68 | // translation if one is available
69 | func formatTitle(res types.Result, borderSize int) {
70 | color.New(color.Bold).Print("✨ Anime: ")
71 | var title string
72 | if len(res.Anilist.Title.English) != 0 {
73 | title = fmt.Sprintf("%s (%s)", res.Anilist.Title.Native, res.Anilist.Title.English)
74 | } else {
75 | title = fmt.Sprintf("%s (%s)", res.Anilist.Title.Native, res.Anilist.Title.Romaji)
76 | }
77 | color.New(color.Bold).Printf("%s\n", title)
78 | }
79 |
80 | // Formats the episode/timestamp sections depending on the media form
81 | // of the anime. TV shows will print the episode section. Movies will
82 | // print a different 'Scene' section containing the scene's timestamp
83 | func formatType(res types.Result, malData jikan.AnimeBase) {
84 | b := color.New(color.Bold)
85 | b.Print("❓ Type: ")
86 |
87 | if malData.Type == "Movie" {
88 | fmt.Println("Movie 🎥")
89 | b.Print("🕐 Scene: ")
90 | formatTimestamp(res.From, res.To)
91 | return
92 | } else {
93 | fmt.Println("TV Show 📺")
94 | formatEpisodes(res.Episode, malData.Episodes, res.From, res.To)
95 | return
96 | }
97 |
98 | }
99 |
100 | // Formats and prints the episode count if the detected anime is a TV Show
101 | func formatEpisodes(episode int, totalEpisodes int, timestampFrom float64, timestampTo float64) {
102 | color.New(color.Bold).Print("🕐 Episode: ")
103 |
104 | if totalEpisodes != 0 {
105 | fmt.Printf("%d/%d @", episode, totalEpisodes)
106 | } else {
107 | color.New(color.FgRed).Printf("%d @", episode)
108 | }
109 | formatTimestamp(timestampFrom, timestampTo)
110 | }
111 |
112 | // Generate the scene's timestamp in the anime
113 | func formatTimestamp(from float64, to float64) {
114 | color.New(color.FgHiBlue).Add(color.Bold).Printf(" [%s - %s]\n", ConvertTimestamp(from), ConvertTimestamp(to))
115 | }
116 |
117 | // Prints the anime's genres as found on MAL
118 | func formatGenre(genres []jikan.MalItem) {
119 | color.New(color.Bold).Print("📜 Genres: ")
120 | r := color.New(color.FgBlue)
121 |
122 | for i, genre := range genres {
123 | if i != 0 {
124 | r.Print(", ", genre.Name)
125 | } else {
126 | r.Print(genre.Name)
127 | }
128 | }
129 | print("\n")
130 | }
131 |
132 | // Prints the score with a color dependent on how high it is.
133 | func formatScore(score float64) {
134 | color.New(color.Bold).Print("📈 Score: ")
135 |
136 | if score >= 8 {
137 | color.New(color.FgHiGreen).Add(color.Bold).Println(score)
138 | return
139 | }
140 | if score >= 7 {
141 | color.New(color.FgYellow).Add(color.Bold).Println(score)
142 | return
143 | }
144 |
145 | color.New(color.FgRed).Add(color.Bold).Println(score)
146 | }
147 |
--------------------------------------------------------------------------------
/utils/search.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/briandowns/spinner"
11 | "github.com/cadecuddy/sauce/types"
12 | "github.com/darenliang/jikan-go"
13 | )
14 |
15 | // Analyzes initial trace.moe API response and handles errors.
16 | // A 3rd party MAL (MyAnimeList) API is queried to get more
17 | // detailed anime data to supplement the trace.moe data.
18 | func HandleResponse(res *http.Response, err error, s *spinner.Spinner) (types.Result, jikan.AnimeById) {
19 | if err != nil {
20 | fmt.Println("❌ Error with request")
21 | s.Stop()
22 | os.Exit(1)
23 | }
24 | defer res.Body.Close()
25 |
26 | // read body to buffer
27 | body, err := ioutil.ReadAll(res.Body)
28 | if err != nil {
29 | os.Exit(1)
30 | }
31 |
32 | // Unmarshall JSON to custom trace.moe response type
33 | var traceMoeResponse types.MoeResponse
34 | json.Unmarshal(body, &traceMoeResponse)
35 | if traceMoeResponse.Error != "" {
36 | s.Stop()
37 | fmt.Println("❌ Invalid Media")
38 | os.Exit(1)
39 | }
40 |
41 | // Query jikan API for MAL data
42 | identifiedAnime := traceMoeResponse.Result[0]
43 | malData, err := jikan.GetAnimeById(identifiedAnime.Anilist.MalID)
44 | if err != nil {
45 | s.Stop()
46 | fmt.Println("❌ Error getting MAL data")
47 | os.Exit(1)
48 | }
49 |
50 | return identifiedAnime, *malData
51 | }
52 |
--------------------------------------------------------------------------------
/utils/spinner.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/briandowns/spinner"
5 | "time"
6 | )
7 |
8 | func ConfigSpinner() *spinner.Spinner {
9 | someSet := []string{"[🥫🌸🌸🌸🌸]", "[🌸🥫🌸🌸🌸]", "[🌸🌸🥫🌸🌸]", "[🌸🌸🌸🥫🌸]", "[🌸🌸🌸🌸🥫]"}
10 | s := spinner.New(someSet, 100*time.Millisecond)
11 | s.Suffix = " 🔍 searching for sauce..."
12 |
13 | return s
14 | }
15 |
--------------------------------------------------------------------------------
/utils/timestamp.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "fmt"
4 |
5 | func ConvertTimestamp(time float64) string {
6 | totalSecs := int64(time)
7 | hours := totalSecs / 3600
8 | minutes := (totalSecs % 3600) / 60
9 | seconds := totalSecs % 60
10 |
11 | var timeString string
12 | if hours != 0 {
13 | timeString = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
14 | } else {
15 | timeString = fmt.Sprintf("%02d:%02d", minutes, seconds)
16 | }
17 |
18 | return timeString
19 | }
20 |
--------------------------------------------------------------------------------