commit 614504e933dd930e2a0769502e557702026ac974 Author: skidoodle Date: Wed Oct 9 01:11:28 2024 +0200 Init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..93eeffc --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +NICK= +PASS= +PROFILE_1=https://ncore.pro/profile.php?id=1577943 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f9b9ab3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:alpine as builder +WORKDIR /build +COPY . . +RUN go build -o trackncore . + +FROM alpine:latest +WORKDIR /app/ +COPY --from=builder /build/trackncore . +COPY --from=builder /build/index.html . +EXPOSE 3000 +CMD ["./trackncore"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..25bff95 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +services: + trackncore: + image: ghcr.io/skidoodle/trackncore:main + container_name: trackncore + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - data:/app + +volumes: + data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e3b5421 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module trackncore + +go 1.23 + +toolchain go1.23.2 + +require ( + github.com/PuerkitoBio/goquery v1.10.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/andybalholm/cascadia v1.3.2 // indirect + golang.org/x/net v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..367fe40 --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= +github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/index.html b/index.html new file mode 100644 index 0000000..d520e0b --- /dev/null +++ b/index.html @@ -0,0 +1,225 @@ + + + + + + nCore Profile Stats + + + + + +

nCore Profile Stats

+ +
+ + + + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..8d98dc4 --- /dev/null +++ b/main.go @@ -0,0 +1,180 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/joho/godotenv" +) + +type ProfileData struct { + Owner string `json:"owner"` + Timestamp time.Time `json:"timestamp"` + Rank string `json:"rank"` + Upload string `json:"upload"` + CurrentUpload string `json:"current_upload"` + CurrentDownload string `json:"current_download"` + Points string `json:"points"` +} + +var ( + profiles = map[string]string{} + jsonFile = "data.json" + nick string + pass string + client *http.Client +) + +func init() { + if err := godotenv.Load(); err != nil { + log.Fatal("Error loading .env file") + } + + nick = os.Getenv("NICK") + pass = os.Getenv("PASS") + + for i := 1; i <= 10; i++ { + profileKey := fmt.Sprintf("PROFILE_%d", i) + if profileURL := os.Getenv(profileKey); profileURL != "" { + profiles[profileURL] = profileURL + } + } + + client = &http.Client{} +} + +func fetchProfile(url string) (*ProfileData, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Cookie", fmt.Sprintf("nick=%s; pass=%s", nick, pass)) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch profile: %s", url) + } + + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return nil, err + } + + owner := strings.TrimSpace(doc.Find(".fobox_fej").Text()) + owner = strings.Replace(owner, " profilja", "", 1) + + profile := &ProfileData{ + Owner: owner, + Timestamp: time.Now(), + } + + doc.Find(".userbox_tartalom_mini .profil_jobb_elso2").Each(func(i int, s *goquery.Selection) { + label := s.Text() + value := s.Next().Text() + + switch label { + case "Helyezés:": + profile.Rank = value + case "Feltöltés:": + profile.Upload = value + case "Aktuális feltöltés:": + profile.CurrentUpload = value + case "Aktuális letöltés:": + profile.CurrentDownload = value + case "Pontok száma:": + profile.Points = value + } + }) + + return profile, nil +} + +func readExistingProfiles() ([]ProfileData, error) { + if _, err := os.Stat(jsonFile); os.IsNotExist(err) { + return []ProfileData{}, nil + } + + file, err := os.Open(jsonFile) + if err != nil { + return nil, err + } + defer file.Close() + + var profiles []ProfileData + byteValue, _ := io.ReadAll(file) + err = json.Unmarshal(byteValue, &profiles) + return profiles, err +} + +func logToJSON(profile *ProfileData) error { + existingProfiles, err := readExistingProfiles() + if err != nil { + return err + } + + existingProfiles = append(existingProfiles, *profile) + + file, err := os.OpenFile(jsonFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer file.Close() + + enc := json.NewEncoder(file) + enc.SetIndent("", " ") + return enc.Encode(existingProfiles) +} + +func dataHandler(w http.ResponseWriter, r *http.Request) { + profiles, err := readExistingProfiles() + if err != nil { + http.Error(w, "Could not read data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(profiles) +} + +func serveHTML(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "index.html") +} + +func main() { + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + + go func() { + for { + for _, url := range profiles { + profile, err := fetchProfile(url) + if err != nil { + log.Println(err) + continue + } + + if err := logToJSON(profile); err != nil { + log.Println(err) + } + } + <-ticker.C + } + }() + + http.HandleFunc("/data", dataHandler) + http.HandleFunc("/", serveHTML) + log.Fatal(http.ListenAndServe(":3000", nil)) +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f1b8c32 --- /dev/null +++ b/readme.md @@ -0,0 +1,75 @@ +# nCore Profile Tracker + +A simple Go project to scrape and track profile statistics (rank, upload, download, points) on nCore, the largest Hungarian BitTorrent tracker. The stats are displayed on a basic web interface and saved as JSON for historical tracking. + +## Features + +- Scrapes and logs profile stats from nCore. +- Serves a simple HTML dashboard to display the latest data. +- Provides a JSON API to fetch historical profile data. +- Automatically updates data every 24 hours. + +## Setup + +1. Clone the repo: + + ```bash + git clone https://github.com/skidoodle/ncore-profile-tracker.git + cd ncore-profile-tracker + ``` + +2. Create a `.env` file with your nCore credentials and profile URLs: + + ```bash + NICK=your_nick + PASS=your_password + PROFILE_1=https://ncore.pro/profile.php?id=1577943 + ``` + +### How to obtain `NICK` and `PASS` + +- Open the developer tools in your browser (F12), go to the "Network" tab. +- Log in using "lower security" mode. +- Find the `login.php` request in the network activity. +- In the response headers, locate the `Set-Cookie` header, which will contain `nick=` and `pass=` values. +- Copy those values and add them to your `.env` file. + +## Running with Docker Compose + +To deploy the project using Docker Compose: + +1. Create the following `docker-compose.yml` file: + + ```yaml + version: "3" + services: + trackncore: + image: ghcr.io/skidoodle/trackncore:main + container_name: trackncore + restart: unless-stopped + ports: + - "3000:3000" + env_file: + - .env + volumes: + - data:/app + + volumes: + data: + ``` + +2. Run the Docker Compose setup: + + ```bash + docker-compose up -d + ``` + +3. Open `:3000` to view your stats. + +### Updating + +To pull the latest image and restart the service: + +```bash +docker-compose pull +docker-compose up -d