From ed74ac1a00a8f4cc71e67e896068aff09b414f19 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 17 Nov 2021 18:47:32 +0100 Subject: [PATCH] First commit --- go.mod | 5 +++ go.sum | 11 +++++ main.go | 46 ++++++++++++++++++++ table.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ xdcc.go | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 313 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 table.go create mode 100644 xdcc.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..17128f5 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module xdcc-cli + +go 1.13 + +require github.com/PuerkitoBio/goquery v1.8.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d5e55e1 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f266a9f --- /dev/null +++ b/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +var registry *XdccProviderRegistry = nil + +func init() { + registry = NewProviderRegistry() + registry.AddProvider(&XdccEuProvider{}) +} + +func search(fileName string) { + printer := NewTablePrinter([]string{"File Name", "Network", "Channel"}) + + res, _ := registry.Search(fileName) + for _, fileInfo := range res { + printer.AddRow(Row{fileInfo.Name, fileInfo.Network, fileInfo.Channel}) + } + + printer.Print() +} + +func main() { + searchCmd := flag.NewFlagSet("foo", flag.ExitOnError) + fileName := searchCmd.String("f", "", "name of the file to search") + + if len(os.Args) < 2 { + fmt.Println("one of the following subcommands is expected: [search, get]") + os.Exit(1) + } + + switch os.Args[1] { + case "search": + searchCmd.Parse(os.Args[2:]) + search(*fileName) + case "get": + break + default: + fmt.Println("no such command: ", os.Args[1]) + os.Exit(1) + } +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..58561f3 --- /dev/null +++ b/table.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "strings" +) + +type Row []string + +type TablePrinter struct { + Headers []string + Rows []Row + MaxRows int +} + +func NewTablePrinter(headers []string) *TablePrinter { + return &TablePrinter{ + Headers: headers, + MaxRows: -1, + } +} + +func centerString(s string, width int) string { + padSpace := width - len(s) + leftPadding := padSpace / 2 + rightPadding := padSpace - leftPadding + return strings.Repeat(" ", leftPadding) + s + strings.Repeat(" ", rightPadding) +} + +func formatStr(fileName string, maxSize int) string { + if len(fileName) <= maxSize { + return centerString(fileName, maxSize) + } + + return centerString(fileName[:maxSize-3]+"...", maxSize) +} + +const paddingDefault = 2 + +func (printer *TablePrinter) NumRows() int { + if printer.Rows == nil { + return 0 + } + return len(printer.Rows) +} + +func (printer *TablePrinter) NumCols() int { + return len(printer.Headers) +} + +func (printer *TablePrinter) hasRows() bool { + return printer.Rows != nil && len(printer.Rows) > 0 +} + +func (printer *TablePrinter) computeColumnWidthds() []int { + widths := make([]int, printer.NumCols()) + for i := 0; i < printer.NumCols(); i++ { + widths[i] = len(printer.Headers[i]) + } + + if printer.hasRows() { + for col := 0; col < printer.NumCols(); col++ { + for row := 0; row < len(printer.Rows); row++ { + if len(printer.Rows[row][col]) > widths[col] { + widths[col] = len(printer.Rows[row][col]) + } + } + } + } + + for i := 0; i < printer.NumCols(); i++ { + widths[i] += paddingDefault + } + return widths +} + +func (printer *TablePrinter) renderRow(s []string, colWidths []int) string { + content := "|" + for i := 0; i < len(printer.Headers)-1; i++ { + content += formatStr(s[i], colWidths[i]) + "|" + } + content += formatStr(s[printer.NumCols()-1], colWidths[printer.NumCols()-1]) + "|" + return content +} + +func (printer *TablePrinter) renderLine(colWidths []int) string { + content := "+" + for i := 0; i < len(printer.Headers)-1; i++ { + content += formatStr(strings.Repeat("-", colWidths[i]), colWidths[i]) + "-" + } + content += formatStr(strings.Repeat("-", colWidths[printer.NumCols()-1]), colWidths[printer.NumCols()-1]) + "+" + return content +} + +func (printer *TablePrinter) renderHeader(colWidths []int) { + fmt.Println(printer.renderLine(colWidths)) + fmt.Println(printer.renderRow(printer.Headers, colWidths)) + fmt.Println(printer.renderLine(colWidths)) +} + +const initialRows = 100 + +func (printer *TablePrinter) AddRow(r Row) { + if printer.Rows == nil { + printer.Rows = make([]Row, 0, initialRows) + } + + if printer.MaxRows > 0 && printer.NumRows() == printer.MaxRows { + return + } + printer.Rows = append(printer.Rows, r) + +} + +func (printer *TablePrinter) Print() { + colWidths := printer.computeColumnWidthds() + + printer.renderHeader(colWidths) + if printer.hasRows() { + for _, row := range printer.Rows { + fmt.Println(printer.renderRow(row, colWidths)) + } + fmt.Println(printer.renderLine(colWidths)) + } +} diff --git a/xdcc.go b/xdcc.go new file mode 100644 index 0000000..180cc86 --- /dev/null +++ b/xdcc.go @@ -0,0 +1,126 @@ +package main + +import ( + "errors" + "log" + "net/http" + "strconv" + "strings" + "sync" + + "github.com/PuerkitoBio/goquery" +) + +type XdccFileInfo struct { + Network string + Channel string + BotName string + Name string + Slot int +} + +type XdccProvider interface { + Search(fileName string) ([]XdccFileInfo, error) +} + +type XdccProviderRegistry struct { + providerList []XdccProvider +} + +const MaxProviders = 100 + +func NewProviderRegistry() *XdccProviderRegistry { + return &XdccProviderRegistry{ + providerList: make([]XdccProvider, 0, MaxProviders), + } +} + +func (registry *XdccProviderRegistry) AddProvider(provider XdccProvider) { + registry.providerList = append(registry.providerList, provider) +} + +const MaxResults = 1024 + +func (registry *XdccProviderRegistry) Search(fileName string) ([]XdccFileInfo, error) { + allResults := make([]XdccFileInfo, 0, MaxResults) + + wg := sync.WaitGroup{} + wg.Add(len(registry.providerList)) + for _, p := range registry.providerList { + go func(p XdccProvider) { + res, err := p.Search(fileName) + + if err != nil { + return + } + + allResults = append(allResults, res...) + + wg.Done() + }(p) + } + wg.Wait() + return allResults, nil +} + +type XdccEuProvider struct{} + +const XdccEuURL = "https://www.xdcc.eu/search.php" + +func (p *XdccEuProvider) parseFields(fields []string) (*XdccFileInfo, error) { + if len(fields) != 7 { + return nil, errors.New("unespected number of search entry fields") + } + + fInfo := &XdccFileInfo{} + fInfo.Network = fields[0] + fInfo.Channel = fields[1] + fInfo.BotName = fields[2] + slot, err := strconv.Atoi(fields[3][1:]) + fInfo.Name = fields[6] + + if err != nil { + return nil, err + } + + fInfo.Slot = slot + return fInfo, nil +} + +func (p *XdccEuProvider) Search(fileName string) ([]XdccFileInfo, error) { + searchkey := strings.Join(strings.Fields(fileName), "+") + res, err := http.Get(XdccEuURL + "?searchkey=" + searchkey) + + if err != nil { + log.Fatal(err) + return nil, err + } + + defer res.Body.Close() + if res.StatusCode != 200 { + log.Fatalf("status code error: %d %s", res.StatusCode, res.Status) + return nil, err + } + + // Load the HTML document + doc, err := goquery.NewDocumentFromReader(res.Body) + if err != nil { + log.Fatal(err) + return nil, err + } + + fileInfos := make([]XdccFileInfo, 0) + doc.Find("tr").Each(func(i int, s *goquery.Selection) { + fields := make([]string, 0) + + s.Find("td").Each(func(j int, si *goquery.Selection) { + fields = append(fields, strings.TrimSpace(si.Text())) + }) + + info, err := p.parseFields(fields) + if err == nil { + fileInfos = append(fileInfos, *info) + } + }) + return fileInfos, nil +}