diff --git a/README.md b/README.md
index 7056ca964b..54f59afd6b 100644
--- a/README.md
+++ b/README.md
@@ -976,6 +976,14 @@ print("run[CQ:image,file="+j["img"]+"]")
- (机器人回答:您的下一条指令将被记录,在@@every 1m时触发)
- mc服务器订阅拉取
+
+ Movies猫眼电影查询
+
+`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies"`
+
+- [x] 今日电影
+- [x] 预售电影
+
摸鱼
diff --git a/main.go b/main.go
index 34e7130c8a..a260c25857 100644
--- a/main.go
+++ b/main.go
@@ -110,6 +110,7 @@ import (
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver" // Minecraft服务器监控&订阅
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies" // 电影插件
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌
diff --git a/plugin/movies/main.go b/plugin/movies/main.go
new file mode 100644
index 0000000000..b17cbb4631
--- /dev/null
+++ b/plugin/movies/main.go
@@ -0,0 +1,435 @@
+// Package movies 电影查询
+package movies
+
+import (
+ "encoding/json"
+ "image"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/FloatTech/floatbox/file"
+ "github.com/FloatTech/floatbox/web"
+ "github.com/FloatTech/gg"
+ "github.com/FloatTech/imgfactory"
+ "github.com/FloatTech/rendercard"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/FloatTech/zbputils/control"
+ "github.com/FloatTech/zbputils/img/text"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+)
+
+const (
+ apiURL = "https://m.maoyan.com/ajax/"
+ ua = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36"
+)
+
+var (
+ mu sync.RWMutex
+ todayPic = make([][]byte, 2)
+ lasttime time.Time
+ en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
+ DisableOnDefault: false,
+ Brief: "电影查询",
+ Help: "- 今日电影\n" +
+ "- 预售电影",
+ PrivateDataFolder: "movies",
+ })
+)
+
+func init() {
+ en.OnFullMatch("今日电影").SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ if todayPic != nil && time.Since(lasttime) < 12*time.Hour {
+ ctx.SendChain(message.ImageBytes(todayPic[0]))
+ return
+ }
+ lasttime = time.Now()
+ movieComingList, err := getMovieList("今日电影")
+ if err != nil {
+ ctx.SendChain(message.Text("[ERROR]:", err))
+ return
+ }
+ if len(movieComingList) == 0 {
+ ctx.SendChain(message.Text("没有今日电影"))
+ return
+ }
+ pic, err := drawOnListPic(movieComingList)
+ if err != nil {
+ ctx.SendChain(message.Text("[ERROR]:", err))
+ return
+ }
+ todayPic[0] = pic
+ ctx.SendChain(message.ImageBytes(pic))
+ })
+ en.OnFullMatch("预售电影").SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ if todayPic[1] != nil && time.Since(lasttime) < 12*time.Hour {
+ ctx.SendChain(message.ImageBytes(todayPic[1]))
+ return
+ }
+ lasttime = time.Now()
+ movieComingList, err := getMovieList("预售电影")
+ if err != nil {
+ ctx.SendChain(message.Text("[ERROR]:", err))
+ return
+ }
+ if len(movieComingList) == 0 {
+ ctx.SendChain(message.Text("没有预售电影"))
+ return
+ }
+ pic, err := drawComListPic(movieComingList)
+ if err != nil {
+ ctx.SendChain(message.Text("[ERROR]:", err))
+ return
+ }
+ todayPic[1] = pic
+ ctx.SendChain(message.ImageBytes(pic))
+ })
+}
+
+type movieInfo struct {
+ ID int64 `json:"id"` // 电影ID
+ Img string `json:"img"` // 海报
+
+ Nm string `json:"nm"` // 名称
+
+ Dir string `json:"dir"` // 导演
+ Star string `json:"star"` // 演员
+
+ OriLang string `json:"oriLang"` // 原语言
+ Cat string `json:"cat"` // 类型
+
+ Version string `json:"version"` // 电影格式
+ Rt string `json:"rt"` // 上映时间
+
+ ShowInfo string `json:"showInfo"` // 今日上映信息
+ ComingTitle string `json:"comingTitle"` // 预售信息
+
+ Sc float64 `json:"sc"` // 评分
+ Wish int64 `json:"wish"` // 观看人数
+ Watched int64 `json:"watched"` // 观看数
+}
+type movieOnList struct {
+ MovieList []movieInfo `json:"movieList"`
+}
+type comingList struct {
+ MovieList []movieInfo `json:"coming"`
+}
+type movieShow struct {
+ MovieInfo movieInfo `json:"detailMovie"`
+}
+
+type cardInfo struct {
+ Avatar image.Image
+ TopLeftText string
+ BottomLeftText []string
+ RightText string
+ Rank string
+}
+
+func getMovieList(mode string) (movieList []movieInfo, err error) {
+ var data []byte
+ if mode == "今日电影" {
+ data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"movieOnInfoList", "", "GET", ua, nil)
+ if err != nil {
+ return
+ }
+ var parsed movieOnList
+ err = json.Unmarshal(data, &parsed)
+ if err != nil {
+ return
+ }
+ movieList = parsed.MovieList
+ } else {
+ data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"comingList?token=", "", "GET", ua, nil)
+ if err != nil {
+ return
+ }
+ var parsed comingList
+ err = json.Unmarshal(data, &parsed)
+ if err != nil {
+ return
+ }
+ movieList = parsed.MovieList
+ }
+ if len(movieList) == 0 {
+ return
+ }
+ for i, info := range movieList {
+ movieID := strconv.FormatInt(info.ID, 10)
+ data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"detailmovie?movieId="+movieID, "", "GET", ua, nil)
+ if err != nil {
+ return
+ }
+ var movieInfo movieShow
+ err = json.Unmarshal(data, &movieInfo)
+ if err != nil {
+ return
+ }
+ if mode != "今日电影" {
+ movieInfo.MovieInfo.ComingTitle = movieList[i].ComingTitle
+ }
+ movieList[i] = movieInfo.MovieInfo
+ }
+ // 整理数据,进行排序
+ sort.Slice(movieList, func(i, j int) bool {
+ if movieList[i].Sc != movieList[j].Sc {
+ return movieList[i].Sc > movieList[j].Sc
+ }
+ if mode == "今日电影" {
+ return movieList[i].Watched > movieList[j].Watched
+ }
+ return movieList[i].Wish > movieList[j].Wish
+ })
+ return movieList, nil
+}
+func drawOnListPic(lits []movieInfo) (data []byte, err error) {
+ rankinfo := make([]*cardInfo, len(lits))
+
+ wg := &sync.WaitGroup{}
+ wg.Add(len(lits))
+ for i := 0; i < len(lits); i++ {
+ go func(i int) {
+ info := lits[i]
+ defer wg.Done()
+ img, err := avatar(&info)
+ if err != nil {
+ return
+ }
+ movieType := "2D"
+ if info.Version != "" {
+ movieType = info.Version
+ }
+ watched := ""
+ switch {
+ case info.Watched > 100000000:
+ watched = strconv.FormatFloat(float64(info.Watched)/100000000, 'f', 2, 64) + "亿"
+ case info.Watched > 10000:
+ watched = strconv.FormatFloat(float64(info.Watched)/10000, 'f', 2, 64) + "万"
+ default:
+ watched = strconv.FormatInt(info.Watched, 10)
+ }
+ rankinfo[i] = &cardInfo{
+ TopLeftText: info.Nm + " (" + strconv.FormatInt(info.ID, 10) + ")",
+ BottomLeftText: []string{
+ "导演:" + info.Dir,
+ "演员:" + info.Star,
+ "标签:" + info.Cat,
+ "语言: " + info.OriLang + " 类型: " + movieType,
+ "上映时间: " + info.Rt,
+ },
+ RightText: watched + "人已看",
+ Avatar: img,
+ Rank: strconv.FormatFloat(info.Sc, 'f', 1, 64),
+ }
+ }(i)
+ }
+ wg.Wait()
+ fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true)
+ if err != nil {
+ return
+ }
+ img, err := drawRankingCard(fontbyte, "今日电影", rankinfo)
+ if err != nil {
+ return
+ }
+ data, err = imgfactory.ToBytes(img)
+ return
+}
+
+func drawComListPic(lits []movieInfo) (data []byte, err error) {
+ rankinfo := make([]*cardInfo, len(lits))
+
+ wg := &sync.WaitGroup{}
+ wg.Add(len(lits))
+ for i := 0; i < len(lits); i++ {
+ go func(i int) {
+ info := lits[i]
+ defer wg.Done()
+ img, err := avatar(&info)
+ if err != nil {
+ return
+ }
+ movieType := "2D"
+ if info.Version != "" {
+ movieType = info.Version
+ }
+ wish := ""
+ switch {
+ case info.Wish > 100000000:
+ wish = strconv.FormatFloat(float64(info.Wish)/100000000, 'f', 2, 64) + "亿"
+ case info.Wish > 10000:
+ wish = strconv.FormatFloat(float64(info.Wish)/10000, 'f', 2, 64) + "万"
+ default:
+ wish = strconv.FormatInt(info.Wish, 10)
+ }
+ rankinfo[i] = &cardInfo{
+ TopLeftText: info.Nm + " (" + strconv.FormatInt(info.ID, 10) + ")",
+ BottomLeftText: []string{
+ "导演:" + info.Dir,
+ "演员:" + info.Star,
+ "标签:" + info.Cat,
+ "语言: " + info.OriLang + " 类型: " + movieType,
+ "上映时间: " + info.Rt + " 播放时间: " + info.ComingTitle,
+ },
+ RightText: wish + "人期待",
+ Avatar: img,
+ Rank: strconv.Itoa(i + 1),
+ }
+ }(i)
+ }
+ wg.Wait()
+ fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true)
+ if err != nil {
+ return
+ }
+ img, err := drawRankingCard(fontbyte, "预售电影", rankinfo)
+ if err != nil {
+ return
+ }
+ data, err = imgfactory.ToBytes(img)
+ return
+}
+
+func drawRankingCard(fontdata []byte, title string, rankinfo []*cardInfo) (img image.Image, err error) {
+ line := len(rankinfo)
+ const lineh = 130
+ const w = 800
+ h := 64 + (lineh+14)*line + 20 - 14
+ canvas := gg.NewContext(w, h)
+ canvas.SetRGBA255(255, 255, 255, 255)
+ canvas.Clear()
+
+ cardh, cardw := lineh, 770
+ cardspac := 14
+ hspac, wspac := 64.0, 16.0
+ r := 16.0
+
+ wg := &sync.WaitGroup{}
+ wg.Add(line)
+ cardimgs := make([]image.Image, line)
+ for i := 0; i < line; i++ {
+ go func(i int) {
+ defer wg.Done()
+ card := gg.NewContext(w, cardh)
+
+ card.NewSubPath()
+
+ card.MoveTo(wspac+float64(cardh)/2, 0)
+
+ card.LineTo(wspac+float64(cardw)-r, 0)
+ card.DrawArc(wspac+float64(cardw)-r, r, r, gg.Radians(-90), gg.Radians(0))
+ card.LineTo(wspac+float64(cardw), float64(cardh)-r)
+ card.DrawArc(wspac+float64(cardw)-r, float64(cardh)-r, r, gg.Radians(0), gg.Radians(90))
+ card.LineTo(wspac+float64(cardh)/2, float64(cardh))
+ card.DrawArc(wspac+r, float64(cardh)-r, r, gg.Radians(90), gg.Radians(180))
+ card.LineTo(wspac, r)
+ card.DrawArc(wspac+r, r, r, gg.Radians(180), gg.Radians(270))
+
+ card.ClosePath()
+
+ card.ClipPreserve()
+
+ avatar := rankinfo[i].Avatar
+
+ PicH := cardh - 20
+ picW := int(float64(avatar.Bounds().Dx()) * float64(PicH) / float64(avatar.Bounds().Dy()))
+ card.DrawImageAnchored(imgfactory.Size(avatar, picW, PicH).Image(), int(wspac)+10+picW/2, cardh/2, 0.5, 0.5)
+
+ card.ResetClip()
+ card.SetRGBA255(0, 0, 0, 127)
+ card.Stroke()
+
+ card.SetRGBA255(240, 210, 140, 200)
+ card.DrawRoundedRectangle(wspac+float64(cardw-8-250), (float64(cardh)-50)/2, 250, 50, 25)
+ card.Fill()
+ card.SetRGB255(rendercard.RandJPColor())
+ card.DrawRoundedRectangle(wspac+float64(cardw-8-60), (float64(cardh)-50)/2, 60, 50, 25)
+ card.Fill()
+ cardimgs[i] = card.Image()
+ }(i)
+ }
+
+ canvas.SetRGBA255(0, 0, 0, 255)
+ err = canvas.ParseFontFace(fontdata, 32)
+ if err != nil {
+ return
+ }
+ canvas.DrawStringAnchored(title, w/2, 64/2, 0.5, 0.5)
+
+ err = canvas.ParseFontFace(fontdata, 22)
+ if err != nil {
+ return
+ }
+ wg.Wait()
+ for i := 0; i < line; i++ {
+ canvas.DrawImageAnchored(cardimgs[i], w/2, int(hspac)+((cardh+cardspac)*i), 0.5, 0)
+ canvas.DrawStringAnchored(rankinfo[i].TopLeftText, wspac+10+80+10, hspac+float64((cardspac+cardh)*i+cardh*3/16), 0, 0.5)
+ }
+
+ // canvas.SetRGBA255(63, 63, 63, 255)
+ err = canvas.ParseFontFace(fontdata, 14)
+ if err != nil {
+ return
+ }
+ for i := 0; i < line; i++ {
+ for j, text := range rankinfo[i].BottomLeftText {
+ canvas.DrawStringAnchored(text, wspac+10+80+10, hspac+float64((cardspac+cardh)*i+cardh*6/16)+float64(j*16), 0, 0.5)
+ }
+ }
+ canvas.SetRGBA255(0, 0, 0, 255)
+ err = canvas.ParseFontFace(fontdata, 20)
+ if err != nil {
+ return
+ }
+ for i := 0; i < line; i++ {
+ canvas.DrawStringAnchored(rankinfo[i].RightText, w-wspac-8-60-8, hspac+float64((cardspac+cardh)*i+cardh/2), 1, 0.5)
+ }
+
+ canvas.SetRGBA255(255, 255, 255, 255)
+ err = canvas.ParseFontFace(fontdata, 28)
+ if err != nil {
+ return
+ }
+ for i := 0; i < line; i++ {
+ canvas.DrawStringAnchored(rankinfo[i].Rank, w-wspac-8-30, hspac+float64((cardspac+cardh)*i+cardh/2), 0.5, 0.5)
+ }
+
+ img = canvas.Image()
+ return
+}
+
+// avatar 获取电影海报,图片大且多,存本地增加响应速度
+func avatar(movieInfo *movieInfo) (pic image.Image, err error) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ aimgfile := filepath.Join(en.DataFolder(), movieInfo.Nm+"("+strconv.FormatInt(movieInfo.ID, 10)+").jpg")
+ if file.IsNotExist(aimgfile) {
+ err = file.DownloadTo(movieInfo.Img, aimgfile)
+ if err != nil {
+ return urlToImg(movieInfo.Img)
+ }
+ }
+ f, err := os.Open(filepath.Join(file.BOTPATH, aimgfile))
+ if err != nil {
+ return urlToImg(movieInfo.Img)
+ }
+ defer f.Close()
+ pic, _, err = image.Decode(f)
+ return
+}
+
+func urlToImg(url string) (img image.Image, err error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+ img, _, err = image.Decode(resp.Body)
+ return
+}