当我们想下载电影时:
而交给机器人做的话:
go get github.com/PuerkitoBio/goquery
go get github.com/gin-gonic/gin
go get github.com/spf13/viper
├── conf # 配置文件统一存放目录
│ ├── config.yaml # 配置文件
├── config # 专门用来处理配置和配置文件的Go package
│ └── config.go
├── handler # 类似MVC架构中的C,用来读取输入,并将处理流程转发给实际的处理函数,最后返回结果
│ ├── handler.go
├── model #数据模型
│ ├── lbldy.go # 电影资源构造体模型
├── pkg # 引用的包
│ ├── errno # 错误码存放位置
│ │ ├── code.go
│ │ └── errno.go
├── router # 路由相关处理
│ ├── middleware # API服务器用的是Gin Web框架,Gin中间件存放位置
│ │ ├── header.go
│ └── router.go # 路由
├── service # 实际业务处理函数存放位置
│ └── service.go
├── main.go # Go程序唯一入口
common:
#http://www.lbldy.com/ 电影资源接口
lbldy:
#搜索电影接口 http://www.lbldy.com/search/星球大战
search: http://www.lbldy.com/search/
#获取下载链接 http://www.lbldy.com/movie/123.html
download: http://www.lbldy.com/movie/%s.html
server: #服务器配置
runmode: debug # 开发模式, debug, release, test
addr: :6663 # HTTP绑定端口
name: apiserver # API Server的名字
url: http://10.10.87.243:6663 # pingServer函数请求的API服务器的ip:port
max_ping_count: 10 # pingServer函数尝试的次数
package config
import (
"github.com/spf13/viper"
"time"
"os"
"log"
)
// LogInfo 初始化日志配置
func LogInfo() {
file := "./logs/" + time.Now().Format("2006-01-02") + ".log"
logFile, _ := os.OpenFile(file,os.O_RDWR| os.O_CREATE| os.O_APPEND, 0755)
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.SetOutput(logFile)
}
// Init 读取初始化配置文件
func Init() error {
//初始化配置
if err := Config();err != nil{
return err
}
//初始化日志
LogInfo()
return nil
}
// Config viper解析配置文件
func Config() error{
viper.AddConfigPath("conf")
viper.SetConfigName("config")
if err := viper.ReadInConfig();err != nil{
return err
}
return nil
}
package handler
import (
"bytes"
"net/http"
"io/ioutil"
"github.com/gin-gonic/gin"
"aimovie/pkg/errno"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
//返回json 格式
func SendResponse(c *gin.Context,err error,data interface{}){
code,message := errno.DecodeErr(err)
//总是返回http状态ok
c.JSON(http.StatusOK,Response{
Code: code,
Message:message,
Data: data,
})
}
//返回html 格式
func SendResponseHtml(c *gin.Context,err error,data string){
c.Header("Content-Type", "text/html; charset=utf-8")
//总是返回http状态ok
c.String(http.StatusOK,data)
}
//http请求 post
func HttpPost(api string,json string) (string, error) {
jsonStr := []byte(json)
req, err := http.NewRequest("POST", api, bytes.NewBuffer(jsonStr))
if err != nil {
return "", errno.ApiServerError
}
req.Header.Set("Content-Type", "application/json") //使用json格式传参
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", errno.ApiServerError
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if !(resp.StatusCode == 200) {
return "", errno.ApiServerError
}
return string(body), nil
}
//http请求 get
func HttpGet(api string) (string,error){
resp, err := http.Get(api)
if err != nil {
return "", errno.ApiServerError
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errno.ApiServerError
}
if !(resp.StatusCode == 200) {
return "", errno.ApiServerError
}
return string(body), nil
}
//http请求 get 没有解析body
func HttpGetBody(api string) (*http.Response,error){
resp, err := http.Get(api)
return resp,err
}
package model
import (
"io"
"log"
"fmt"
"strings"
"regexp"
"github.com/spf13/viper"
"github.com/PuerkitoBio/goquery"
"aimovie/pkg/errno"
. "aimovie/handler"
)
//龙部落电影获取电影资源
type Media struct {
Name string
Size string
Link string
}
//获取电影id
func SearchLbldy(movie string) (string,error){
api := viper.GetString("common.lbldy.search")
api = fmt.Sprintf(api+"%s",movie)
log.Println("api ", api)
//请求接口,获取电影信息
result, err := HttpGet(api)
if err != nil{
return "", errno.ModelError
}
//正则获取 post-id class="postlist" id="post-64115">
re, err := regexp.Compile("")
if err != nil{
return "", errno.ModelError
}
firstId := re.FindSubmatch([]byte(result)) //find first match case
//log.Println("firstId ", string(firstId[1]))
if len(firstId) == 0 {
return "", errno.ModelError
}
return string(firstId[1]),nil
}
//获取电影下载链接
func DownloadLbldy(movieId string) (string,error){
var ms []Media
api := viper.GetString("common.lbldy.download")
//请求接口,获取电影信息
api = fmt.Sprintf(api,movieId)
//log.Println("api ", api)
result, err := HttpGetBody(api)
//log.Println("err :", err)
if err != nil{
return "", errno.ModelError
}
defer result.Body.Close()
doc, err := goquery.NewDocumentFromReader(io.Reader(result.Body))
if err != nil {
return "", errno.ModelError
}
//正则匹配寻找 a标签链接
doc.Find("p").Each(func(i int, selection *goquery.Selection) {
name := selection.Find("a").Text()
link, _ := selection.Find("a").Attr("href")
if strings.HasPrefix(link, "ed2k") || strings.HasPrefix(link, "magnet") || strings.HasPrefix(link, "thunder") {
m := Media{
Name: name,
Link: link,
}
ms = append(ms, m)
}
})
message := ConvertMsg(ms)
return message,nil
}
//数组转换字符串
func ConvertMsg(ms []Media) string{
ret := "龙部落电影资源列表
"
for i, m := range ms {
ret += fmt.Sprintf("*%s*
```%s```
", m.Name, m.Link,m.Link)
//when results are too large, we split it.
if i%4 == 0 && i < len(ms)-1 && i > 0 {
ret += fmt.Sprintf("*切割部分 %d*
", i/4+1)
}
}
return ret
}
建立文件夹和文件 aimovie/pkg/errno/code.go ,code.go 内容如下:
package errno
var (
// Common errors
OK = &Errno{Code: 0, Message: "OK"}
VALUEERROR = &Errno{Code: -1, Message: "输入错误"}
InternalServerError = &Errno{Code: 10001, Message: "服务器错误"}
ApiServerError = &Errno{Code: 20001, Message: "接口服务器错误"}
ModelError = &Errno{Code: 30001, Message: "聊天模型错误"}
)
建立文件夹和文件 aimovie/pkg/errno/errno.go ,errno.go 内容如下:
package errno
import "fmt"
type Errno struct {
Code int
Message string
}
//返回错误信息
func (err Errno) Error() string{
return err.Message
}
//设置 Err 结构体
type Err struct {
Code int
Message string
Err error
}
//声明构造体
func New(errno *Errno,err error) *Err{
return &Err{Code:errno.Code,Message:errno.Message,Err:err}
}
//添加错误信息
func (err *Err) Add(message string) error{
err.Message += " " + message
return err
}
//添加指定格式的错误信息
func (err * Err) Addf(format string,args...interface{}) error{
err.Message += " " + fmt.Sprintf(format,args...)
return err
}
//拼接错误信息字符串
func (err *Err) Error() string{
return fmt.Sprintf("Err - code: %d, message: %s, error: %s",err.Code,err.Message,err.Err)
}
// 解析 错误信息, 返回字符串
func DecodeErr(err error) (int,string){
if err == nil{
return OK.Code,OK.Message
}
switch typed := err.(type) {
case *Err:
return typed.Code,typed.Message
case *Errno:
return typed.Code,typed.Message
default:
}
return InternalServerError.Code,err.Error()
}
建立文件夹和文件 aimovie/router/middleware/header.go ,header.go 内容如下:
package middleware
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
//无缓存头部中间件 ,
//要来防止客户端获取已经缓存的响应信息
func NoCache(c *gin.Context){
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Next()
}
//选项中间件
//要来给预请求 终止并退出中间件 ,链接并结束请求
func Options(c *gin.Context){
if c.Request.Method != "OPTIONS"{
c.Next()
}else{
c.Header("Access-Control-Allow-Origin","*")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Content-Type", "application/json")
c.AbortWithStatus(200)
}
}
//安全中间件
//要来保障数据安全的头部
func Secure(c *gin.Context){
c.Header("Access-Control-Allow-Origin", "*")
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
if c.Request.TLS != nil {
c.Header("Strict-Transport-Security", "max-age=31536000")
}
//也可以考虑添加一个安全代理的头部
//c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}
建立文件夹和文件 aimovie/router/router.go ,router.go 内容如下:
package router
import (
"net/http"
"github.com/gin-gonic/gin"
"aimovie/service"
"aimovie/router/middleware"
)
//初始化路由
func InitRouter(g *gin.Engine){
middlewares := []gin.HandlerFunc{}
//中间件
g.Use(gin.Recovery())
g.Use(middleware.NoCache)
g.Use(middleware.Options)
g.Use(middleware.Secure)
g.Use(middlewares...)
//404处理
g.NoRoute(func(c *gin.Context){
c.String(http.StatusNotFound,"该路径不存在")
})
//健康检查中间件
g.GET("/",service.Index)//主页
g.GET("/movie",service.SearchMovie)//获取电影下载链接
}
建立文件夹和文件 aimovie/service/service.go ,service.go 内容如下:
package service
import (
"sync"
"github.com/gin-gonic/gin"
. "aimovie/handler"
"aimovie/model"
"aimovie/pkg/errno"
)
//首页
func Index(c *gin.Context){
html := `
hello world
hello world
`
SendResponseHtml(c,nil,html)
}
//获取电影下载链接
func SearchMovie(c *gin.Context){
//获取聊天信息
movie := c.Query("movie")
if movie == ""{
SendResponse(c,errno.VALUEERROR,nil)
return
}
results:= make(chan string)
go DownloadMovie(results,movie)
html := `
电影资源
`
for {
msg, ok := <-results //retrive result from channel
if !ok {
return
}
html = html+msg
SendResponseHtml(c,nil,html)
}
}
func DownloadMovie(results chan<- string,movie string) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
results <- getResourceFromLbldy(movie) //使用龙部落电影资源
}()
wg.Wait()
close(results)
}
func getResourceFromLbldy(movie string) (string){
//获取电影id
movieId,_ := model.SearchLbldy(movie)
//获取下载链接
movieLink,_ := model.DownloadLbldy(movieId)
return movieLink
}
建立文件夹和文件 aimovie/main.go ,main.go 内容如下:
package main
import (
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"log"
"aimovie/config"
"aimovie/router"
)
func main() {
if err := config.Init();err != nil{
panic(err)
}
//设置gin模式
gin.SetMode(viper.GetString("common.server.runmode"))
//创建一个gin引擎
g := gin.New()
router.InitRouter(g)
log.Printf("开始监听服务器地址: %s\n", viper.GetString("common.server.url"))
if err := g.Run(viper.GetString("common.server.addr"));err != nil {
log.Fatal("监听错误:", err)
}
}
初始化包
[root@localhost aimovie]# go mod init aimovie
go: creating new go.mod: module aimovie
启动服务器
[root@localhost aimovie]# go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> aimovie/service.Index (5 handlers)
[GIN-debug] GET /movie --> aimovie/service.SearchMovie (5 handlers)
[GIN-debug] Listening and serving HTTP on :6663
直接使用浏览器访问:
参考:https://www.jianshu.com/p/81d155c3b065