-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathurlclip.go
187 lines (173 loc) · 3.78 KB
/
urlclip.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package urlclip
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
"sync"
"time"
"github.com/gen2brain/beeep"
"golang.design/x/clipboard"
"golang.org/x/net/html"
)
var (
dataFileName = "data/data.json"
urlRegex = `^(http|https)?://(.*)?(/.*)?$`
throttleLimit = 10
)
type Url struct {
URL string `json:"url"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
}
var datas []*Url
func SaveData(filename string, url []*Url) error {
data, err := json.MarshalIndent(url, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, data, os.ModePerm)
}
func ReadData(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return err
}
return json.Unmarshal(data, &datas)
}
func CheckData(url string) bool {
for _, x := range datas {
if x.URL == url {
return true
}
}
return false
}
func Notify(title, msg string) error {
return beeep.Notify(title, msg, "assets/information.png")
}
func Alert(title, msg string) error {
return beeep.Alert(title, msg, "assets/warning.png")
}
func GetTitle(ctx context.Context, url string) (string, error) {
res, err := http.Get(url)
if err != nil {
return "", nil
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("url not reached")
}
return ParseTitle(res.Body)
}
func GetTitleWg(ctx context.Context, url *Url, throttle chan struct{}, result chan<- *Url, wg *sync.WaitGroup) {
defer wg.Done()
throttle <- struct{}{}
defer func() { <-throttle }()
res, err := http.Get(url.URL)
if err != nil {
result <- nil
return
}
defer res.Body.Close()
title, err := ParseTitle(res.Body)
if err != nil {
result <- nil
return
}
url.Title = title
result <- url
}
func ParseTitle(r io.Reader) (string, error) {
doc, err := html.Parse(r)
if err != nil {
return "", err
}
var title string
var traverse func(*html.Node)
traverse = func(node *html.Node) {
if node.Type == html.ElementNode && node.Data == "title" {
if node.FirstChild != nil {
title = node.FirstChild.Data
}
return
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
traverse(child)
}
}
traverse(doc)
return title, nil
}
func Run(quitCh chan os.Signal, active *bool) {
if err := ReadData(dataFileName); err != nil {
log.Println(err)
}
if err := clipboard.Init(); err != nil {
log.Fatal(err)
}
cbCh := clipboard.Watch(context.TODO(), clipboard.FmtText)
if len(datas) > 0 {
var wg sync.WaitGroup
results := make(chan *Url, len(datas))
throttle := make(chan struct{}, throttleLimit)
log.Println("wg start check url title")
for _, item := range datas {
if item.Title == "" {
wg.Add(1)
go GetTitleWg(context.TODO(), item, throttle, results, &wg)
}
}
go func() {
wg.Wait()
close(results)
close(throttle)
log.Println("wg closed")
}()
for result := range results {
for _, d := range datas {
if result != nil && d.URL == result.URL {
d.Title = result.Title
log.Println("add title url:", d.URL, d.Title)
break
}
}
}
SaveData(dataFileName, datas)
}
regex := regexp.MustCompile(urlRegex)
log.Println("urlclip start watching...")
loop:
for {
select {
case text := <-cbCh:
if *active {
url := regex.FindString(string(text))
if url != "" {
title, _ := GetTitle(context.TODO(), url)
log.Println("saved url:", url, title)
if !CheckData(url) {
datas = append(datas, &Url{
URL: url,
Title: title,
CreatedAt: time.Now(),
})
if SaveData(dataFileName, datas) == nil {
Notify("url saved", url)
}
} else {
Alert("url already exist", url)
}
}
}
case <-quitCh:
log.Println("urlclip gracefull termination")
close(quitCh)
break loop
}
}
}