First commit

main
Stefano 2021-11-17 18:47:32 +01:00
commit ed74ac1a00
5 changed files with 313 additions and 0 deletions

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module xdcc-cli
go 1.13
require github.com/PuerkitoBio/goquery v1.8.0

11
go.sum Normal file
View File

@ -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=

46
main.go Normal file
View File

@ -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)
}
}

125
table.go Normal file
View File

@ -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))
}
}

126
xdcc.go Normal file
View File

@ -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
}