Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SQUASH SQUASH
- Loading branch information
0 parents
commit 5cfc6e5
Showing
4 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module stacker-beep | ||
|
||
go 1.21.1 | ||
|
||
require ( | ||
github.com/PuerkitoBio/goquery v1.8.1 // indirect | ||
github.com/andybalholm/cascadia v1.3.1 // indirect | ||
github.com/faiface/beep v1.1.0 // indirect | ||
github.com/hajimehoshi/oto v1.0.1 // indirect | ||
github.com/pkg/errors v0.9.1 // indirect | ||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect | ||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect | ||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect | ||
golang.org/x/net v0.7.0 // indirect | ||
golang.org/x/sys v0.5.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= | ||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= | ||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= | ||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= | ||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= | ||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= | ||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= | ||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= | ||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= | ||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= | ||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= | ||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= | ||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= | ||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= | ||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= | ||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= | ||
github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4= | ||
github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= | ||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= | ||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= | ||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= | ||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= | ||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= | ||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | ||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= | ||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= | ||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
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/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= | ||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= | ||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= | ||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= | ||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= | ||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= | ||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
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-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= | ||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
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/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 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= | ||
golang.org/x/sys v0.5.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/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.6/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/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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
package main | ||
|
||
import ( | ||
_ "embed" | ||
"flag" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"os" | ||
"regexp" | ||
"strings" | ||
"time" | ||
|
||
"github.com/PuerkitoBio/goquery" | ||
"github.com/faiface/beep" | ||
"github.com/faiface/beep/speaker" | ||
"github.com/faiface/beep/wav" | ||
) | ||
|
||
//go:embed alert.wav | ||
var alertSound []byte | ||
|
||
const intitialCheck = 300 * time.Minute | ||
|
||
var lastChecked time.Time | ||
|
||
var ( | ||
acceptAuthors []string | ||
interestingTopics []string | ||
interestingDomains []string | ||
territory string | ||
checkInterval time.Duration | ||
targetURL string | ||
) | ||
|
||
const ( | ||
customGray = "\x1b[37m" | ||
customBlu = "\x1b[36m" | ||
customMagenta = "\x1b[38;5;198m" | ||
customYellow = "\x1b[38;5;220m" | ||
reset = "\x1b[0m" // Reset flag | ||
) | ||
|
||
func main() { | ||
fmt.Println("") | ||
|
||
authorsPtr := flag.String("authors", "", "Comma-separated list of accepted authors") | ||
topicsPtr := flag.String("topics", "", "Comma-separated list of interesting topics") | ||
domainsPtr := flag.String("domains", "", "Comma-separated list of interesting domains") | ||
territoryPtr := flag.String("territory", "", "Territory, default is home (all)") | ||
checkIntervalValue := flag.Int("interval", 5, "Interval check in minutes, default is 5") | ||
|
||
flag.Parse() | ||
|
||
if (*authorsPtr == "") && (*topicsPtr == "") && (*domainsPtr == "") { | ||
fmt.Println(customMagenta + "You need to give me some filters!" + reset + "\n") | ||
flag.Usage() | ||
os.Exit(0) | ||
} | ||
|
||
if *authorsPtr != "" { | ||
acceptAuthors = strings.Split(*authorsPtr, ",") | ||
} | ||
if *topicsPtr != "" { | ||
interestingTopics = strings.Split(*topicsPtr, ",") | ||
} | ||
if *domainsPtr != "" { | ||
interestingDomains = strings.Split(*domainsPtr, ",") | ||
} | ||
if *territoryPtr != "" { | ||
territory = *territoryPtr | ||
} | ||
|
||
if territory != "" { | ||
targetURL = "https://stacker.news/~" + territory + "/recent" | ||
} else { | ||
targetURL = "https://stacker.news/recent" | ||
} | ||
|
||
checkInterval = time.Duration(*checkIntervalValue) * time.Minute | ||
|
||
playBeep() | ||
// Run initially and start the ticker for periodic scraping | ||
checkForNewItems(intitialCheck) | ||
ticker := time.NewTicker(checkInterval) | ||
defer ticker.Stop() | ||
|
||
for { | ||
select { | ||
case <-ticker.C: | ||
checkForNewItems(checkInterval) | ||
} | ||
} | ||
} | ||
|
||
func checkForNewItems(interval time.Duration) { | ||
resp, err := http.Get(targetURL) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
log.Fatalf("Error: status code %d", resp.StatusCode) | ||
} | ||
|
||
doc, err := goquery.NewDocumentFromReader(resp.Body) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
doc.Find(".item_hunk__DFX1z").Each(func(i int, s *goquery.Selection) { | ||
htmlContent, _ := goquery.OuterHtml(s) | ||
author := extractAuthor(htmlContent) | ||
date := extractDate(htmlContent) | ||
title, url := extractTitleAndURL(htmlContent) | ||
domain := extractDomain(htmlContent) | ||
parsedDate, err := time.Parse(time.RFC3339, date) | ||
if err != nil { | ||
fmt.Println("Error parsing date:", err) | ||
return | ||
} | ||
if time.Since(parsedDate) < interval && | ||
(isAuthorAccepted(author) || | ||
containsInterestingTopic(title) || | ||
containsInterestingDomains(domain)) { | ||
localTime := parsedDate.Local() | ||
fmt.Println(customMagenta + author + reset + " - " + customGray + localTime.Format("2006-01-02 15:04") + reset) | ||
fmt.Println(customBlu + title + reset) | ||
if domain != "" { | ||
fmt.Println(domain) | ||
} | ||
fmt.Println(customYellow + "https://stacker.news" + url + reset + "\n") | ||
playBeep() | ||
} | ||
}) | ||
|
||
lastChecked = time.Now() | ||
} | ||
|
||
func extractAuthor(htmlString string) string { | ||
re := regexp.MustCompile(`@<!-- -->(\w+)<span>`) | ||
matches := re.FindStringSubmatch(htmlString) | ||
if len(matches) >= 2 { | ||
return matches[1] | ||
} | ||
return "" | ||
} | ||
|
||
func isAuthorAccepted(author string) bool { | ||
if len(acceptAuthors) == 0 { | ||
return false | ||
} | ||
for _, accepted := range acceptAuthors { | ||
if author == accepted { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func extractDate(htmlString string) string { | ||
re := regexp.MustCompile(`title="(2023[^"]*)"`) | ||
matches := re.FindStringSubmatch(htmlString) | ||
if len(matches) >= 2 { | ||
return matches[1] | ||
} | ||
return "" | ||
} | ||
|
||
func extractTitleAndURL(htmlString string) (string, string) { | ||
re := regexp.MustCompile(`<a[^>]*\bclass="item_title[^"]*"[^>]*\bhref="([^"]+)"[^>]*>([^<]+)</a>`) | ||
matches := re.FindStringSubmatch(htmlString) | ||
if len(matches) >= 3 { | ||
return matches[2], matches[1] | ||
} | ||
return "", "" | ||
} | ||
|
||
func extractDomain(htmlString string) string { | ||
re := regexp.MustCompile(`<a[^>]*\bclass="item_link[^"]*"[^>]*\bhref="([^"]+)"[^>]*>[^<]+</a>`) | ||
matches := re.FindStringSubmatch(htmlString) | ||
if len(matches) >= 2 { | ||
return matches[1] | ||
} | ||
return "" | ||
} | ||
|
||
func containsInterestingTopic(text string) bool { | ||
if len(interestingTopics) == 0 { | ||
return false | ||
} | ||
lowercaseText := strings.ToLower(text) | ||
for _, topic := range interestingTopics { | ||
if strings.Contains(lowercaseText, topic) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func containsInterestingDomains(url string) bool { | ||
if len(interestingDomains) == 0 { | ||
return false | ||
} | ||
lowercaseText := strings.ToLower(url) | ||
for _, topic := range interestingDomains { | ||
if strings.Contains(lowercaseText, topic) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func playBeep() { | ||
f, err := os.CreateTemp("", "temp_*.wav") | ||
if err != nil { | ||
fmt.Println("Error creating temporary WAV file:", err) | ||
return | ||
} | ||
defer os.Remove(f.Name()) | ||
|
||
_, err = f.Write(alertSound) | ||
if err != nil { | ||
fmt.Println("Error writing temporary WAV file:", err) | ||
return | ||
} | ||
|
||
file, err := os.Open(f.Name()) | ||
if err != nil { | ||
fmt.Println("Error opening temporary WAV file:", err) | ||
return | ||
} | ||
defer file.Close() | ||
|
||
streamer, format, err := wav.Decode(file) | ||
if err != nil { | ||
fmt.Println("Error decoding WAV file:", err) | ||
return | ||
} | ||
defer streamer.Close() | ||
|
||
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) | ||
if err != nil { | ||
fmt.Println("Error initializing speaker:", err) | ||
return | ||
} | ||
|
||
done := make(chan struct{}) | ||
speaker.Play(beep.Seq(streamer, beep.Callback(func() { | ||
close(done) | ||
}))) | ||
|
||
<-done | ||
} |