-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.go
287 lines (262 loc) · 7.33 KB
/
main.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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/fs"
"math/rand"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path"
"strings"
"sync"
"time"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
errGetCookie = "读取 sessdata 失败,请设置命令行参数 -sessdata"
errGetEmoji = "获取表情包列表失败:%v"
errParseEmoji = "解析表情包列表失败:%v"
errGetInfo = "获取表情包信息失败"
errRetryDownload = "第%d次下载%s失败,正在重试:%v"
errDownloadImg = "下载%s失败:%s"
errParseLink = "解析%s表情下载地址失败:%s"
successDownload = "下载%s成功"
fileAlreadyExist = "文件%s已存在,跳过下载"
)
var (
client *http.Client = nil
// from template: https://github.com/charmbracelet/bubbletea/blob/master/examples/send-msg/main.go
spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0)
dotStyle = helpStyle.Copy().UnsetMargins()
durationStyle = dotStyle.Copy()
appStyle = lipgloss.NewStyle().Margin(1, 2, 0, 2)
)
func randomEmoji() string {
emojis := []string{
"🍔",
"🍕",
"🌭",
"🍣",
"🍦",
"🍩",
"🍪",
"🍎",
"🍌",
"🍇",
}
return emojis[rand.Intn(len(emojis))]
}
type resultMsg struct {
emoji string
msg string
quit bool
err error
duration time.Duration
}
func (r resultMsg) String() string {
if r.duration == 0 {
return dotStyle.Render(strings.Repeat(".", 30))
}
var msg string
if r.msg != "" {
msg = r.msg
} else {
msg = r.err.Error()
}
return fmt.Sprintf("%s %s %s", r.emoji, msg,
durationStyle.Render(r.duration.String()))
}
type model struct {
spinner spinner.Model
results []resultMsg
quit bool
abort bool
error bool
}
func newModel() model {
const numLastResults = 8
s := spinner.New()
s.Style = spinnerStyle
return model{
spinner: s,
results: make([]resultMsg, numLastResults),
}
}
func (m model) Init() tea.Cmd {
return m.spinner.Tick
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyCtrlC {
m.abort = true
return m, tea.Quit
}
return m, nil
case resultMsg:
if msg.quit {
m.quit = true
return m, tea.Quit
} else if msg.err != nil {
m.results = append(m.results[1:], msg)
m.error = true
return m, tea.Quit
}
m.results = append(m.results[1:], msg)
return m, nil
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
default:
return m, nil
}
}
func (m model) View() string {
var s string
if m.quit {
s += "下载已完成!"
} else if m.abort {
s += "下载被中断!"
} else if m.error {
s += m.spinner.View() + "执行出现错误!"
} else {
s += m.spinner.View() + " 下载表情包中..."
}
s += "\n\n"
for _, res := range m.results {
s += res.String() + "\n"
}
if m.quit || m.error {
s += "\n"
}
return appStyle.Render(s)
}
func initClient(cookie string) {
url, _ := url.Parse("https://api.bilibili.com")
cookieJar, _ := cookiejar.New(nil)
cookieJar.SetCookies(url, []*http.Cookie{
{
Name: "SESSDATA",
Value: cookie,
},
})
client = &http.Client{
Timeout: 10 * time.Second,
Jar: cookieJar,
}
}
// Downloader 图标包下载器
type Downloader struct {
wg sync.WaitGroup
}
// Download 下载单个表情包
func (d *Downloader) Download(p *tea.Program, startTime time.Time, stickerInfo map[string]interface{}) {
name, ok := stickerInfo["text"].(string) // 读取表情包名称
if !ok {
p.Send(resultMsg{emoji: randomEmoji(), msg: errGetInfo, duration: time.Since(startTime)})
return
}
dirName := path.Join("stickers", name)
os.MkdirAll(dirName, os.FileMode(0755)) // 为每个表情包创建单独文件夹
stickerItems, ok := stickerInfo["emote"].([]interface{})
if !ok {
p.Send(resultMsg{emoji: randomEmoji(), msg: errGetInfo, duration: time.Since(startTime)})
return
}
for idx := range stickerItems { // 遍历单个表情
d.wg.Add(1)
go func(index int) { // 串行下载表情包,并行下载表情包内单个表情
item, ok := stickerItems[index].(map[string]interface{})
if !ok {
p.Send(resultMsg{emoji: randomEmoji(), msg: errGetInfo, duration: time.Since(startTime)})
}
var links []string
staticLink, linkOk := item["url"].(string) // 静态表情
gifLink, gifOk := item["gif_url"].(string) //gif 动态表情
if linkOk {
links = append(links, staticLink)
}
if gifOk {
links = append(links, gifLink)
}
for _, link := range links {
iconName := item["text"].(string)
dotPos := strings.LastIndexByte(link, '.')
isLink := dotPos != -1 && strings.HasPrefix(link, "http") // 有 http 前缀并且有 . 符号的认为是链接
if !isLink {
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(errParseLink, iconName, link), duration: time.Since(startTime)})
continue
}
fileName := path.Join(dirName, iconName+link[dotPos:])
if _, err := os.Stat(fileName); os.IsNotExist(err) {
for i := 1; i <= 3; i++ { // 重试三次
if resp, err := client.Get(link); err == nil {
defer resp.Body.Close()
if img, err := io.ReadAll(resp.Body); err == nil {
os.WriteFile(fileName, img, fs.FileMode(0644))
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(successDownload, fileName), duration: time.Since(startTime)})
break
}
} else {
if i == 3 {
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(errDownloadImg, fileName, err), duration: time.Since(startTime)})
break
}
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(errRetryDownload, i, fileName, err), duration: time.Since(startTime)})
time.Sleep(3 * time.Second)
}
}
} else {
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(fileAlreadyExist, fileName), duration: time.Since(startTime)})
}
}
d.wg.Done()
}(idx)
}
d.wg.Wait()
}
func main() {
p := tea.NewProgram(newModel())
sessData := flag.String("sessdata", "", "你的 SESSDATA")
flag.Parse()
startTime := time.Now()
go func() {
if *sessData == "" && os.Getenv("SESSDATA") == "" {
p.Send(resultMsg{emoji: randomEmoji(), err: fmt.Errorf(errGetCookie), duration: time.Since(startTime)})
return
}
if *sessData == "" {
*sessData = os.Getenv("SESSDATA")
}
initClient(*sessData)
resp, err := client.Get("https://api.bilibili.com/x/emote/setting/panel?business=reply")
if err != nil {
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(errGetEmoji, err), duration: time.Since(startTime)})
return
}
content, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var tmp map[string]interface{}
if err = json.Unmarshal(content, &tmp); err != nil {
p.Send(resultMsg{emoji: randomEmoji(), msg: fmt.Sprintf(errParseEmoji, err), duration: time.Since(startTime)})
}
stickers := (tmp["data"].(map[string]interface{}))["all_packages"].([]interface{})
downloader := Downloader{}
for idx := range stickers {
stickerInfo := stickers[idx].(map[string]interface{})
downloader.Download(p, startTime, stickerInfo)
}
p.Send(resultMsg{emoji: randomEmoji(), quit: true})
}()
if _, err := p.Run(); err != nil {
os.Exit(1)
}
}