First commit
commit
ed74ac1a00
|
|
@ -0,0 +1,5 @@
|
||||||
|
module xdcc-cli
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require github.com/PuerkitoBio/goquery v1.8.0
|
||||||
|
|
@ -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=
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue