├── GO_VERSION ├── static ├── style │ ├── config.css │ ├── config.less │ ├── common.css │ ├── common.less │ ├── mobile.css │ ├── mobile.less │ ├── handBook.css │ ├── handBook.less │ └── home.css ├── img │ ├── logo │ │ ├── logo.png │ │ ├── 溜忙logo.png │ │ ├── favicon_128.ico │ │ └── favicon_64.ico │ └── loading │ │ └── loading.jpg └── js │ └── emoji.js ├── utils ├── SpiderTask.go ├── JsonUtil.go ├── spider │ ├── SpiderTaskPolicy.go │ ├── CategoriesStr.go │ └── SpiderApi.go ├── Cron.go ├── RedisUtil.go ├── Pagination.go ├── Dingrobot.go └── Spider.go ├── model ├── paging.go ├── post.go ├── version.go ├── error.go ├── movie.go └── user.go ├── Dockerfile.API ├── Dockerfile.UI ├── .gitignore ├── Makefile ├── templates ├── components │ ├── footer.html │ ├── notification.html │ ├── header.html │ ├── examine.html │ ├── classification.html │ ├── header_createBlog.html │ ├── pagination.html │ ├── login.html │ ├── nav.html │ ├── header_ViewHandBook.html │ └── comment.html └── movie │ ├── movieDetail.html │ └── movie.html ├── error ├── notfound.go └── handler.go ├── runner └── runner.go ├── docker-compose.UI.yml ├── docker-compose.API.yml ├── .vscode └── launch.json ├── .github └── FUNDING.yml ├── config ├── app.go.backup └── config.go ├── mode ├── mode_test.go └── mode.go ├── test ├── asserts.go └── testdb │ └── database.go ├── README.md ├── test.http ├── api ├── internalutil.go ├── user_test.go ├── user.go ├── html.go ├── movie.go └── post.go ├── database ├── post_test.go ├── database.go ├── database_test.go ├── user_test.go ├── post.go ├── user.go └── movie.go ├── docker-compose-express-mongo.yml ├── LICENSE ├── go.mod ├── router └── router.go ├── app.go └── .drone.yml /GO_VERSION: -------------------------------------------------------------------------------- 1 | 1.12.0 2 | -------------------------------------------------------------------------------- /static/style/config.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenquan321/movie-spider/HEAD/static/img/logo/logo.png -------------------------------------------------------------------------------- /static/img/logo/溜忙logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenquan321/movie-spider/HEAD/static/img/logo/溜忙logo.png -------------------------------------------------------------------------------- /static/img/loading/loading.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenquan321/movie-spider/HEAD/static/img/loading/loading.jpg -------------------------------------------------------------------------------- /static/img/logo/favicon_128.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenquan321/movie-spider/HEAD/static/img/logo/favicon_128.ico -------------------------------------------------------------------------------- /static/img/logo/favicon_64.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhenquan321/movie-spider/HEAD/static/img/logo/favicon_64.ico -------------------------------------------------------------------------------- /utils/SpiderTask.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type SpiderTask interface { 4 | Start() 5 | PageDetail(id string) 6 | } 7 | -------------------------------------------------------------------------------- /static/style/config.less: -------------------------------------------------------------------------------- 1 | @mainColor: #e59233; 2 | @mainColorHover: #c67e2b; 3 | @Bghover:#fafafa; 4 | @boxShadow:0 1px 2px 0 rgba(0, 0, 0, 0.05); -------------------------------------------------------------------------------- /utils/JsonUtil.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import jsoniter "github.com/json-iterator/go" 4 | 5 | var Json = jsoniter.ConfigCompatibleWithStandardLibrary 6 | -------------------------------------------------------------------------------- /model/paging.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Paging Model 4 | type Paging struct { 5 | Skip *int64 6 | Limit *int64 7 | SortKey string 8 | SortVal int 9 | Condition interface{} 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile.API: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-glibc:glibc-2.29 2 | 3 | WORKDIR /bin 4 | ADD release/linux/amd64/api-ten-minutes /bin/ 5 | ADD config.yml /bin/ 6 | 7 | EXPOSE 6868 8 | ENTRYPOINT ["/bin/api-ten-minutes"] 9 | -------------------------------------------------------------------------------- /Dockerfile.UI: -------------------------------------------------------------------------------- 1 | FROM node:10.15.1-alpine 2 | 3 | RUN apk add --no-cache tini && npm install http-server -g && mkdir /ten 4 | 5 | WORKDIR /ten 6 | 7 | COPY app/build . 8 | 9 | EXPOSE 3000 10 | 11 | ENTRYPOINT ["/sbin/tini", "--"] 12 | CMD [ "http-server", "-p", "3000" ] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | config.yml 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | .DS_Store 16 | un 17 | release -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_GO_BUILD=go build -mod=readonly -a -installsuffix cgo -ldflags "$$LD_FLAGS" 2 | 3 | build_linux_amd64: 4 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ${DOCKER_GO_BUILD} -v -o release/linux/amd64/api-ten-minutes 5 | 6 | docker: 7 | docker build -t lotteryjs/api-ten-minutes . 8 | 9 | test: 10 | go test -v . -------------------------------------------------------------------------------- /utils/spider/SpiderTaskPolicy.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | import ( 4 | "movie_spider/utils" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // 定义 mod 的映射关系 10 | var spiderModMap = map[string]utils.SpiderTask{ 11 | "api": &SpiderApi{}, 12 | "WebPage": &utils.Spider{}} 13 | 14 | func Create() utils.SpiderTask { 15 | 16 | mod := viper.GetString(`app.spider_mod`) 17 | 18 | return spiderModMap[mod] 19 | } 20 | -------------------------------------------------------------------------------- /templates/components/footer.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /error/notfound.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import ( 4 | "net/http" 5 | 6 | "movie_spider/model" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // NotFound creates a gin middleware for handling page not found. 12 | func NotFound() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | c.JSON(http.StatusNotFound, &model.Error{ 15 | Error: http.StatusText(http.StatusNotFound), 16 | ErrorCode: http.StatusNotFound, 17 | ErrorDescription: "page not found", 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "movie_spider/config" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // Run starts the http server 14 | func Run(engine *gin.Engine, conf *config.Configuration) { 15 | var httpHandler http.Handler = engine 16 | 17 | addr := fmt.Sprintf("%s:%d", conf.Server.ListenAddr, conf.Server.Port) 18 | fmt.Println("Started Listening for plain HTTP connection on " + addr) 19 | log.Fatal(http.ListenAndServe(addr, httpHandler)) 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.UI.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | ui-ten-minutes: 5 | image: lotteryjs/ui-ten-minutes 6 | networks: 7 | - web 8 | logging: 9 | options: 10 | max-size: "100k" 11 | max-file: "3" 12 | labels: 13 | - "traefik.docker.network=web" 14 | - "traefik.enable=true" 15 | - "traefik.basic.frontend.rule=Host:ten-minutes.lotteryjs.com" 16 | - "traefik.basic.port=3000" 17 | - "traefik.basic.protocol=http" 18 | 19 | networks: 20 | web: 21 | external: true 22 | -------------------------------------------------------------------------------- /docker-compose.API.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | api-ten-minutes: 5 | image: lotteryjs/api-ten-minutes 6 | networks: 7 | - web 8 | logging: 9 | options: 10 | max-size: "100k" 11 | max-file: "3" 12 | labels: 13 | - "traefik.docker.network=web" 14 | - "traefik.enable=true" 15 | - "traefik.basic.frontend.rule=Host:api-ten-minutes.lotteryjs.com" 16 | - "traefik.basic.port=6868" 17 | - "traefik.basic.protocol=http" 18 | 19 | networks: 20 | web: 21 | external: true 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /model/post.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | // The Post holds 8 | type Post struct { 9 | ID primitive.ObjectID `bson:"_id" json:"id"` 10 | UserID primitive.ObjectID `bson:"userId" json:"userId"` 11 | Title string `bson:"title" json:"title"` 12 | Body string `bson:"body" json:"body"` 13 | } 14 | 15 | // New is an instance 16 | func (p *Post) New() *Post { 17 | return &Post{ 18 | ID: primitive.NewObjectID(), 19 | UserID: p.UserID, 20 | Title: p.Title, 21 | Body: p.Body, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /model/version.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // VersionInfo Model 4 | // 5 | // swagger:model VersionInfo 6 | type VersionInfo struct { 7 | // The current version. 8 | // 9 | // required: true 10 | // example: 5.2.6 11 | Version string `json:"version"` 12 | // The git commit hash on which this binary was built. 13 | // 14 | // required: true 15 | // example: ae9512b6b6feea56a110d59a3353ea3b9c293864 16 | Commit string `json:"commit"` 17 | // The date on which this binary was built. 18 | // 19 | // required: true 20 | // example: 2018-02-27T19:36:10.5045044+01:00 21 | BuildDate string `json:"buildDate"` 22 | } 23 | -------------------------------------------------------------------------------- /utils/Cron.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | "github.com/spf13/viper" 6 | "log" 7 | ) 8 | 9 | func TimingSpider(cmd func()) { 10 | 11 | log.Println("cron TimingSpider start:") 12 | 13 | // v3 用法 干 14 | c := cron.New(cron.WithSeconds()) 15 | 16 | // 每天定时执行的条件 17 | spec := viper.GetString(`cron.timing_spider`) 18 | 19 | c.AddFunc(spec, func() { 20 | //go StartSpider() 21 | //go spider.StartApi() 22 | cmd() 23 | log.Println("corn Spider ing:") 24 | }) 25 | 26 | go c.Start() 27 | 28 | // 关闭计划任务, 但是不能关闭已经在执行中的任务. 29 | // defer c.Stop() 30 | 31 | // 阻塞 32 | //select {} 33 | } 34 | -------------------------------------------------------------------------------- /model/error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Error Model 4 | // 5 | // The Error contains error relevant information. 6 | // 7 | // swagger:model Error 8 | type Error struct { 9 | // The general error message 10 | // 11 | // required: true 12 | // example: Unauthorized 13 | Error string `json:"error"` 14 | // The http error code. 15 | // 16 | // required: true 17 | // example: 401 18 | ErrorCode int `json:"errorCode"` 19 | // The http error code. 20 | // 21 | // required: true 22 | // example: you need to provide a valid access token or user credentials to access this api 23 | ErrorDescription string `json:"errorDescription"` 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | 13 | -------------------------------------------------------------------------------- /utils/RedisUtil.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/go-redis/redis/v7" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // 声明一个全局的redisdb变量 9 | var RedisDB *redis.Client 10 | 11 | func InitRedisDB() (err error) { 12 | RedisDB = redis.NewClient(&redis.Options{ 13 | Addr: viper.GetString(`redis.addr`) + ":" + viper.GetString(`redis.port`), 14 | Password: viper.GetString(`redis.password`), // no password set 15 | DB: viper.GetInt(`redis.db`), // use default DB 16 | }) 17 | 18 | _, err = RedisDB.Ping().Result() 19 | if err != nil { 20 | panic(err) 21 | } 22 | return nil 23 | } 24 | 25 | func CloseRedisDB() error { 26 | return RedisDB.Close() 27 | } 28 | -------------------------------------------------------------------------------- /utils/Pagination.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Page struct { 4 | PageNo int `json:"page_no"` 5 | PageSize int `json:"page_size"` 6 | TotalPage int `json:"total_page"` 7 | TotalCount int `json:"total_count"` 8 | FirstPage bool `json:"first_page"` 9 | LastPage bool `json:"last_page"` 10 | List interface{} `json:"list"` 11 | } 12 | 13 | // 分页工具类 14 | func PageUtil(count int, pageNo int, pageSize int, list interface{}) Page { 15 | tp := count / pageSize 16 | if count%pageSize > 0 { 17 | tp = count/pageSize + 1 18 | } 19 | return Page{PageNo: pageNo, PageSize: pageSize, TotalPage: tp, TotalCount: count, FirstPage: pageNo == 1, LastPage: pageNo == tp, List: list} 20 | } 21 | -------------------------------------------------------------------------------- /templates/components/notification.html: -------------------------------------------------------------------------------- 1 | <% if (req && req.flash){ %> 2 |
3 | <% if (req &&req.flash.success){ %> 4 |
5 | <%= req.flash.success %> 6 |
7 | <% } else if (req &&req.flash.warning) { %> 8 |
9 | <%= req.flash.warning %> 10 |
11 | <% } %> 12 | 13 |
14 | <% } %> 15 | 23 | -------------------------------------------------------------------------------- /config/app.go.backup: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /** 4 | 参数说明: 5 | app.spider_path: 爬虫路由 6 | app.spider_path_name: 爬虫路由名称 7 | app.debug_path: debug的路由 8 | app.debug_path_name: debug的路由名称 9 | cron.timing_spider: 定时爬虫的CRON表达式 10 | ding.access_token: 钉钉机器人token 11 | */ 12 | var AppJsonConfig = []byte(` 13 | { 14 | "app": { 15 | "port": ":8899", 16 | "spider_path": "/movies-spider", 17 | "spider_path_name": "MoviesSpider", 18 | "debug_path": "/debug", 19 | "debug_path_name": "Debug", 20 | "spider_mod": "api" 21 | }, 22 | "redis": { 23 | "port": "6379", 24 | "addr": "localhost", 25 | "password": "", 26 | "db": 10 27 | }, 28 | "cron": { 29 | "timing_spider": "0 0 1 * * ?" 30 | }, 31 | "ding": { 32 | "access_token": "" 33 | } 34 | } 35 | `) 36 | -------------------------------------------------------------------------------- /mode/mode_test.go: -------------------------------------------------------------------------------- 1 | package mode 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDevMode(t *testing.T) { 11 | Set(Dev) 12 | assert.Equal(t, Get(), Dev) 13 | assert.True(t, IsDev()) 14 | assert.Equal(t, gin.Mode(), gin.DebugMode) 15 | } 16 | 17 | func TestTestDevMode(t *testing.T) { 18 | Set(TestDev) 19 | assert.Equal(t, Get(), TestDev) 20 | assert.True(t, IsDev()) 21 | assert.Equal(t, gin.Mode(), gin.TestMode) 22 | } 23 | 24 | func TestProdMode(t *testing.T) { 25 | Set(Prod) 26 | assert.Equal(t, Get(), Prod) 27 | assert.False(t, IsDev()) 28 | assert.Equal(t, gin.Mode(), gin.ReleaseMode) 29 | } 30 | 31 | func TestInvalidMode(t *testing.T) { 32 | assert.Panics(t, func() { 33 | Set("asdasda") 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /test/asserts.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http/httptest" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // BodyEquals asserts the content from the response recorder with the encoded json of the provided instance. 12 | func BodyEquals(t assert.TestingT, obj interface{}, recorder *httptest.ResponseRecorder) { 13 | bytes, err := ioutil.ReadAll(recorder.Body) 14 | assert.Nil(t, err) 15 | actual := string(bytes) 16 | 17 | JSONEquals(t, obj, actual) 18 | } 19 | 20 | // JSONEquals asserts the content of the string with the encoded json of the provided instance. 21 | func JSONEquals(t assert.TestingT, obj interface{}, expected string) { 22 | bytes, err := json.Marshal(obj) 23 | assert.Nil(t, err) 24 | objJSON := string(bytes) 25 | 26 | assert.JSONEq(t, expected, objJSON) 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 电影爬虫 2 | 3 | ### [😀 点击访问电影 https://lmongo.com/view/movie](https://lmongo.com/view/movie) 4 | 5 | #### Traefik + Docker Deploy 6 | 7 | [golang](https://golang.org/) v1.12.x + [mongo-go-driver](https://github.com/mongodb/mongo-go-driver) v1.x + [gin](https://github.com/gin-gonic/gin) v1.3.x + [mongodb](https://www.mongodb.com/) v4.0.6 + [JSONPlaceholder](http://jsonplaceholder.typicode.com/) 8 | 9 | [使用 Docker](https://github.com/Kirk-Wang/Hello-Gopher/tree/master/mongo) 10 | 11 | ##### 运行 Dev 12 | ```sh 13 | # api 14 | go run . 15 | # app 16 | ``` 17 | 18 | ##### 发布 Pro 19 | ```sh 20 | 21 | go build app.go 22 | 23 | nohup ./app /dev/null & 24 | 25 | ``` 26 | 27 | ##### redis 28 | - 启动: 29 | service redisd start 30 | - 关闭: 31 | service redisd stop 32 | - 查看 33 | ps -aux | grep redis 34 | 35 | #### mac -redis 36 | - redis-server 37 | -------------------------------------------------------------------------------- /mode/mode.go: -------------------------------------------------------------------------------- 1 | package mode 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | const ( 6 | // Dev for development mode 7 | Dev = "dev" 8 | // Prod for production mode 9 | Prod = "prod" 10 | // TestDev used for tests 11 | TestDev = "testdev" 12 | ) 13 | 14 | var mode = Dev 15 | 16 | // Set sets the new mode. 17 | func Set(newMode string) { 18 | mode = newMode 19 | updateGinMode() 20 | } 21 | 22 | // Get returns the current mode. 23 | func Get() string { 24 | return mode 25 | } 26 | 27 | // IsDev returns true if the current mode is dev mode. 28 | func IsDev() bool { 29 | return Get() == Dev || Get() == TestDev 30 | } 31 | 32 | func updateGinMode() { 33 | switch Get() { 34 | case Dev: 35 | gin.SetMode(gin.DebugMode) 36 | case TestDev: 37 | gin.SetMode(gin.TestMode) 38 | case Prod: 39 | gin.SetMode(gin.ReleaseMode) 40 | default: 41 | panic("unknown mode") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test.http: -------------------------------------------------------------------------------- 1 | 2 | ### typicode users 3 | GET http://jsonplaceholder.typicode.com/users?_end=5&_order=DESC&_sort=id&_start=0 4 | 5 | 6 | ### local 分页获取用户列表 7 | GET http://localhost:6868/users?_end=5&_order=DESC&_sort=id&_start=0 8 | 9 | ### local 获取用户,ID 不正确 10 | GET http://localhost:6868/users?id=11 11 | 12 | ### local 获取用户,ID 正确 13 | GET http://localhost:6868/users?id=5c933ae7a49cac27417def6f&id=5c933ae7a49cac27417def70 14 | 15 | ### local 删除用户,ID 正确 16 | DELETE http://localhost:6868/users/5c99bd941ba7b2304ad8c52b 17 | 18 | ### local 分页获取文章列表 19 | GET http://localhost:6868/posts?_end=5&_order=DESC&_sort=id&_start=0&userId=5c938131ca447e20e7b66974 20 | 21 | ### local 获取文章,ID 不正确 22 | GET http://localhost:6868/posts/11 23 | 24 | ### local 获取文章,ID 正确 25 | GET http://localhost:6868/posts/5c92e6199929adef73bceea1 26 | 27 | ### local 删除文章,ID 正确 28 | DELETE http://localhost:6868/posts/5c98678fbf0b9c5d8699e587 29 | 30 | -------------------------------------------------------------------------------- /api/internalutil.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "go.mongodb.org/mongo-driver/bson/primitive" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func withID(ctx *gin.Context, name string, f func(id primitive.ObjectID)) { 11 | if id, err := primitive.ObjectIDFromHex(ctx.Param(name)); err == nil { 12 | f(id) 13 | } else { 14 | ctx.AbortWithError(400, errors.New("invalid id")) 15 | } 16 | } 17 | 18 | func withIDs(ctx *gin.Context, name string, f func(id []primitive.ObjectID)) { 19 | ids, b := ctx.GetQueryArray(name) 20 | objectIds := []primitive.ObjectID{} 21 | abort := errors.New("invalid id") 22 | if b { 23 | for _, id := range ids { 24 | if objID, err := primitive.ObjectIDFromHex(id); err == nil { 25 | objectIds = append(objectIds, objID) 26 | } else { 27 | ctx.AbortWithError(400, abort) 28 | } 29 | } 30 | f(objectIds) 31 | } else { 32 | ctx.AbortWithError(400, abort) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/components/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.title}} 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{template "nav.html" .}} 20 |
-------------------------------------------------------------------------------- /database/post_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "movie_spider/model" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | func (s *DatabaseSuite) TestCreatePost() { 11 | s.db.DB.Collection("posts").Drop(nil) 12 | 13 | user := s.db.GetUserByName("Graham") 14 | 15 | article := (&model.Post{ 16 | UserID: user.ID, 17 | Title: "title1", 18 | Body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto", 19 | }).New() 20 | 21 | s.db.CreatePost(article) 22 | post := s.db.GetPostByID(article.ID) 23 | 24 | assert.Equal(s.T(), post, article) 25 | } 26 | 27 | func (s *DatabaseSuite) TestCountPost() { 28 | assert.Equal(s.T(), "1", s.db.CountPost(nil)) 29 | } 30 | 31 | func (s *DatabaseSuite) TestGetPostByID() { 32 | id, _ := primitive.ObjectIDFromHex("5cc5ca2f6a670dd59ea3a590") 33 | post := s.db.GetPostByID(id) 34 | assert.Equal(s.T(), "title1", post.Title) 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose-express-mongo.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | mongo: 6 | image: mongo:4.0.6 7 | restart: always 8 | networks: 9 | - web 10 | ports: 11 | - 27017:27017 12 | volumes: 13 | - ./data:/data/db 14 | environment: 15 | MONGO_INITDB_ROOT_USERNAME: example 16 | MONGO_INITDB_ROOT_PASSWORD: example 17 | 18 | mongo-express: 19 | image: mongo-express 20 | restart: always 21 | networks: 22 | - web 23 | logging: 24 | options: 25 | max-size: "100k" 26 | max-file: "3" 27 | labels: 28 | - "traefik.docker.network=web" 29 | - "traefik.enable=true" 30 | - "traefik.basic.frontend.rule=Host:mongo-express.example.com" 31 | - "traefik.basic.port=8081" 32 | - "traefik.basic.protocol=http" 33 | environment: 34 | ME_CONFIG_BASICAUTH_USERNAME: example 35 | ME_CONFIG_BASICAUTH_PASSWORD: example 36 | ME_CONFIG_MONGODB_ADMINUSERNAME: example 37 | ME_CONFIG_MONGODB_ADMINPASSWORD: example 38 | 39 | networks: 40 | web: 41 | external: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 jmattheis & lotteryjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /api/user_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "movie_spider/mode" 9 | "movie_spider/test/testdb" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | func TestUserSuite(t *testing.T) { 17 | suite.Run(t, new(UserSuite)) 18 | } 19 | 20 | type UserSuite struct { 21 | suite.Suite 22 | db *testdb.Database 23 | a *UserAPI 24 | ctx *gin.Context 25 | recorder *httptest.ResponseRecorder 26 | } 27 | 28 | func (s *UserSuite) BeforeTest(suiteName, testName string) { 29 | mode.Set(mode.TestDev) 30 | s.recorder = httptest.NewRecorder() 31 | s.ctx, _ = gin.CreateTestContext(s.recorder) 32 | s.db = testdb.NewDB(s.T()) 33 | s.a = &UserAPI{DB: s.db} 34 | } 35 | func (s *UserSuite) AfterTest(suiteName, testName string) { 36 | s.db.Close() 37 | } 38 | 39 | func (s *UserSuite) Test_GetUsers() { 40 | s.db.TenDatabase.DB.Collection("users").Drop(nil) 41 | 42 | for i := 1; i <= 5; i++ { 43 | s.db.NewUser(fmt.Sprintf("Big Brother_%d", i)) 44 | } 45 | assert.Equal(s.T(), 1, 1) 46 | // s.a.GetUsers(s.ctx) 47 | // assert.Equal(s.T(), 200, s.recorder.Code) 48 | } 49 | -------------------------------------------------------------------------------- /static/js/emoji.js: -------------------------------------------------------------------------------- 1 | var emoji = { 2 | "+1": "👍", 3 | "-1": "👎", 4 | "confounded": "😖", 5 | "confused": "😕", 6 | "yum": "😋", 7 | "crossed_fingers": "🤞", 8 | "disappointed": "😞", 9 | "disappointed_relieved": "😥", 10 | "expressionless": "😑", 11 | "drooling_face": "🤤", 12 | "face_with_head_bandage": "🤕", 13 | "face_with_thermometer": "🤒", 14 | "facepunch": "👊", 15 | "fist": "✊", 16 | "fist_left": "🤛", 17 | "fist_oncoming": "👊", 18 | "fist_raised": "✊", 19 | "fist_right": "🤜", 20 | "flushed": "😳", 21 | "frowning": "😦", 22 | "grimacing": "😬", 23 | "grin": "😁", 24 | "grinning": "😀", 25 | "kissing_closed_eyes": "😚", 26 | "kissing_heart": "😘", 27 | "nerd_face": "🤓", 28 | "kissing_smiling_eyes": "😙", 29 | "relieved": "😌", 30 | "rofl": "🤣", 31 | "roll_eyes": "🙄", 32 | "scream": "😱", 33 | "sleeping": "😴", 34 | "sleepy": "😪", 35 | "slightly_frowning_face": "🙁", 36 | "slightly_smiling_face": "🙂", 37 | "smiley": "😃", 38 | "sneezing_face": "🤧", 39 | "smirk": "😏", 40 | "stuck_out_tongue_closed_eyes": "😝", 41 | "stuck_out_tongue_winking_eye": "😜", 42 | "sweat": "😓", 43 | "sweat_drops": "💦", 44 | "sweat_smile": "😅", 45 | "upside_down_face": "🙃", 46 | "zipper_mouth_face": "🤐", 47 | "unamused": "😒", 48 | }; -------------------------------------------------------------------------------- /templates/components/examine.html: -------------------------------------------------------------------------------- 1 | 37 |
38 | 39 | 审核中~ 40 |
41 | 42 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.mongodb.org/mongo-driver/mongo" 8 | "go.mongodb.org/mongo-driver/mongo/options" 9 | "go.mongodb.org/mongo-driver/mongo/readpref" 10 | ) 11 | 12 | // New creates a new wrapper for the mongo-go-driver. 13 | func New(connection, dbname,username,password string) (*TenDatabase, error) { 14 | opts := &options.ClientOptions{} 15 | opts.SetAuth(options.Credential{ 16 | AuthMechanism: "SCRAM-SHA-1", 17 | AuthSource: "users_db", 18 | Username: username, 19 | Password: password}) 20 | 21 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 22 | defer cancel() 23 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(connection), opts) 24 | if err != nil { 25 | return nil, err 26 | } 27 | ctxping, cancel := context.WithTimeout(context.Background(), 5*time.Second) 28 | defer cancel() 29 | err = client.Ping(ctxping, readpref.Primary()) 30 | if err != nil { 31 | return nil, err 32 | } 33 | db := client.Database(dbname) 34 | return &TenDatabase{DB: db, Client: client, Context: ctx}, nil 35 | } 36 | 37 | // TenDatabase is a wrapper for the mongo-go-driver. 38 | type TenDatabase struct { 39 | DB *mongo.Database 40 | Client *mongo.Client 41 | Context context.Context 42 | } 43 | 44 | // Close closes the mongo-go-driver connection. 45 | func (d *TenDatabase) Close() { 46 | d.Client.Disconnect(d.Context) 47 | } 48 | -------------------------------------------------------------------------------- /test/testdb/database.go: -------------------------------------------------------------------------------- 1 | package testdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "movie_spider/model" 7 | 8 | "movie_spider/database" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // Database is the wrapper for the gorm database with sleek helper methods. 14 | type Database struct { 15 | *database.TenDatabase 16 | t *testing.T 17 | } 18 | 19 | // NewDB creates a new test db instance. 20 | func NewDB(t *testing.T) *Database { 21 | db, err := database.New("mongodb://root:123456@localhost:27017", "tenapi") 22 | assert.Nil(t, err) 23 | assert.NotNil(t, db) 24 | return &Database{TenDatabase: db, t: t} 25 | } 26 | 27 | // NewUser creates a user and returns the user. 28 | func (d *Database) NewUser(name string) *model.User { 29 | return d.NewUserWithName(name) 30 | } 31 | 32 | // NewUserWithName creates a user with a name and returns the user. 33 | func (d *Database) NewUserWithName(name string) *model.User { 34 | user := (&model.User{ 35 | Name: name, 36 | UserName: "Bret", 37 | Email: "Sincere@april.biz", 38 | Address: model.UserAddress{ 39 | Street: "Kulas Light", 40 | Suite: "Apt. 556", 41 | City: "Gwenborough", 42 | Zipcode: "92998-3874", 43 | Geo: model.UserAddressGeo{ 44 | Lat: "-37.3159", 45 | Lng: "81.1496", 46 | }, 47 | }, 48 | Phone: "1-770-736-8031 x56442", 49 | Website: "hildegard.org", 50 | Company: model.UserCompany{ 51 | Name: "Romaguera-Crona", 52 | CatchPhrase: "Multi-layered client-server neural-net", 53 | BS: "harness real-time e-markets", 54 | }, 55 | }).New() 56 | d.CreateUser(user) 57 | return user 58 | } 59 | -------------------------------------------------------------------------------- /database/database_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "movie_spider/model" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | ) 13 | 14 | func TestDatabaseSuite(t *testing.T) { 15 | suite.Run(t, new(DatabaseSuite)) 16 | } 17 | 18 | type DatabaseSuite struct { 19 | suite.Suite 20 | db *TenDatabase 21 | } 22 | 23 | func (s *DatabaseSuite) BeforeTest(suiteName, testName string) { 24 | s.T().Log("--BeforeTest--") 25 | db, _ := New("mongodb://root:123456@localhost:27017", "tenapi") 26 | s.db = db 27 | } 28 | 29 | func (s *DatabaseSuite) AfterTest(suiteName, testName string) { 30 | s.db.Close() 31 | } 32 | 33 | func (s *DatabaseSuite) TestPost() { 34 | s.db.DB.Collection("posts").Drop(nil) 35 | 36 | var err error 37 | for i := 1; i <= 25; i++ { 38 | // user1 39 | UserID, _ := primitive.ObjectIDFromHex("5c99bd941ba7b2304ad8c52a") 40 | article := (&model.Post{ 41 | UserID: UserID, 42 | Title: fmt.Sprintf("tile%d", i), 43 | Body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto", 44 | }).New() 45 | s.db.CreatePost(article) 46 | } 47 | assert.Nil(s.T(), err) 48 | } 49 | 50 | func (s *DatabaseSuite) TestUpdatePost() { 51 | id, _ := primitive.ObjectIDFromHex("5c92e6199929adef73bceea1") 52 | userID, _ := primitive.ObjectIDFromHex("5c8f9a83da2c3fed4eee9dc1") 53 | 54 | post := &model.Post{ 55 | ID: id, 56 | UserID: userID, 57 | Title: "title1", 58 | Body: "title1bodytitle1body", 59 | } 60 | 61 | assert.Equal(s.T(), post, s.db.UpdatePost(post)) 62 | } 63 | -------------------------------------------------------------------------------- /model/movie.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | // The Movie holds 8 | type Movie struct { 9 | ID primitive.ObjectID `bson:"_id" json:"id"` 10 | Link string `bson:"link" json:"link"` 11 | Cover string `bson:"cover" json:"cover"` 12 | Name string `bson:"name" json:"name"` 13 | Quality string `bson:"quality" json:"quality"` 14 | Score string `bson:"score" json:"score"` 15 | Kuyun []map[string]string `bson:"kuyun" json:"kuyun"` 16 | Ckm3u8 []map[string]string `bson:"ckm3u8" json:"ckm3u8"` 17 | Download []map[string]string `bson:"download" json:"download"` 18 | TypeID int `bson:"typeId" json:"typeId"` 19 | Released string `bson:"released" json:"released"` 20 | Area string `bson:"area" json:"area"` 21 | Language string `bson:"language" json:"language"` 22 | Detail map[string]interface{} `bson:"detail" json:"detail"` 23 | StrID string `bson:"strid" json:"strid"` 24 | UpdateTime string `bson:"updateTime" json:"updateTime"` 25 | } 26 | 27 | // New is 28 | func (m *Movie) New() *Movie { 29 | return &Movie{ 30 | ID: primitive.NewObjectID(), 31 | Link: m.Link, 32 | Cover: m.Cover, 33 | Name: m.Name, 34 | Quality: m.Quality, 35 | Score: m.Score, 36 | Kuyun: m.Kuyun, 37 | Ckm3u8: m.Ckm3u8, 38 | Download: m.Download, 39 | TypeID: m.TypeID, 40 | Released: m.Released, 41 | Area: m.Area, 42 | Language: m.Language, 43 | Detail: m.Detail, 44 | UpdateTime: m.UpdateTime, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/lotteryjs/configor" 5 | ) 6 | 7 | // Configuration is stuff that can be configured externally per env variables or config file (config.yml). 8 | type Configuration struct { 9 | Server struct { 10 | ListenAddr string `default:""` 11 | Port int `default:"6868"` 12 | ResponseHeaders map[string]string 13 | } 14 | Database struct { 15 | Dbname string `default:""` 16 | Connection string `default:""` 17 | Authmechanism string `default:""` 18 | Authsource string `default:""` 19 | Username string `default:""` 20 | Password string `default:""` 21 | } 22 | } 23 | 24 | // Get returns the configuration extracted from env variables or config file. 25 | func Get() *Configuration { 26 | conf := new(Configuration) 27 | err := configor.New(&configor.Config{EnvironmentPrefix: "TenMinutesApi"}).Load(conf, "config.yml") 28 | if err != nil { 29 | panic(err) 30 | } 31 | return conf 32 | } 33 | 34 | /** 35 | 36 | =========爬虫========== 37 | 38 | 参数说明: 39 | app.spider_path: 爬虫路由 40 | app.spider_path_name: 爬虫路由名称 41 | app.debug_path: debug的路由 42 | app.debug_path_name: debug的路由名称 43 | cron.timing_spider: 定时爬虫的CRON表达式 44 | ding.access_token: 钉钉机器人token 45 | */ 46 | var AppJsonConfig = []byte(` 47 | { 48 | "app": { 49 | "port": ":8899", 50 | "spider_path": "/movies-spider", 51 | "spider_path_name": "MoviesSpider", 52 | "debug_path": "/debug", 53 | "debug_path_name": "Debug", 54 | "spider_mod": "api" 55 | }, 56 | "redis": { 57 | "port": "6379", 58 | "addr": "localhost", 59 | "password": "", 60 | "db": 10 61 | }, 62 | "cron": { 63 | "timing_spider": "0 0 1 * * ?" 64 | }, 65 | "ding": { 66 | "access_token": "" 67 | } 68 | } 69 | `) 70 | -------------------------------------------------------------------------------- /error/handler.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "unicode" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/gotify/server/model" 11 | "gopkg.in/go-playground/validator.v8" 12 | ) 13 | 14 | // Handler creates a gin middleware for handling errors. 15 | func Handler() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | c.Next() 18 | 19 | if len(c.Errors) > 0 { 20 | for _, e := range c.Errors { 21 | switch e.Type { 22 | case gin.ErrorTypeBind: 23 | errs, ok := e.Err.(validator.ValidationErrors) 24 | 25 | if !ok { 26 | writeError(c, e.Error()) 27 | return 28 | } 29 | 30 | var stringErrors []string 31 | for _, err := range errs { 32 | stringErrors = append(stringErrors, validationErrorToText(err)) 33 | } 34 | writeError(c, strings.Join(stringErrors, "; ")) 35 | default: 36 | writeError(c, e.Err.Error()) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | func validationErrorToText(e *validator.FieldError) string { 44 | runes := []rune(e.Field) 45 | runes[0] = unicode.ToLower(runes[0]) 46 | fieldName := string(runes) 47 | switch e.Tag { 48 | case "required": 49 | return fmt.Sprintf("Field '%s' is required", fieldName) 50 | case "max": 51 | return fmt.Sprintf("Field '%s' must be less or equal to %s", fieldName, e.Param) 52 | case "min": 53 | return fmt.Sprintf("Field '%s' must be more or equal to %s", fieldName, e.Param) 54 | } 55 | return fmt.Sprintf("Field '%s' is not valid", fieldName) 56 | } 57 | 58 | func writeError(ctx *gin.Context, errString string) { 59 | status := http.StatusBadRequest 60 | if ctx.Writer.Status() != http.StatusOK { 61 | status = ctx.Writer.Status() 62 | } 63 | ctx.JSON(status, &model.Error{Error: http.StatusText(status), ErrorCode: status, ErrorDescription: errString}) 64 | } 65 | -------------------------------------------------------------------------------- /static/style/common.css: -------------------------------------------------------------------------------- 1 | .iconfont { 2 | margin: 0 5px; 3 | } 4 | @font-face { 5 | font-family: 'iconfont'; 6 | /* project id 1476614 */ 7 | src: url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.eot'); 8 | src: url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.eot?#iefix') format('embedded-opentype'), url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.woff2') format('woff2'), url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.woff') format('woff'), url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.ttf') format('truetype'), url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.svg#iconfont') format('svg'); 9 | } 10 | body { 11 | background-color: #f4f5f5; 12 | } 13 | .mb20 { 14 | margin-bottom: 20px; 15 | } 16 | .iconfont { 17 | font-family: "iconfont" !important; 18 | font-size: 16px; 19 | font-style: normal; 20 | -webkit-font-smoothing: antialiased; 21 | -webkit-text-stroke-width: 0.2px; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | :focus { 25 | outline: -webkit-focus-ring-color auto 0px; 26 | } 27 | textarea:focus { 28 | outline-offset: 0px; 29 | } 30 | .dropdown-menu > li > a { 31 | padding: 8px 20px; 32 | } 33 | .a-upload { 34 | padding: 4px 10px; 35 | line-height: 34px; 36 | position: relative; 37 | cursor: pointer; 38 | color: #888; 39 | background: #fafafa; 40 | border: 1px solid #ddd; 41 | border-radius: 4px; 42 | overflow: hidden; 43 | display: inline-block; 44 | /* top: 6px; */ 45 | display: inline; 46 | } 47 | .a-upload input { 48 | position: absolute; 49 | font-size: 10px; 50 | right: 0; 51 | top: 0; 52 | opacity: 0; 53 | filter: alpha(opacity=0); 54 | cursor: pointer; 55 | } 56 | .a-upload:hover { 57 | color: #444; 58 | background: #eee; 59 | border-color: #ccc; 60 | text-decoration: none; 61 | } 62 | .vditor-wysiwyg { 63 | border: 0; 64 | background-color: #fff; 65 | padding: 30px 50px!important; 66 | } 67 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | "time" 6 | ) 7 | 8 | // The User holds 9 | type User struct { 10 | ID primitive.ObjectID `bson:"_id" json:"id"` 11 | Name string `bson:"name" json:"name"` 12 | UserName string `bson:"username" json:"username"` 13 | Email string `bson:"email" json:"email"` 14 | Address UserAddress `bson:"address" json:"address"` 15 | Phone string `bson:"phone" json:"phone"` 16 | Website string `bson:"website" json:"website"` 17 | Company UserCompany `bson:"company" json:"company"` 18 | Created time.Time `bson:"created" json:"created"` 19 | Updated time.Time `bson:"updated" json:"updated"` 20 | } 21 | 22 | // The UserAddress holds 23 | type UserAddress struct { 24 | Street string `bson:"street" json:"street"` 25 | Suite string `bson:"suite" json:"suite"` 26 | City string `bson:"city" json:"city"` 27 | Zipcode string `bson:"zipcode" json:"zipcode"` 28 | Geo UserAddressGeo `bson:"geo" json:"geo"` 29 | } 30 | 31 | // The UserAddressGeo holds 32 | type UserAddressGeo struct { 33 | Lat string `bson:"lat" json:"lat"` 34 | Lng string `bson:"lng" json:"lng"` 35 | } 36 | 37 | // The UserCompany holds 38 | type UserCompany struct { 39 | Name string `bson:"name" json:"name"` 40 | CatchPhrase string `bson:"catchPhrase" json:"catchPhrase"` 41 | BS string `bson:"bs" json:"bs"` 42 | } 43 | 44 | // New is 45 | func (u *User) New() *User { 46 | return &User{ 47 | ID: primitive.NewObjectID(), 48 | Name: u.Name, 49 | UserName: u.UserName, 50 | Email: u.Email, 51 | Address: u.Address, 52 | Phone: u.Phone, 53 | Website: u.Website, 54 | Company: u.Company, 55 | Created: time.Now(), 56 | Updated: time.Now(), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /static/style/common.less: -------------------------------------------------------------------------------- 1 | .iconfont { 2 | margin: 0 5px; 3 | } 4 | 5 | 6 | 7 | @font-face { 8 | font-family: 'iconfont'; 9 | /* project id 1476614 */ 10 | src: url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.eot'); 11 | src: url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.eot?#iefix') format('embedded-opentype'), 12 | url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.woff2') format('woff2'), 13 | url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.woff') format('woff'), 14 | url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.ttf') format('truetype'), 15 | url('//at.alicdn.com/t/font_1476614_zjy3tw9vyz.svg#iconfont') format('svg'); 16 | } 17 | 18 | body { 19 | background-color: #f4f5f5; 20 | } 21 | 22 | .mb20 { 23 | margin-bottom: 20px; 24 | } 25 | 26 | .iconfont { 27 | font-family: "iconfont" !important; 28 | font-size: 16px; 29 | font-style: normal; 30 | -webkit-font-smoothing: antialiased; 31 | -webkit-text-stroke-width: 0.2px; 32 | -moz-osx-font-smoothing: grayscale; 33 | } 34 | 35 | :focus { 36 | outline: -webkit-focus-ring-color auto 0px; 37 | } 38 | 39 | textarea:focus { 40 | outline-offset: -0px; 41 | } 42 | 43 | .dropdown-menu>li>a{ 44 | padding: 8px 20px; 45 | } 46 | 47 | .a-upload { 48 | padding: 4px 10px; 49 | line-height: 34px; 50 | position: relative; 51 | cursor: pointer; 52 | color: #888; 53 | background: #fafafa; 54 | border: 1px solid #ddd; 55 | border-radius: 4px; 56 | overflow: hidden; 57 | display: inline-block; 58 | /* top: 6px; */ 59 | display: inline; 60 | } 61 | 62 | .a-upload input { 63 | position: absolute; 64 | font-size: 10px; 65 | right: 0; 66 | top: 0; 67 | opacity: 0; 68 | filter: alpha(opacity=0); 69 | cursor: pointer 70 | } 71 | 72 | .a-upload:hover { 73 | color: #444; 74 | background: #eee; 75 | border-color: #ccc; 76 | text-decoration: none 77 | } 78 | 79 | .vditor-wysiwyg{ 80 | border: 0; 81 | background-color: #fff; 82 | padding: 30px 50px!important; 83 | } -------------------------------------------------------------------------------- /templates/components/classification.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /database/user_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "movie_spider/model" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | func (s *DatabaseSuite) TestCreateUser() { 11 | s.db.DB.Collection("users").Drop(nil) 12 | 13 | kirk := (&model.User{ 14 | Name: "Graham", 15 | UserName: "Bret", 16 | Email: "Sincere@april.biz", 17 | Address: model.UserAddress{ 18 | Street: "Kulas Light", 19 | Suite: "Apt. 556", 20 | City: "Gwenborough", 21 | Zipcode: "92998-3874", 22 | Geo: model.UserAddressGeo{ 23 | Lat: "-37.3159", 24 | Lng: "81.1496", 25 | }, 26 | }, 27 | Phone: "1-770-736-8031 x56442", 28 | Website: "hildegard.org", 29 | Company: model.UserCompany{ 30 | Name: "Romaguera-Crona", 31 | CatchPhrase: "Multi-layered client-server neural-net", 32 | BS: "harness real-time e-markets", 33 | }, 34 | }).New() 35 | err := s.db.CreateUser(kirk) 36 | assert.Nil(s.T(), err) 37 | } 38 | 39 | func (s *DatabaseSuite) TestGetUsers() { 40 | start := int64(0) 41 | limit := int64(10) 42 | sort := "_id" 43 | order := -1 44 | 45 | users := s.db.GetUsers(&model.Paging{ 46 | Skip: &start, 47 | Limit: &limit, 48 | SortKey: sort, 49 | SortVal: order, 50 | Condition: nil, 51 | }) 52 | 53 | assert.Len(s.T(), users, 1) 54 | } 55 | 56 | func (s *DatabaseSuite) TestGetUserByName() { 57 | user := s.db.GetUserByName("Graham") 58 | 59 | assert.Equal(s.T(), "Graham", user.Name) 60 | } 61 | 62 | func (s *DatabaseSuite) TestGetUserByIDs() { 63 | user := s.db.GetUserByName("Graham") 64 | objectIds := []primitive.ObjectID{user.ID} 65 | users := s.db.GetUserByIDs(objectIds) 66 | 67 | assert.Len(s.T(), users, 1) 68 | } 69 | 70 | func (s *DatabaseSuite) TestCountUser() { 71 | len := s.db.CountUser() 72 | assert.Equal(s.T(), len, "1") 73 | } 74 | 75 | func (s *DatabaseSuite) TestDeleteUserByID() { 76 | user := s.db.GetUserByName("Graham") 77 | s.db.DeleteUserByID(user.ID) 78 | len := s.db.CountUser() 79 | assert.Equal(s.T(), "0", len) 80 | } 81 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module movie_spider 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.5.1 // indirect 7 | github.com/antchfx/htmlquery v1.2.3 // indirect 8 | github.com/antchfx/xmlquery v1.2.4 // indirect 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 10 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 // indirect 11 | github.com/gin-gonic/gin v1.3.0 12 | github.com/go-delve/delve v1.5.0 // indirect 13 | github.com/go-redis/redis/v7 v7.4.0 14 | github.com/go-stack/stack v1.8.0 // indirect 15 | github.com/gobwas/glob v0.2.3 // indirect 16 | github.com/gocolly/colly v1.2.0 17 | github.com/golang/snappy v0.0.1 // indirect 18 | github.com/gotify/server v1.2.1 19 | github.com/json-iterator/go v1.1.10 20 | github.com/kennygrant/sanitize v1.2.4 // indirect 21 | github.com/lotteryjs/configor v1.0.2 22 | github.com/mattn/go-colorable v0.1.7 // indirect 23 | github.com/mattn/go-runewidth v0.0.9 // indirect 24 | github.com/panjf2000/ants/v2 v2.4.1 25 | github.com/peterh/liner v1.2.0 // indirect 26 | github.com/rakyll/statik v0.1.7 27 | github.com/robfig/cron/v3 v3.0.1 28 | github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect 29 | github.com/spf13/cobra v0.0.5 // indirect 30 | github.com/spf13/pflag v1.0.5 // indirect 31 | github.com/spf13/viper v1.7.0 32 | github.com/stretchr/testify v1.4.0 33 | github.com/temoto/robotstxt v1.1.1 // indirect 34 | github.com/ugorji/go/codec v0.0.0-20190316192920-e2bddce071ad // indirect 35 | github.com/valyala/fasthttp v1.15.1 36 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect 37 | github.com/xdg/stringprep v1.0.0 // indirect 38 | go.mongodb.org/mongo-driver v1.0.0 39 | go.starlark.net v0.0.0-20200804153121-4379bb3f9ac0 // indirect 40 | golang.org/x/arch v0.0.0-20200511175325-f7c78586839d // indirect 41 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 42 | golang.org/x/sys v0.0.0-20200817085935-3ff754bf58a9 // indirect 43 | gopkg.in/go-playground/validator.v8 v8.18.2 44 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 45 | gopkg.in/yaml.v2 v2.3.0 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /templates/components/header_createBlog.html: -------------------------------------------------------------------------------- 1 | 27 | 28 | <%- include('../components/login.html') %> 29 | -------------------------------------------------------------------------------- /templates/movie/movieDetail.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 | 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |

{{.movies.Name}} {{.movies.Score}}

15 |
更新时间: {{.movies.Detail.update}}
16 |

导演:{{.movies.Detail.director}}

17 |

主演:{{.movies.Detail.starring}}

18 |

地区:{{.movies.Detail.area}}

19 |

语言:{{.movies.Detail.language}}

20 |

上映:{{.movies.Detail.released}}

21 |

简介:{{.movies.Detail.vod_play_info}}

22 |
23 |
24 |
25 |
26 |
27 |

在线播放

28 | 33 |

点击下载

34 | 39 |
40 |
41 |
42 | 45 | 50 | {{template "footer.html" .}} -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "movie_spider/api" 9 | "movie_spider/config" 10 | "movie_spider/database" 11 | "movie_spider/error" 12 | "movie_spider/model" 13 | ) 14 | 15 | // Create creates the gin engine with all routes. 16 | func Create(db *database.TenDatabase, vInfo *model.VersionInfo, conf *config.Configuration) *gin.Engine { 17 | g := gin.Default() 18 | g.Use(gin.Logger(), gin.Recovery(), error.Handler()) 19 | g.NoRoute(error.NotFound()) 20 | 21 | g.Use(func(ctx *gin.Context) { 22 | ctx.Header("Content-Type", "application/json") 23 | origin := ctx.Request.Header.Get("Origin") 24 | for header, value := range conf.Server.ResponseHeaders { 25 | if origin == "http://localhost:3000" && header == "Access-Control-Allow-Origin" { 26 | ctx.Header("Access-Control-Allow-Origin", "http://localhost:3000") 27 | } else { 28 | ctx.Header(header, value) 29 | } 30 | } 31 | if ctx.Request.Method == "OPTIONS" { 32 | ctx.AbortWithStatus(http.StatusNoContent) 33 | } 34 | }) 35 | 36 | userHandler := api.UserAPI{DB: db} 37 | postHandler := api.PostAPI{DB: db} 38 | movieHandler := api.MovieAPI{DB: db} 39 | htmlHandler := api.HtmlAPI{DB: db} 40 | 41 | postU := g.Group("/users") 42 | { 43 | postU.GET("", userHandler.GetUsers) 44 | postU.DELETE(":id", userHandler.DeleteUserByID) 45 | } 46 | 47 | postG := g.Group("/posts") 48 | { 49 | postG.GET("", postHandler.GetPosts) 50 | postG.POST("", postHandler.CreatePost) 51 | postG.GET(":id", postHandler.GetPostByID) 52 | postG.PUT(":id", postHandler.UpdatePostByID) 53 | postG.DELETE(":id", postHandler.DeletePostByID) 54 | } 55 | 56 | postM := g.Group("/movie") 57 | { 58 | postM.GET("", movieHandler.GetMovies) 59 | postM.GET(":id", movieHandler.GetMovieByID) 60 | postM.DELETE(":id", movieHandler.DeleteMovieByID) 61 | } 62 | 63 | g.LoadHTMLGlob("templates/**/*") 64 | g.Static("/static", "./static/") 65 | postH := g.Group("/view") 66 | { 67 | postH.GET("movie", htmlHandler.Movie) 68 | postH.GET("movieDetail/:id", htmlHandler.MovieDetail) 69 | } 70 | 71 | g.GET("version", func(ctx *gin.Context) { 72 | ctx.JSON(200, vInfo) 73 | }) 74 | 75 | return g 76 | } 77 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "time" 9 | 10 | "movie_spider/config" 11 | "movie_spider/database" 12 | "movie_spider/mode" 13 | "movie_spider/model" 14 | "movie_spider/router" 15 | "movie_spider/runner" 16 | "movie_spider/utils" 17 | "movie_spider/utils/spider" 18 | 19 | _ "movie_spider/statik" 20 | 21 | "github.com/spf13/viper" 22 | ) 23 | 24 | var ( 25 | // Version the version of TMA. 26 | Version = "unknown" 27 | // Commit the git commit hash of this version. 28 | Commit = "unknown" 29 | // BuildDate the date on which this binary was build. 30 | BuildDate = "unknown" 31 | // Mode the build mode 32 | Mode = mode.Dev 33 | ) 34 | 35 | // 首次启动自动开启爬虫 36 | func firstSpider() { 37 | 38 | hasHK := utils.RedisDB.Exists("detail_links:id:14").Val() 39 | log.Println("hasHK", hasHK) 40 | // 不存在首页的key 则认为是第一次启动 41 | if hasHK == 0 { 42 | spider.Create().Start() 43 | } 44 | } 45 | 46 | // 初始化配置文件 47 | func init() { 48 | viper.SetConfigType("json") // 设置配置文件的类型 49 | 50 | if err := viper.ReadConfig(bytes.NewBuffer(config.AppJsonConfig)); err != nil { 51 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 52 | // Config file not found; ignore error if desired 53 | log.Println("no such config file") 54 | } else { 55 | // Config file was found but another error was produced 56 | log.Println("read config error") 57 | } 58 | log.Fatal(err) // 读取配置文件失败致命错误 59 | } 60 | } 61 | 62 | func main() { 63 | vInfo := &model.VersionInfo{Version: Version, Commit: Commit, BuildDate: BuildDate} 64 | mode.Set(Mode) 65 | 66 | fmt.Println("Starting TMA version", vInfo.Version+"@"+BuildDate) 67 | rand.Seed(time.Now().UnixNano()) 68 | conf := config.Get() 69 | log.Println(conf) 70 | db, err := database.New(conf.Database.Connection, conf.Database.Dbname, conf.Database.Username, conf.Database.Password) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | defer db.Close() 75 | 76 | // 初始化 redis 连接 77 | utils.InitRedisDB() 78 | defer utils.CloseRedisDB() 79 | //第一次启动检查爬虫 80 | // firstSpider() 81 | // 启动定时爬虫任务 82 | utils.TimingSpider(func() { 83 | spider.Create().Start() 84 | return 85 | }) 86 | 87 | engine := router.Create(db, vInfo, conf) 88 | runner.Run(engine, conf) 89 | } 90 | -------------------------------------------------------------------------------- /api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strconv" 7 | 8 | "movie_spider/model" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | ) 13 | 14 | // The UserDatabase interface for encapsulating database access. 15 | type UserDatabase interface { 16 | GetUserByIDs(ids []primitive.ObjectID) []*model.User 17 | DeleteUserByID(id primitive.ObjectID) error 18 | CreateUser(user *model.User) error 19 | GetUsers(paging *model.Paging) []*model.User 20 | CountUser() string 21 | } 22 | 23 | // The UserAPI provides handlers for managing users. 24 | type UserAPI struct { 25 | DB UserDatabase 26 | } 27 | 28 | // GetUserByIDs returns the user by id 29 | func (a *UserAPI) GetUserByIDs(ctx *gin.Context) { 30 | withIDs(ctx, "id", func(ids []primitive.ObjectID) { 31 | ctx.JSON(200, a.DB.GetUserByIDs(ids)) 32 | }) 33 | } 34 | 35 | // DeleteUserByID deletes the user by id 36 | func (a *UserAPI) DeleteUserByID(ctx *gin.Context) { 37 | withID(ctx, "id", func(id primitive.ObjectID) { 38 | if err := a.DB.DeleteUserByID(id); err == nil { 39 | ctx.JSON(200, http.StatusOK) 40 | } else { 41 | if err != nil { 42 | ctx.AbortWithError(500, err) 43 | } else { 44 | ctx.AbortWithError(404, errors.New("user does not exist")) 45 | } 46 | } 47 | }) 48 | } 49 | 50 | // GetUsers returns all the users 51 | // _end=5&_order=DESC&_sort=id&_start=0 adapt react-admin 52 | func (a *UserAPI) GetUsers(ctx *gin.Context) { 53 | var ( 54 | start int64 55 | end int64 56 | sort string 57 | order int 58 | ) 59 | id := ctx.DefaultQuery("id", "") 60 | if id != "" { 61 | a.GetUserByIDs(ctx) 62 | return 63 | } 64 | start, _ = strconv.ParseInt(ctx.DefaultQuery("_start", "0"), 10, 64) 65 | end, _ = strconv.ParseInt(ctx.DefaultQuery("_end", "10"), 10, 64) 66 | sort = ctx.DefaultQuery("_sort", "_id") 67 | order = 1 68 | 69 | if sort == "id" { 70 | sort = "_id" 71 | } 72 | 73 | if ctx.DefaultQuery("_order", "DESC") == "DESC" { 74 | order = -1 75 | } 76 | 77 | limit := end - start 78 | users := a.DB.GetUsers( 79 | &model.Paging{ 80 | Skip: &start, 81 | Limit: &limit, 82 | SortKey: sort, 83 | SortVal: order, 84 | Condition: nil, 85 | }) 86 | 87 | ctx.Header("X-Total-Count", a.DB.CountUser()) 88 | ctx.JSON(200, users) 89 | } 90 | -------------------------------------------------------------------------------- /templates/components/pagination.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 共页 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/components/login.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /templates/components/nav.html: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /templates/components/header_ViewHandBook.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | <%- include('../components/login.html') %> 43 | -------------------------------------------------------------------------------- /database/post.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "movie_spider/model" 8 | 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | // GetPosts returns all posts. 15 | // start, end int, order, sort string 16 | func (d *TenDatabase) GetPosts(paging *model.Paging) []*model.Post { 17 | posts := []*model.Post{} 18 | condition := bson.D{} 19 | if paging.Condition != nil { 20 | condition = (paging.Condition).(bson.D) 21 | } 22 | cursor, err := d.DB.Collection("posts"). 23 | Find(context.Background(), condition, 24 | &options.FindOptions{ 25 | Skip: paging.Skip, 26 | Sort: bson.D{bson.E{Key: paging.SortKey, Value: paging.SortVal}}, 27 | Limit: paging.Limit, 28 | }) 29 | if err != nil { 30 | return nil 31 | } 32 | defer cursor.Close(context.Background()) 33 | 34 | for cursor.Next(context.Background()) { 35 | post := &model.Post{} 36 | if err := cursor.Decode(post); err != nil { 37 | return nil 38 | } 39 | posts = append(posts, post) 40 | } 41 | 42 | return posts 43 | } 44 | 45 | // CreatePost creates a post. 46 | func (d *TenDatabase) CreatePost(post *model.Post) *model.Post { 47 | // Specifies the order in which to return results. 48 | upsert := true 49 | result := d.DB.Collection("posts"). 50 | FindOneAndReplace(context.Background(), 51 | bson.D{{Key: "_id", Value: post.ID}}, 52 | post, 53 | &options.FindOneAndReplaceOptions{ 54 | Upsert: &upsert, 55 | }, 56 | ) 57 | if result != nil { 58 | return post 59 | } 60 | return nil 61 | } 62 | 63 | // GetPostByID returns the post by the given id or nil. 64 | func (d *TenDatabase) GetPostByID(id primitive.ObjectID) *model.Post { 65 | var post *model.Post 66 | err := d.DB.Collection("posts"). 67 | FindOne(context.Background(), bson.D{{Key: "_id", Value: id}}). 68 | Decode(&post) 69 | if err != nil { 70 | return nil 71 | } 72 | return post 73 | } 74 | 75 | // DeletePostByID deletes a post by its id. 76 | func (d *TenDatabase) DeletePostByID(id primitive.ObjectID) error { 77 | _, err := d.DB.Collection("posts").DeleteOne(context.Background(), bson.D{{Key: "_id", Value: id}}) 78 | return err 79 | } 80 | 81 | // UpdatePost updates a post. 82 | func (d *TenDatabase) UpdatePost(post *model.Post) *model.Post { 83 | result := d.DB.Collection("posts"). 84 | FindOneAndReplace(context.Background(), 85 | bson.D{{Key: "_id", Value: post.ID}}, 86 | post, 87 | &options.FindOneAndReplaceOptions{}, 88 | ) 89 | if result != nil { 90 | return post 91 | } 92 | return nil 93 | } 94 | 95 | // CountPost returns the post count 96 | func (d *TenDatabase) CountPost(condition interface{}) string { 97 | cd := bson.D{} 98 | if condition != nil { 99 | cd = (condition).(bson.D) 100 | } 101 | total, err := d.DB.Collection("posts").CountDocuments(context.Background(), cd, &options.CountOptions{}) 102 | if err != nil { 103 | return "0" 104 | } 105 | return strconv.Itoa(int(total)) 106 | } 107 | -------------------------------------------------------------------------------- /database/user.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strconv" 7 | 8 | "movie_spider/model" 9 | 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | // GetUsers returns all users. 16 | // start, end int, order, sort string 17 | func (d *TenDatabase) GetUsers(paging *model.Paging) []*model.User { 18 | users := []*model.User{} 19 | cursor, err := d.DB.Collection("users"). 20 | Find(context.Background(), bson.D{}, 21 | &options.FindOptions{ 22 | Skip: paging.Skip, 23 | Sort: bson.D{bson.E{Key: paging.SortKey, Value: paging.SortVal}}, 24 | Limit: paging.Limit, 25 | }) 26 | if err != nil { 27 | return nil 28 | } 29 | defer cursor.Close(context.Background()) 30 | 31 | for cursor.Next(context.Background()) { 32 | user := &model.User{} 33 | if err := cursor.Decode(user); err != nil { 34 | return nil 35 | } 36 | users = append(users, user) 37 | } 38 | 39 | return users 40 | } 41 | 42 | // CreateUser creates a user. 43 | func (d *TenDatabase) CreateUser(user *model.User) error { 44 | if _, err := d.DB.Collection("users"). 45 | InsertOne(context.Background(), user); err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | // GetUserByName returns the user by the given name or nil. 52 | func (d *TenDatabase) GetUserByName(name string) *model.User { 53 | var user *model.User 54 | err := d.DB.Collection("users"). 55 | FindOne(context.Background(), bson.D{{Key: "name", Value: name}}). 56 | Decode(&user) 57 | if err != nil { 58 | return nil 59 | } 60 | return user 61 | } 62 | 63 | // GetUserByIDs returns the user by the given id or nil. 64 | func (d *TenDatabase) GetUserByIDs(ids []primitive.ObjectID) []*model.User { 65 | var users []*model.User 66 | cursor, err := d.DB.Collection("users"). 67 | Find(context.Background(), bson.D{{ 68 | Key: "_id", 69 | Value: bson.D{{ 70 | Key: "$in", 71 | Value: ids, 72 | }}, 73 | }}) 74 | if err != nil { 75 | return nil 76 | } 77 | defer cursor.Close(context.Background()) 78 | 79 | for cursor.Next(context.Background()) { 80 | user := &model.User{} 81 | if err := cursor.Decode(user); err != nil { 82 | return nil 83 | } 84 | users = append(users, user) 85 | } 86 | 87 | return users 88 | } 89 | 90 | // CountUser returns the user count 91 | func (d *TenDatabase) CountUser() string { 92 | total, err := d.DB.Collection("users").CountDocuments(context.Background(), bson.D{{}}, &options.CountOptions{}) 93 | if err != nil { 94 | return "0" 95 | } 96 | return strconv.Itoa(int(total)) 97 | } 98 | 99 | // DeleteUserByID deletes a user by its id. 100 | func (d *TenDatabase) DeleteUserByID(id primitive.ObjectID) error { 101 | if d.CountPost(bson.D{{Key: "userId", Value: id}}) == "0" { 102 | _, err := d.DB.Collection("users").DeleteOne(context.Background(), bson.D{{Key: "_id", Value: id}}) 103 | return err 104 | } 105 | return errors.New("the current user has posts published") 106 | } 107 | -------------------------------------------------------------------------------- /api/html.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "movie_spider/model" 5 | "movie_spider/utils/spider" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | // The MovieAPI provides handlers for managing movies. 14 | type HtmlAPI struct { 15 | DB MovieDatabase 16 | } 17 | 18 | func (a *HtmlAPI) Movie(ctx *gin.Context) { 19 | 20 | var ( 21 | page int64 22 | sort string 23 | order int 24 | start int64 25 | activeYear int 26 | selZiCategories int 27 | selCategories int 28 | keyWords string 29 | ) 30 | page, _ = strconv.ParseInt(ctx.DefaultQuery("page", "0"), 10, 64) 31 | start = page * 20 32 | activeYear, _ = strconv.Atoi(ctx.DefaultQuery("activeYear", "")) 33 | sort = ctx.DefaultQuery("sort", "updateTime") 34 | keyWords = ctx.DefaultQuery("keyWords", "") 35 | selZiCategories, _ = strconv.Atoi(ctx.DefaultQuery("selZiCategories", "")) 36 | selCategories, _ = strconv.Atoi(ctx.DefaultQuery("selCategories", "")) 37 | 38 | var limit int64 = 20 39 | 40 | order = 1 41 | if sort == "id" { 42 | sort = "_id" 43 | } 44 | if ctx.DefaultQuery("_order", "DESC") == "DESC" { 45 | order = -1 46 | } 47 | 48 | categories := spider.CategoriesStr() 49 | 50 | condition := make(bson.M) 51 | if activeYear > 0 { 52 | condition["released"] = strconv.Itoa(activeYear) 53 | } 54 | 55 | if keyWords != "" { 56 | condition["name"] = primitive.Regex{Pattern: keyWords} 57 | }else{ 58 | if selZiCategories > 0 { 59 | condition["typeId"] = selZiCategories 60 | } else { 61 | condition["typeId"] = 6 62 | } 63 | } 64 | 65 | movies := a.DB.GetMovies( 66 | &model.Paging{ 67 | Skip: &start, 68 | Limit: &limit, 69 | SortKey: sort, 70 | SortVal: order, 71 | Condition: condition, 72 | }) 73 | moviesCount := a.DB.CountMovie(condition) 74 | allMoviesCount := a.DB.CountMovie(nil) 75 | 76 | for i := 0; i < len(movies); i++ { 77 | movies[i].StrID = movies[i].ID.Hex() /* 设置元素为 i + 100 */ 78 | } 79 | 80 | //生成年份列表 81 | var yearList [15]int 82 | for i := 0; i < 15; i++ { 83 | yearList[i] = 2020 - i /* 设置元素为 i + 100 */ 84 | } 85 | ctx.Header("Content-Type", "text/html; charset=utf-8") 86 | ctx.HTML(200, "movie.html", gin.H{ 87 | "title": "牛逼的电影全在这", 88 | "movies": movies, 89 | "allCount": moviesCount, 90 | "keyWords": keyWords, 91 | "pageCount": limit, 92 | "page": page, 93 | "yearList": yearList, 94 | "activeYear": activeYear, 95 | "categories": categories, 96 | "selCategories": selCategories, 97 | "selZiCategories": selZiCategories, 98 | "allMoviesCount": allMoviesCount, 99 | }) 100 | } 101 | 102 | func (a *HtmlAPI) MovieDetail(ctx *gin.Context) { 103 | withID(ctx, "id", func(id primitive.ObjectID) { 104 | ctx.Header("Content-Type", "text/html; charset=utf-8") 105 | movies := a.DB.GetMovieByID(id) 106 | ctx.HTML(200, "movieDetail.html", gin.H{ 107 | "movies": movies, 108 | }) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /api/movie.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "movie_spider/model" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "gopkg.in/mgo.v2/bson" 13 | ) 14 | 15 | // The MovieDatabase interface for encapsulating database access. 16 | type MovieDatabase interface { 17 | GetMovieByIDs(ids []primitive.ObjectID) []*model.Movie 18 | GetMovieByID(id primitive.ObjectID) *model.Movie 19 | GetMovieByName(name string) *model.Movie 20 | DeleteMovieByID(id primitive.ObjectID) error 21 | FindOneAndReplace(movie *model.Movie) *mongo.SingleResult 22 | CreateMovie(movie *model.Movie) error 23 | GetMovies(paging *model.Paging) []*model.Movie 24 | CountMovie(condition interface{}) string 25 | } 26 | 27 | // The MovieAPI provides handlers for managing movies. 28 | type MovieAPI struct { 29 | DB MovieDatabase 30 | } 31 | 32 | // GetMovieByIDs returns the movie by id 33 | func (a *MovieAPI) GetMovieByIDs(ctx *gin.Context) { 34 | withIDs(ctx, "id", func(ids []primitive.ObjectID) { 35 | ctx.JSON(200, a.DB.GetMovieByIDs(ids)) 36 | }) 37 | } 38 | 39 | // GetMovieByIDs returns the movie by id 40 | func (a *MovieAPI) GetMovieByID(ctx *gin.Context) { 41 | withID(ctx, "id", func(id primitive.ObjectID) { 42 | ctx.JSON(200, a.DB.GetMovieByID(id)) 43 | }) 44 | } 45 | 46 | 47 | 48 | // DeleteMovieByID deletes the movie by id 49 | func (a *MovieAPI) DeleteMovieByID(ctx *gin.Context) { 50 | withID(ctx, "id", func(id primitive.ObjectID) { 51 | if err := a.DB.DeleteMovieByID(id); err == nil { 52 | ctx.JSON(200, http.StatusOK) 53 | } else { 54 | if err != nil { 55 | ctx.AbortWithError(500, err) 56 | } else { 57 | ctx.AbortWithError(404, errors.New("movie does not exist")) 58 | } 59 | } 60 | }) 61 | } 62 | 63 | // GetMovies returns all the movies 64 | // _end=5&_order=DESC&_sort=id&_start=0 adapt react-admin 65 | func (a *MovieAPI) GetMovies(ctx *gin.Context) { 66 | 67 | var ( 68 | page int64 69 | sort string 70 | order int 71 | start int64 72 | activeYear int 73 | selZiCategories int 74 | ) 75 | page, _ = strconv.ParseInt(ctx.DefaultQuery("page", "0"), 10, 64) 76 | start = (page-1) * 20 77 | activeYear, _ = strconv.Atoi(ctx.DefaultQuery("activeYear", "")) 78 | sort = ctx.DefaultQuery("sort", "updateTime") 79 | selZiCategories, _ = strconv.Atoi(ctx.DefaultQuery("selZiCategories", "")) 80 | 81 | var limit int64 = 20 82 | 83 | order = 1 84 | if sort == "id" { 85 | sort = "_id" 86 | } 87 | if ctx.DefaultQuery("_order", "DESC") == "DESC" { 88 | order = -1 89 | } 90 | 91 | condition := make(bson.M) 92 | if activeYear > 0 { 93 | condition["released"] = strconv.Itoa(activeYear) 94 | } 95 | if selZiCategories > 0 { 96 | condition["typeId"] = selZiCategories 97 | } else { 98 | condition["typeId"] = 6 99 | } 100 | 101 | movies := a.DB.GetMovies( 102 | &model.Paging{ 103 | Skip: &start, 104 | Limit: &limit, 105 | SortKey: sort, 106 | SortVal: order, 107 | Condition: condition, 108 | }) 109 | // moviesCount := a.DB.CountMovie(condition) 110 | // allMoviesCount := a.DB.CountMovie(nil) 111 | 112 | ctx.JSON(200, movies) 113 | } 114 | -------------------------------------------------------------------------------- /api/post.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strconv" 7 | 8 | "movie_spider/model" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | ) 14 | 15 | // The PostDatabase interface for encapsulating database access. 16 | type PostDatabase interface { 17 | GetPosts(paging *model.Paging) []*model.Post 18 | GetPostByID(id primitive.ObjectID) *model.Post 19 | CreatePost(post *model.Post) *model.Post 20 | UpdatePost(post *model.Post) *model.Post 21 | DeletePostByID(id primitive.ObjectID) error 22 | CountPost(condition interface{}) string 23 | } 24 | 25 | // The PostAPI provides handlers for managing posts. 26 | type PostAPI struct { 27 | DB PostDatabase 28 | } 29 | 30 | // CreatePost creates a post. 31 | func (a *PostAPI) CreatePost(ctx *gin.Context) { 32 | var post = model.Post{} 33 | if err := ctx.ShouldBind(&post); err == nil { 34 | if result := a.DB.CreatePost(post.New()); result != nil { 35 | ctx.JSON(201, result) 36 | } else { 37 | ctx.AbortWithError(500, errors.New("CreatePost error")) 38 | } 39 | } else { 40 | ctx.AbortWithError(500, errors.New("ShouldBind error")) 41 | } 42 | } 43 | 44 | // GetPosts returns all the posts 45 | // _end=5&_order=DESC&_sort=id&_start=0 adapt react-admin 46 | func (a *PostAPI) GetPosts(ctx *gin.Context) { 47 | var ( 48 | start int64 49 | end int64 50 | sort string 51 | order int 52 | userID string 53 | ) 54 | 55 | start, _ = strconv.ParseInt(ctx.DefaultQuery("_start", "0"), 10, 64) 56 | end, _ = strconv.ParseInt(ctx.DefaultQuery("_end", "10"), 10, 64) 57 | userID = ctx.DefaultQuery("userId", "") 58 | sort = ctx.DefaultQuery("_sort", "_id") 59 | order = 1 60 | 61 | if sort == "id" { 62 | sort = "_id" 63 | } 64 | 65 | if ctx.DefaultQuery("_order", "DESC") == "DESC" { 66 | order = -1 67 | } 68 | 69 | condition := bson.D{} 70 | if userID != "" { 71 | coditionUserID, _ := primitive.ObjectIDFromHex(userID) 72 | condition = bson.D{{ 73 | Key: "userId", 74 | Value: coditionUserID, 75 | }} 76 | } 77 | 78 | limit := end - start 79 | posts := a.DB.GetPosts( 80 | &model.Paging{ 81 | Skip: &start, 82 | Limit: &limit, 83 | SortKey: sort, 84 | SortVal: order, 85 | Condition: condition, 86 | }) 87 | 88 | ctx.Header("X-Total-Count", a.DB.CountPost(nil)) 89 | ctx.JSON(200, posts) 90 | } 91 | 92 | // GetPostByID returns the post by id 93 | func (a *PostAPI) GetPostByID(ctx *gin.Context) { 94 | withID(ctx, "id", func(id primitive.ObjectID) { 95 | if post := a.DB.GetPostByID(id); post != nil { 96 | ctx.JSON(200, post) 97 | } else { 98 | ctx.AbortWithError(404, errors.New("post does not exist")) 99 | } 100 | }) 101 | } 102 | 103 | // DeletePostByID deletes the post by id 104 | func (a *PostAPI) DeletePostByID(ctx *gin.Context) { 105 | withID(ctx, "id", func(id primitive.ObjectID) { 106 | if err := a.DB.DeletePostByID(id); err == nil { 107 | ctx.JSON(200, http.StatusOK) 108 | } else { 109 | ctx.AbortWithError(404, errors.New("post does not exist")) 110 | } 111 | }) 112 | } 113 | 114 | // UpdatePostByID is 115 | func (a *PostAPI) UpdatePostByID(ctx *gin.Context) { 116 | withID(ctx, "id", func(id primitive.ObjectID) { 117 | var post = model.Post{} 118 | abort := errors.New("post does not exist") 119 | if err := ctx.ShouldBind(&post); err == nil { 120 | if result := a.DB.UpdatePost(&post); result != nil { 121 | ctx.JSON(200, result) 122 | } else { 123 | ctx.AbortWithError(404, abort) 124 | } 125 | } else { 126 | ctx.AbortWithError(404, abort) 127 | } 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: CI/CD for UI 3 | 4 | clone: 5 | depth: 50 6 | 7 | steps: 8 | - name: fetch tags 9 | image: docker:git 10 | commands: 11 | - git fetch --tags 12 | 13 | - name: build 14 | image: node:10.15.1 15 | volumes: 16 | - name: cache 17 | path: /tmp/cache 18 | commands: 19 | - cd app 20 | - npm install 21 | - npm run build 22 | 23 | - name: publish image 24 | image: plugins/docker:17.12 25 | settings: 26 | repo: lotteryjs/ui-ten-minutes 27 | auto_tag: true 28 | dockerfile: Dockerfile.UI 29 | username: 30 | from_secret: docker_username 31 | password: 32 | from_secret: docker_password 33 | 34 | - name: update docker-compose 35 | image: appleboy/drone-scp 36 | settings: 37 | host: 38 | from_secret: host 39 | port: 40 | from_secret: port 41 | username: 42 | from_secret: username 43 | password: 44 | from_secret: password 45 | target: /data/wwwroot/ten-minutes 46 | source: docker-compose.UI.yml 47 | 48 | - name: restart 49 | image: appleboy/drone-ssh 50 | pull: true 51 | settings: 52 | host: 53 | from_secret: host 54 | port: 55 | from_secret: port 56 | username: 57 | from_secret: username 58 | password: 59 | from_secret: password 60 | script: 61 | - cd /data/wwwroot/ten-minutes 62 | - docker-compose -f docker-compose.UI.yml pull ui-ten-minutes 63 | - docker-compose -f docker-compose.UI.yml up -d --force-recreate --no-deps ui-ten-minutes 64 | - docker images --quiet --filter=dangling=true | xargs --no-run-if-empty docker rmi -f 65 | 66 | 67 | volumes: 68 | - name: cache 69 | temp: {} 70 | 71 | trigger: 72 | event: 73 | - tag 74 | 75 | --- 76 | 77 | kind: pipeline 78 | name: CI/CD for API 79 | 80 | clone: 81 | depth: 50 82 | 83 | steps: 84 | - name: fetch tags 85 | image: docker:git 86 | commands: 87 | - git fetch --tags 88 | 89 | - name: build 90 | image: golang:1.12 91 | pull: true 92 | commands: 93 | - export LD_FLAGS="-w -s -X main.Version=$(git describe --tags | cut -c 2-) -X main.BuildDate=$(date "+%F-%T") -X main.Commit=$(git rev-parse --verify HEAD) -X main.Mode=prod" 94 | - make build_linux_amd64 95 | 96 | - name: publish image 97 | image: plugins/docker:17.12 98 | settings: 99 | repo: lotteryjs/api-ten-minutes 100 | auto_tag: true 101 | dockerfile: Dockerfile.API 102 | username: 103 | from_secret: docker_username 104 | password: 105 | from_secret: docker_password 106 | 107 | - name: update docker-compose 108 | image: appleboy/drone-scp 109 | settings: 110 | host: 111 | from_secret: host 112 | port: 113 | from_secret: port 114 | username: 115 | from_secret: username 116 | password: 117 | from_secret: password 118 | target: /data/wwwroot/tenapi 119 | source: docker-compose.API.yml 120 | 121 | - name: restart 122 | image: appleboy/drone-ssh 123 | pull: true 124 | settings: 125 | host: 126 | from_secret: host 127 | port: 128 | from_secret: port 129 | username: 130 | from_secret: username 131 | password: 132 | from_secret: password 133 | script: 134 | - cd /data/wwwroot/tenapi 135 | - docker-compose -f docker-compose.API.yml pull api-ten-minutes 136 | - docker-compose -f docker-compose.API.yml up -d --force-recreate --no-deps api-ten-minutes 137 | - docker images --quiet --filter=dangling=true | xargs --no-run-if-empty docker rmi -f 138 | 139 | trigger: 140 | event: 141 | - tag 142 | -------------------------------------------------------------------------------- /database/movie.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strconv" 7 | 8 | "movie_spider/model" 9 | 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | "go.mongodb.org/mongo-driver/mongo/options" 14 | ) 15 | 16 | // GetMovies returns all movies. 17 | // start, end int, order, sort string 18 | func (d *TenDatabase) GetMovies(paging *model.Paging) []*model.Movie { 19 | movies := []*model.Movie{} 20 | cursor, err := d.DB.Collection("movies"). 21 | Find(context.Background(), paging.Condition, 22 | &options.FindOptions{ 23 | Skip: paging.Skip, 24 | Sort: bson.D{bson.E{Key: paging.SortKey, Value: paging.SortVal}}, 25 | Limit: paging.Limit, 26 | }) 27 | if err != nil { 28 | return nil 29 | } 30 | defer cursor.Close(context.Background()) 31 | 32 | for cursor.Next(context.Background()) { 33 | movie := &model.Movie{} 34 | if err := cursor.Decode(movie); err != nil { 35 | return nil 36 | } 37 | movies = append(movies, movie) 38 | } 39 | 40 | return movies 41 | } 42 | 43 | // CreateMovie creates a movie. 44 | func (d *TenDatabase) FindOneAndReplace(movie *model.Movie) *mongo.SingleResult { 45 | upsert := true 46 | result := d.DB.Collection("movies"). 47 | FindOneAndReplace(context.Background(), 48 | bson.D{{Key: "name", Value: movie.Name}}, 49 | movie, 50 | &options.FindOneAndReplaceOptions{ 51 | Upsert: &upsert, 52 | }, 53 | ) 54 | if result != nil { 55 | return result 56 | } 57 | return nil 58 | } 59 | 60 | func (d *TenDatabase) CreateMovie(movie *model.Movie) error { 61 | if _, err := d.DB.Collection("movies"). 62 | InsertOne(context.Background(), movie); err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // GetMovieByName returns the movie by the given name or nil. 69 | func (d *TenDatabase) GetMovieByName(name string) *model.Movie { 70 | var movie *model.Movie 71 | err := d.DB.Collection("movies"). 72 | FindOne(context.Background(), bson.D{{Key: "name", Value: name}}). 73 | Decode(&movie) 74 | if err != nil { 75 | return nil 76 | } 77 | return movie 78 | } 79 | 80 | // GetMovieByIDs returns the movie by the given id or nil. 81 | func (d *TenDatabase) GetMovieByID(id primitive.ObjectID) *model.Movie { 82 | var movie *model.Movie 83 | err := d.DB.Collection("movies"). 84 | FindOne(context.Background(), bson.D{{Key: "_id", Value: id}}). 85 | Decode(&movie) 86 | if err != nil { 87 | return nil 88 | } 89 | return movie 90 | } 91 | 92 | // GetMovieByIDs returns the movie by the given id or nil. 93 | func (d *TenDatabase) GetMovieByIDs(ids []primitive.ObjectID) []*model.Movie { 94 | var movies []*model.Movie 95 | cursor, err := d.DB.Collection("movies"). 96 | Find(context.Background(), bson.D{{ 97 | Key: "_id", 98 | Value: bson.D{{ 99 | Key: "$in", 100 | Value: ids, 101 | }}, 102 | }}) 103 | if err != nil { 104 | return nil 105 | } 106 | defer cursor.Close(context.Background()) 107 | 108 | for cursor.Next(context.Background()) { 109 | movie := &model.Movie{} 110 | if err := cursor.Decode(movie); err != nil { 111 | return nil 112 | } 113 | movies = append(movies, movie) 114 | } 115 | 116 | return movies 117 | } 118 | 119 | // CountMovie returns the movie count 120 | func (d *TenDatabase) CountMovie(condition interface{}) string { 121 | if condition == nil { 122 | condition = bson.D{{}} 123 | } 124 | total, err := d.DB.Collection("movies").CountDocuments(context.Background(), condition, &options.CountOptions{}) 125 | if err != nil { 126 | return "0" 127 | } 128 | return strconv.Itoa(int(total)) 129 | } 130 | 131 | // DeleteMovieByID deletes a movie by its id. 132 | func (d *TenDatabase) DeleteMovieByID(id primitive.ObjectID) error { 133 | if d.CountPost(bson.D{{Key: "movieId", Value: id}}) == "0" { 134 | _, err := d.DB.Collection("movies").DeleteOne(context.Background(), bson.D{{Key: "_id", Value: id}}) 135 | return err 136 | } 137 | return errors.New("the current movie has posts published") 138 | } 139 | -------------------------------------------------------------------------------- /utils/spider/CategoriesStr.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | // 定义主类与子类的关系 4 | func CategoriesStr() string { 5 | categories := `[ 6 | { 7 | "link": "/?m=vod-type-id-1.html", 8 | "name": "电影片", 9 | "type_id": "1", 10 | "sub": [ 11 | { 12 | "link": "/?m=vod-type-id-6.html", 13 | "name": "动作片", 14 | "type_id": "6", 15 | "sub": null 16 | }, 17 | { 18 | "link": "/?m=vod-type-id-7.html", 19 | "name": "喜剧片", 20 | "type_id": "7", 21 | "sub": null 22 | }, 23 | { 24 | "link": "/?m=vod-type-id-8.html", 25 | "name": "爱情片", 26 | "type_id": "8", 27 | "sub": null 28 | }, 29 | { 30 | "link": "/?m=vod-type-id-9.html", 31 | "name": "科幻片", 32 | "type_id": "9", 33 | "sub": null 34 | }, 35 | { 36 | "link": "/?m=vod-type-id-10.html", 37 | "name": "恐怖片", 38 | "type_id": "10", 39 | "sub": null 40 | }, 41 | { 42 | "link": "/?m=vod-type-id-11.html", 43 | "name": "剧情片", 44 | "type_id": "11", 45 | "sub": null 46 | }, 47 | { 48 | "link": "/?m=vod-type-id-12.html", 49 | "name": "战争片", 50 | "type_id": "12", 51 | "sub": null 52 | }, 53 | { 54 | "link": "/?m=vod-type-id-20.html", 55 | "name": "纪录片", 56 | "type_id": "20", 57 | "sub": null 58 | }, 59 | { 60 | "link": "/?m=vod-type-id-21.html", 61 | "name": "微电影", 62 | "type_id": "21", 63 | "sub": null 64 | }, 65 | { 66 | "link": "/?m=vod-type-id-37.html", 67 | "name": "伦理片", 68 | "type_id": "37", 69 | "sub": null 70 | } 71 | ] 72 | }, 73 | { 74 | "link": "/?m=vod-type-id-2.html", 75 | "name": "连续剧", 76 | "type_id": "2", 77 | "sub": [ 78 | { 79 | "link": "/?m=vod-type-id-13.html", 80 | "name": "国产剧", 81 | "type_id": "13", 82 | "sub": null 83 | }, 84 | { 85 | "link": "/?m=vod-type-id-14.html", 86 | "name": "香港剧", 87 | "type_id": "14", 88 | "sub": null 89 | }, 90 | { 91 | "link": "/?m=vod-type-id-15.html", 92 | "name": "韩国剧", 93 | "type_id": "15", 94 | "sub": null 95 | }, 96 | { 97 | "link": "/?m=vod-type-id-16.html", 98 | "name": "欧美剧", 99 | "type_id": "16", 100 | "sub": null 101 | }, 102 | { 103 | "link": "/?m=vod-type-id-22.html", 104 | "name": "台湾剧", 105 | "type_id": "22", 106 | "sub": null 107 | }, 108 | { 109 | "link": "/?m=vod-type-id-23.html", 110 | "name": "日本剧", 111 | "type_id": "23", 112 | "sub": null 113 | }, 114 | { 115 | "link": "/?m=vod-type-id-24.html", 116 | "name": "海外剧", 117 | "type_id": "24", 118 | "sub": null 119 | } 120 | ] 121 | }, 122 | { 123 | "link": "/?m=vod-type-id-4.html", 124 | "name": "动漫片", 125 | "type_id": "4", 126 | "sub": [ 127 | { 128 | "link": "/?m=vod-type-id-29.html", 129 | "name": "国产动漫", 130 | "type_id": "29", 131 | "sub": null 132 | }, 133 | { 134 | "link": "/?m=vod-type-id-30.html", 135 | "name": "日韩动漫", 136 | "type_id": "30", 137 | "sub": null 138 | }, 139 | { 140 | "link": "/?m=vod-type-id-31.html", 141 | "name": "欧美动漫", 142 | "type_id": "31", 143 | "sub": null 144 | }, 145 | { 146 | "link": "/?m=vod-type-id-32.html", 147 | "name": "港台动漫", 148 | "type_id": "32", 149 | "sub": null 150 | }, 151 | { 152 | "link": "/?m=vod-type-id-33.html", 153 | "name": "海外动漫", 154 | "type_id": "33", 155 | "sub": null 156 | } 157 | ] 158 | } 159 | ]` 160 | 161 | return categories 162 | } 163 | -------------------------------------------------------------------------------- /static/style/mobile.css: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 500px) { 2 | .col-lg-1, 3 | .col-lg-10, 4 | .col-lg-11, 5 | .col-lg-12, 6 | .col-lg-2, 7 | .col-lg-3, 8 | .col-lg-4, 9 | .col-lg-5, 10 | .col-lg-6, 11 | .col-lg-7, 12 | .col-lg-8, 13 | .col-lg-9, 14 | .col-md-1, 15 | .col-md-10, 16 | .col-md-11, 17 | .col-md-12, 18 | .col-md-2, 19 | .col-md-3, 20 | .col-md-4, 21 | .col-md-5, 22 | .col-md-6, 23 | .col-md-7, 24 | .col-md-8, 25 | .col-md-9, 26 | .col-sm-1, 27 | .col-sm-10, 28 | .col-sm-11, 29 | .col-sm-12, 30 | .col-sm-2, 31 | .col-sm-3, 32 | .col-sm-4, 33 | .col-sm-5, 34 | .col-sm-6, 35 | .col-sm-7, 36 | .col-sm-8, 37 | .col-sm-9, 38 | .col-xs-1, 39 | .col-xs-10, 40 | .col-xs-11, 41 | .col-xs-12, 42 | .col-xs-2, 43 | .col-xs-3, 44 | .col-xs-4, 45 | .col-xs-5, 46 | .col-xs-6, 47 | .col-xs-7, 48 | .col-xs-8, 49 | .col-xs-9 { 50 | position: relative; 51 | min-height: 1px; 52 | padding-right: 8px; 53 | padding-left: 8px; 54 | } 55 | html, 56 | body { 57 | width: 100%; 58 | } 59 | body { 60 | background-color: #f4f5f5 !important; 61 | } 62 | .row { 63 | margin: 0; 64 | } 65 | .allCount { 66 | display: none; 67 | } 68 | .navbar { 69 | padding: 0!important; 70 | margin: 0!important; 71 | width: 100%!important; 72 | overflow: hidden; 73 | } 74 | .navbar .container { 75 | padding: 0; 76 | margin: 0; 77 | } 78 | .container { 79 | padding-right: 8px; 80 | padding-left: 8px; 81 | } 82 | .movieList { 83 | padding: 0; 84 | margin: 0 -5px; 85 | } 86 | .movieList .movieCard { 87 | padding: 5px; 88 | } 89 | .movieList .movieCard a { 90 | margin-bottom: 0px; 91 | } 92 | .blogSearch { 93 | display: none; 94 | } 95 | .breadcrumb { 96 | background-color: #f4f5f5 !important; 97 | } 98 | .topDiv { 99 | margin-bottom: 5px; 100 | margin-top: 6px; 101 | padding: 8px 15px 4px; 102 | position: relative; 103 | } 104 | .selectHeader { 105 | margin-bottom: 0px; 106 | background-color: #fff; 107 | border-radius: 5px; 108 | padding: 0; 109 | } 110 | .selectHeader ul { 111 | padding-left: 0 ; 112 | } 113 | .selectHeader h4 { 114 | width: 15%; 115 | vertical-align: middle; 116 | float: left; 117 | margin: 0px 0; 118 | padding: 3px 0; 119 | } 120 | .selectHeader div { 121 | width: 85%; 122 | overflow-x: auto; 123 | vertical-align: middle; 124 | float: right; 125 | margin: 0px 0; 126 | padding: 3px; 127 | background-color: #fafafa; 128 | } 129 | .selectHeader ul { 130 | width: 1000px; 131 | } 132 | .logoDiv { 133 | margin-right: 10px; 134 | } 135 | .logoDiv span { 136 | display: none; 137 | } 138 | .nav-left .pingpu li { 139 | padding: 0 5px; 140 | font-size: 15px; 141 | } 142 | .nav-right { 143 | display: none; 144 | } 145 | .movieCard img { 146 | height: auto; 147 | height: 270px; 148 | } 149 | .breadcrumb { 150 | display: block; 151 | overflow: hidden; 152 | white-space: nowrap; 153 | } 154 | .handBookCard a { 155 | padding: 0 10px; 156 | } 157 | .handBookCard .photoCard { 158 | left: 0; 159 | padding-left: 10px; 160 | } 161 | .blogList .blogCard { 162 | padding: 10px 0px; 163 | } 164 | .blogList .handBookCard { 165 | margin-right: 0; 166 | padding-right: 0; 167 | padding-left: 100px; 168 | } 169 | .blogList .handBookCard a { 170 | width: 100%; 171 | } 172 | .blogList .handBookCard .createdAt { 173 | display: none; 174 | } 175 | .classificationHeader ul li a { 176 | font-size: 14px; 177 | } 178 | .topDiv .searchTop .btn-default { 179 | display: none; 180 | } 181 | .blogList .blogCard .removeBlog { 182 | display: none!important; 183 | } 184 | .blogList .blogCard .removeBlog a { 185 | display: inline; 186 | } 187 | .handBookCreate .partList { 188 | width: 60px; 189 | overflow-x: hidden; 190 | } 191 | .handBookCreate .partList .title { 192 | display: none; 193 | } 194 | .handBookCreate .partList .listBody .listCard { 195 | padding: 10px; 196 | } 197 | .handBookPartView { 198 | padding: 55px 5px 40px 65px!important; 199 | } 200 | #handBookPart { 201 | padding: 10px; 202 | } 203 | .comment .listCard { 204 | padding: 10px; 205 | } 206 | .logoDiv img { 207 | margin-left: 10px; 208 | } 209 | .blogList { 210 | margin-top: 43px; 211 | } 212 | .searchTop .form-inline { 213 | height: 40px; 214 | position: absolute; 215 | top: 10px; 216 | right: 10px; 217 | } 218 | .searchTop .movieTypeList { 219 | margin-top: 18px; 220 | } 221 | .searchTop .movieTypeList .movieTypeItem { 222 | font-size: 14px; 223 | } 224 | .searchTop .movieTypeList .active { 225 | font-size: 18px; 226 | } 227 | #movieTypeZiList { 228 | width: 100%; 229 | margin-top: 30px; 230 | padding: 0; 231 | font-size: 13px; 232 | } 233 | .selectHeader h4 { 234 | font-size: 14px; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /static/style/mobile.less: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 500px) { 2 | .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-xs-1, .col-xs-10, .col-xs-11, .col-xs-12, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9 { 3 | position: relative; 4 | min-height: 1px; 5 | padding-right: 8px; 6 | padding-left: 8px; 7 | } 8 | 9 | html,body{ 10 | width: 100%; 11 | } 12 | body{ 13 | background-color: #f4f5f5!important; 14 | } 15 | .row { 16 | margin: 0; 17 | } 18 | .allCount{ 19 | display: none; 20 | } 21 | .navbar { 22 | padding: 0!important; 23 | margin: 0!important; 24 | width: 100%!important; 25 | overflow: hidden; 26 | 27 | .container{ 28 | padding: 0; 29 | margin: 0; 30 | } 31 | } 32 | .container { 33 | padding-right: 8px; 34 | padding-left: 8px; 35 | } 36 | 37 | .movieList { 38 | padding: 0; 39 | margin:0 -5px; 40 | .movieCard { 41 | padding: 5px; 42 | a { 43 | margin-bottom: 0px; 44 | } 45 | } 46 | } 47 | 48 | .blogSearch { 49 | display: none; 50 | } 51 | .breadcrumb{ 52 | background-color: #f4f5f5!important; 53 | } 54 | .topDiv{ 55 | margin-bottom: 5px; 56 | margin-top: 6px; 57 | padding: 8px 15px 4px; 58 | position: relative; 59 | } 60 | .selectHeader { 61 | margin-bottom: 0px; 62 | background-color: #fff; 63 | border-radius: 5px; 64 | padding: 0; 65 | ul{ 66 | padding-left:0 ; 67 | } 68 | h4 { 69 | width: 15%; 70 | vertical-align: middle; 71 | float: left; 72 | margin: 0px 0; 73 | padding: 3px 0; 74 | } 75 | 76 | div { 77 | width: 85%; 78 | overflow-x: auto; 79 | vertical-align: middle; 80 | float: right; 81 | margin: 0px 0; 82 | padding: 3px; 83 | background-color: #fafafa; 84 | } 85 | 86 | ul { 87 | width: 1000px; 88 | } 89 | } 90 | 91 | 92 | 93 | .logoDiv { 94 | margin-right: 10px; 95 | span{ 96 | display: none; 97 | } 98 | } 99 | 100 | .nav-left { 101 | .pingpu { 102 | li { 103 | padding: 0 5px; 104 | font-size: 15px; 105 | } 106 | } 107 | } 108 | 109 | .nav-right { 110 | display: none; 111 | } 112 | 113 | .movieCard img { 114 | height: auto; 115 | height: 270px; 116 | } 117 | 118 | .breadcrumb { 119 | display: block; 120 | overflow: hidden; 121 | white-space: nowrap; 122 | } 123 | .handBookCard a{ 124 | padding:0 10px; 125 | } 126 | 127 | .handBookCard .photoCard{ 128 | left: 0; 129 | padding-left: 10px; 130 | } 131 | .blogList .blogCard{ 132 | padding: 10px 0px; 133 | } 134 | .blogList .handBookCard{ 135 | margin-right: 0; 136 | padding-right: 0; 137 | padding-left: 100px; 138 | a{ 139 | width: 100%; 140 | } 141 | .createdAt{ 142 | display: none; 143 | } 144 | } 145 | .classificationHeader ul li a{ 146 | font-size: 14px; 147 | } 148 | .topDiv .searchTop .btn-default{ 149 | display: none; 150 | } 151 | .blogList .blogCard .removeBlog{ 152 | display: none!important; 153 | a{ 154 | display: inline; 155 | } 156 | } 157 | .handBookCreate .partList{ 158 | width: 60px; 159 | overflow-x: hidden; 160 | .title{ 161 | display: none; 162 | } 163 | .listBody .listCard{ 164 | padding: 10px; 165 | } 166 | } 167 | .handBookPartView{ 168 | padding: 55px 5px 40px 65px!important; 169 | } 170 | #handBookPart{ 171 | padding: 10px; 172 | } 173 | .comment .listCard{ 174 | padding: 10px; 175 | } 176 | .logoDiv img { 177 | margin-left: 10px; 178 | } 179 | .blogList{ 180 | margin-top: 43px; 181 | } 182 | .searchTop .form-inline{ 183 | height: 40px; 184 | position: absolute; 185 | top: 10px; 186 | right: 10px; 187 | } 188 | .searchTop .movieTypeList{ 189 | margin-top: 18px; 190 | .movieTypeItem{ 191 | font-size: 14px; 192 | } 193 | .active{ 194 | font-size: 18px; 195 | } 196 | } 197 | #movieTypeZiList{ 198 | width: 100%; 199 | margin-top: 30px; 200 | padding: 0; 201 | font-size: 13px; 202 | } 203 | .selectHeader h4{ 204 | font-size: 14px; 205 | 206 | } 207 | } 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /templates/movie/movie.html: -------------------------------------------------------------------------------- 1 | {{template "header.html" .}} 2 |
3 |
4 |
5 |
6 | 共 {{.allCount}}/{{.allMoviesCount}} 部 7 |
8 |
9 |
10 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |

年份

21 |
22 |
    23 | {{$activeYear := .activeYear}} 24 |
  • 全部 25 |
  • 26 | {{range $i, $v := .yearList }} 27 |
  • {{$v}} 28 |
  • 29 | {{end}} 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 | {{range $i, $v := .movies }} 38 | 56 | {{else}} 57 |

抱歉,没有更多影片了~我们正在加紧收录中。。。

58 | {{end}} 59 | 60 | 61 |
62 |
63 | 64 | 139 | 140 | {{template "pagination.html" .}} 141 | {{template "footer.html" .}} -------------------------------------------------------------------------------- /utils/Dingrobot.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | ) 15 | 16 | // @link https://github.com/royeo/dingrobot 17 | // Roboter is the interface implemented by Robot that can send multiple types of messages. 18 | type Roboter interface { 19 | SendText(content string, atMobiles []string, isAtAll bool) error 20 | SendLink(title, text, messageURL, picURL string) error 21 | SendMarkdown(title, text string, atMobiles []string, isAtAll bool) error 22 | SendActionCard(title, text, singleTitle, singleURL, btnOrientation, hideAvatar string) error 23 | SetSecret(secret string) 24 | } 25 | 26 | // Msg 27 | 28 | const ( 29 | msgTypeText = "text" 30 | msgTypeLink = "link" 31 | msgTypeMarkdown = "markdown" 32 | msgTypeActionCard = "actionCard" 33 | ) 34 | 35 | type textMessage struct { 36 | MsgType string `json:"msgtype"` 37 | Text textParams `json:"text"` 38 | At atParams `json:"at"` 39 | } 40 | 41 | type textParams struct { 42 | Content string `json:"content"` 43 | } 44 | 45 | type atParams struct { 46 | AtMobiles []string `json:"atMobiles,omitempty"` 47 | IsAtAll bool `json:"isAtAll,omitempty"` 48 | } 49 | 50 | type linkMessage struct { 51 | MsgType string `json:"msgtype"` 52 | Link linkParams `json:"link"` 53 | } 54 | 55 | type linkParams struct { 56 | Title string `json:"title"` 57 | Text string `json:"text"` 58 | MessageURL string `json:"messageUrl"` 59 | PicURL string `json:"picUrl,omitempty"` 60 | } 61 | 62 | type markdownMessage struct { 63 | MsgType string `json:"msgtype"` 64 | Markdown markdownParams `json:"markdown"` 65 | At atParams `json:"at"` 66 | } 67 | 68 | type markdownParams struct { 69 | Title string `json:"title"` 70 | Text string `json:"text"` 71 | } 72 | 73 | type actionCardMessage struct { 74 | MsgType string `json:"msgtype"` 75 | ActionCard actionCardParams `json:"actionCard"` 76 | } 77 | 78 | type actionCardParams struct { 79 | Title string `json:"title"` 80 | Text string `json:"text"` 81 | SingleTitle string `json:"singleTitle"` 82 | SingleURL string `json:"singleURL"` 83 | BtnOrientation string `json:"btnOrientation,omitempty"` 84 | HideAvatar string `json:"hideAvatar,omitempty"` 85 | } 86 | 87 | // Robot represents a dingtalk custom robot that can send messages to groups. 88 | type Robot struct { 89 | webHook string 90 | secret string 91 | } 92 | 93 | // NewRobot returns a roboter that can send messages. 94 | func NewRobot(webHook string) Roboter { 95 | return &Robot{webHook: webHook} 96 | } 97 | 98 | // SetSecret set the secret to add additional signature when send request 99 | func (r *Robot) SetSecret(secret string) { 100 | r.secret = secret 101 | } 102 | 103 | // SendText send a text type message. 104 | func (r Robot) SendText(content string, atMobiles []string, isAtAll bool) error { 105 | return r.send(&textMessage{ 106 | MsgType: msgTypeText, 107 | Text: textParams{ 108 | Content: content, 109 | }, 110 | At: atParams{ 111 | AtMobiles: atMobiles, 112 | IsAtAll: isAtAll, 113 | }, 114 | }) 115 | } 116 | 117 | // SendLink send a link type message. 118 | func (r Robot) SendLink(title, text, messageURL, picURL string) error { 119 | return r.send(&linkMessage{ 120 | MsgType: msgTypeLink, 121 | Link: linkParams{ 122 | Title: title, 123 | Text: text, 124 | MessageURL: messageURL, 125 | PicURL: picURL, 126 | }, 127 | }) 128 | } 129 | 130 | // SendMarkdown send a markdown type message. 131 | func (r Robot) SendMarkdown(title, text string, atMobiles []string, isAtAll bool) error { 132 | return r.send(&markdownMessage{ 133 | MsgType: msgTypeMarkdown, 134 | Markdown: markdownParams{ 135 | Title: title, 136 | Text: text, 137 | }, 138 | At: atParams{ 139 | AtMobiles: atMobiles, 140 | IsAtAll: isAtAll, 141 | }, 142 | }) 143 | } 144 | 145 | // SendActionCard send a action card type message. 146 | func (r Robot) SendActionCard(title, text, singleTitle, singleURL, btnOrientation, hideAvatar string) error { 147 | return r.send(&actionCardMessage{ 148 | MsgType: msgTypeActionCard, 149 | ActionCard: actionCardParams{ 150 | Title: title, 151 | Text: text, 152 | SingleTitle: singleTitle, 153 | SingleURL: singleURL, 154 | BtnOrientation: btnOrientation, 155 | HideAvatar: hideAvatar, 156 | }, 157 | }) 158 | } 159 | 160 | type dingResponse struct { 161 | Errcode int `json:"errcode"` 162 | Errmsg string `json:"errmsg"` 163 | } 164 | 165 | func (r Robot) send(msg interface{}) error { 166 | m, err := json.Marshal(msg) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | webURL := r.webHook 172 | if len(r.secret) != 0 { 173 | webURL += genSignedURL(r.secret) 174 | } 175 | resp, err := http.Post(webURL, "application/json", bytes.NewReader(m)) 176 | if err != nil { 177 | return err 178 | } 179 | defer resp.Body.Close() 180 | 181 | data, err := ioutil.ReadAll(resp.Body) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | var dr dingResponse 187 | err = json.Unmarshal(data, &dr) 188 | if err != nil { 189 | return err 190 | } 191 | if dr.Errcode != 0 { 192 | return fmt.Errorf("dingrobot send failed: %v", dr.Errmsg) 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func genSignedURL(secret string) string { 199 | timeStr := fmt.Sprintf("%d", time.Now().UnixNano()/1e6) 200 | sign := fmt.Sprintf("%s\n%s", timeStr, secret) 201 | signData := computeHmacSha256(sign, secret) 202 | encodeURL := url.QueryEscape(signData) 203 | return fmt.Sprintf("×tamp=%s&sign=%s", timeStr, encodeURL) 204 | } 205 | 206 | func computeHmacSha256(message string, secret string) string { 207 | key := []byte(secret) 208 | h := hmac.New(sha256.New, key) 209 | h.Write([]byte(message)) 210 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 211 | } 212 | -------------------------------------------------------------------------------- /templates/components/comment.html: -------------------------------------------------------------------------------- 1 |
2 |

评论

3 |
4 |
5 |
6 | <% if(req.session.user&&req.session.user.profile&&req.session.user.profile.picture){%> 7 | 8 | <% } else { %> 9 | 10 | <% }%> 11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 | 19 | 表情 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 45 |
46 | -------------------------------------------------------------------------------- /static/style/handBook.css: -------------------------------------------------------------------------------- 1 | .HandBookCreateModel { 2 | width: 100%; 3 | height: 100%; 4 | background-color: rgba(0, 0, 0, 0.4) !important; 5 | position: fixed; 6 | z-index: 100; 7 | top: 0; 8 | left: 0; 9 | } 10 | .HandBookCreateModel .form-group { 11 | height: 130px; 12 | margin-top: 20px; 13 | margin-bottom: 0px; 14 | } 15 | .HandBookCreateModel .form-group .headerImg { 16 | height: 120px; 17 | width: 90px; 18 | border: 1px solid #ccc; 19 | } 20 | .HandBookCreateModel .form-group .headerImg img { 21 | width: 88px; 22 | height: 118px; 23 | margin-top: 2px; 24 | } 25 | .HandBookCreateModel .form-group .a-upload { 26 | top: 44px; 27 | } 28 | .HandBookCreateModel .form-group .a-upload input { 29 | margin-top: 0px; 30 | } 31 | .HandBookCreateModel .body { 32 | width: 400px; 33 | height: 540px; 34 | position: relative; 35 | top: 50%; 36 | left: 50%; 37 | transform: translate(-50%, -50%); 38 | padding: 20px; 39 | background-color: #fff; 40 | border-radius: 8px; 41 | } 42 | .HandBookCreateModel .body h4 { 43 | color: #e59233; 44 | } 45 | .HandBookCreateModel .body input { 46 | width: 360px; 47 | height: 40px; 48 | margin-top: 12px; 49 | } 50 | .HandBookCreateModel .body textarea { 51 | width: 360px; 52 | margin-top: 10px; 53 | } 54 | .HandBookCreateModel .body .selTypes { 55 | color: #888; 56 | cursor: pointer; 57 | display: block; 58 | height: 40px; 59 | line-height: 40px; 60 | } 61 | .HandBookCreateModel .body .selTypes input { 62 | margin-top: 0px; 63 | border: 0px; 64 | width: 324px; 65 | } 66 | .HandBookCreateModel .body .selTypes .dropdown-toggle { 67 | font-size: 18px; 68 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 69 | } 70 | .HandBookCreateModel .body .selTypes .dropdown-toggle i { 71 | font-size: 20px; 72 | position: relative; 73 | top: 1px; 74 | right: -4px; 75 | } 76 | .HandBookCreateModel .body .selTypes .dropdown-toggle i:hover { 77 | color: #e59233; 78 | } 79 | .HandBookCreateModel .body .selTypes:hover { 80 | color: #555; 81 | } 82 | .HandBookCreateModel .body .btn { 83 | margin-top: 20px; 84 | height: 50px; 85 | font-size: 22px; 86 | } 87 | .HandBookCreateModel .body .close { 88 | position: fixed; 89 | top: 10px; 90 | right: 5px; 91 | font-size: 24px; 92 | color: #888; 93 | cursor: pointer; 94 | } 95 | .HandBookCreateModel .divider { 96 | margin: 0; 97 | } 98 | .HandBookCreateModel ul { 99 | padding-left: 0; 100 | text-align: center; 101 | } 102 | .HandBookCreateModel ul li { 103 | padding: 0; 104 | display: block; 105 | } 106 | .handBookCard { 107 | position: relative; 108 | } 109 | .handBookCard .photoCard { 110 | position: absolute; 111 | left: 40px; 112 | top: 20px; 113 | } 114 | .handBookCard .photoCard img { 115 | width: 90px; 116 | height: 120px; 117 | } 118 | .handBookCard a h4 { 119 | font-size: 18px !important; 120 | } 121 | .handBookCard .removeBlog { 122 | top: 58px !important; 123 | } 124 | .handBookCard .removeBlog a { 125 | padding: 0px; 126 | } 127 | .handBookCard a { 128 | padding: 0 130px; 129 | display: inline-block; 130 | min-height: 140px; 131 | } 132 | .handBookCard a p { 133 | display: -webkit-box; 134 | -webkit-box-orient: vertical; 135 | -webkit-line-clamp: 2; 136 | overflow: hidden; 137 | height: 46px; 138 | padding-top: 4px; 139 | color: #777; 140 | margin-bottom: 0; 141 | } 142 | .handBookCreate { 143 | width: 100%; 144 | position: relative; 145 | display: flex; 146 | } 147 | .handBookCreate .partList { 148 | width: 20%; 149 | box-shadow: 0 0px 5px 0 rgba(0, 0, 0, 0.1); 150 | position: fixed; 151 | top: 50px; 152 | height: 100%; 153 | overflow-y: auto; 154 | z-index: 1; 155 | background-color: #fff; 156 | padding-bottom: 100px; 157 | } 158 | .handBookCreate .partList .title { 159 | font-size: 18px; 160 | text-align: center; 161 | border-bottom: 1px dashed #ccc; 162 | padding: 15px; 163 | color: #e59133; 164 | font-size: 26px; 165 | overflow: hidden; 166 | text-overflow: ellipsis; 167 | white-space: nowrap; 168 | } 169 | .handBookCreate .partList .listBody { 170 | padding: 10px 0; 171 | } 172 | .handBookCreate .partList .listBody .listCard { 173 | padding: 15px 20px; 174 | display: flex; 175 | cursor: pointer; 176 | } 177 | .handBookCreate .partList .listBody .listCard:last-child .step .step-btn:after { 178 | height: 0; 179 | } 180 | .handBookCreate .partList .listBody .listCard:first-child .step .step-btn:before { 181 | height: 0; 182 | } 183 | .handBookCreate .partList .listBody .listCard:hover { 184 | background-color: #ebebeb; 185 | } 186 | .handBookCreate .partList .listBody .listCard:hover .step .step-btn { 187 | border-color: #e59233; 188 | color: #e59233; 189 | } 190 | .handBookCreate .partList .listBody .listCard:hover .step .step-btn:before { 191 | background-color: #e59233; 192 | } 193 | .handBookCreate .partList .listBody .listCard:hover .step .step-btn:after { 194 | background-color: #e59233; 195 | } 196 | .handBookCreate .partList .listBody .listCard .step { 197 | position: relative; 198 | margin: -20px 0; 199 | } 200 | .handBookCreate .partList .listBody .listCard .step .step-btn { 201 | width: 40px; 202 | height: 40px; 203 | border-radius: 50%; 204 | text-align: center; 205 | line-height: 36px; 206 | border: 2px solid #b5b7ba; 207 | font-size: 22px; 208 | margin-top: 20px; 209 | background-color: #fff; 210 | z-index: 1; 211 | position: relative; 212 | color: #b5b7ba; 213 | } 214 | .handBookCreate .partList .listBody .listCard .step .step-btn:before { 215 | z-index: 0; 216 | position: absolute; 217 | left: 50%; 218 | transform: translateX(-50%); 219 | width: 2px; 220 | background-color: #b5b7ba; 221 | height: 50%; 222 | content: ""; 223 | top: -50%; 224 | } 225 | .handBookCreate .partList .listBody .listCard .step .step-btn:after { 226 | z-index: 0; 227 | position: absolute; 228 | left: 50%; 229 | transform: translateX(-50%); 230 | width: 2px; 231 | background-color: #b5b7ba; 232 | height: 50%; 233 | content: ""; 234 | top: 100%; 235 | } 236 | .handBookCreate .partList .listBody .listCard .center { 237 | flex: 1; 238 | height: 40px; 239 | line-height: 40px; 240 | padding-left: 20px; 241 | font-size: 16px; 242 | overflow: hidden; 243 | text-overflow: ellipsis; 244 | white-space: nowrap; 245 | } 246 | .handBookCreate .partList .listBody .active { 247 | background-color: #ebebeb; 248 | } 249 | .handBookCreate .partList .listBody .active .step .step-btn { 250 | border-color: #e59233; 251 | color: #e59233; 252 | } 253 | .handBookCreate .partList .listBody .active .step .step-btn:before { 254 | background-color: #e59233; 255 | } 256 | .handBookCreate .partList .listBody .active .step .step-btn:after { 257 | background-color: #e59233; 258 | } 259 | .handBookCreate .partList .btn { 260 | width: 80%; 261 | height: 44px; 262 | margin-left: 10%; 263 | font-size: 18px; 264 | margin-top: 20px; 265 | } 266 | .handBookCreate .concent { 267 | flex: 1; 268 | padding-left: 20%; 269 | } 270 | .handBookCreate .addPart { 271 | display: none; 272 | padding: 0 20px; 273 | } 274 | .handBookCreate .addPart .btn { 275 | width: 30%; 276 | height: 40px; 277 | font-size: 16px; 278 | display: inline-block; 279 | } 280 | #handBookPart { 281 | max-width: 800px; 282 | margin-left: auto; 283 | margin-right: auto; 284 | padding: 30px; 285 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.15); 286 | background-color: #fff; 287 | border-radius: 2px; 288 | box-sizing: border-box; 289 | min-height: 100%; 290 | } 291 | .handBookPartView { 292 | position: fixed; 293 | width: 100%; 294 | height: 100%; 295 | top: 0; 296 | padding: 80px 0 40px; 297 | overflow-y: auto; 298 | } 299 | .main-body .vditor-toolbar label input { 300 | width: 32px; 301 | height: 32px; 302 | opacity: 0.001; 303 | overflow: hidden; 304 | top: -8px; 305 | left: -10px; 306 | cursor: pointer; 307 | z-index: 3; 308 | } 309 | .moveBtn { 310 | cursor: move; 311 | cursor: -webkit-grabbing; 312 | height: 40px; 313 | line-height: 40px; 314 | color: #888; 315 | } 316 | .moveBtn i { 317 | font-size: 20px; 318 | } 319 | .moveBtn:hover i { 320 | color: red; 321 | } 322 | -------------------------------------------------------------------------------- /static/style/handBook.less: -------------------------------------------------------------------------------- 1 | @import "./config.less"; 2 | 3 | 4 | .HandBookCreateModel { 5 | width: 100%; 6 | height: 100%; 7 | background-color: rgba(0, 0, 0, 0.4) !important; 8 | position: fixed; 9 | z-index: 100; 10 | top: 0; 11 | left: 0; 12 | 13 | .form-group { 14 | height: 130px; 15 | margin-top: 20px; 16 | margin-bottom: 0px; 17 | 18 | .headerImg { 19 | height: 120px; 20 | width: 90px; 21 | border: 1px solid #ccc; 22 | 23 | img { 24 | width: 88px; 25 | height: 118px; 26 | margin-top: 2px; 27 | } 28 | } 29 | 30 | .a-upload { 31 | top: 44px; 32 | 33 | input { 34 | margin-top: 0px; 35 | } 36 | } 37 | } 38 | 39 | .body { 40 | width: 400px; 41 | height: 540px; 42 | position: relative; 43 | top: 50%; 44 | left: 50%; 45 | transform: translate(-50%, -50%); 46 | padding: 20px; 47 | background-color: #fff; 48 | border-radius: 8px; 49 | 50 | h4 { 51 | color: @mainColor; 52 | } 53 | 54 | input { 55 | width: 360px; 56 | height: 40px; 57 | margin-top: 12px; 58 | } 59 | 60 | textarea { 61 | width: 360px; 62 | margin-top: 10px; 63 | } 64 | 65 | .selTypes { 66 | color: #888; 67 | cursor: pointer; 68 | display: block; 69 | height: 40px; 70 | line-height: 40px; 71 | 72 | input { 73 | margin-top: 0px; 74 | border: 0px; 75 | width: 324px; 76 | } 77 | 78 | .dropdown-toggle { 79 | font-size: 18px; 80 | box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0) !important; 81 | 82 | i { 83 | font-size: 20px; 84 | position: relative; 85 | top: 1px; 86 | right: -4px; 87 | 88 | &:hover { 89 | color: @mainColor; 90 | } 91 | } 92 | } 93 | 94 | &:hover { 95 | color: #555 96 | } 97 | } 98 | 99 | 100 | .btn { 101 | margin-top: 20px; 102 | height: 50px; 103 | font-size: 22px; 104 | } 105 | 106 | .close { 107 | position: fixed; 108 | top: 10px; 109 | right: 5px; 110 | font-size: 24px; 111 | color: #888; 112 | cursor: pointer; 113 | } 114 | } 115 | 116 | .divider { 117 | margin: 0; 118 | } 119 | 120 | ul { 121 | padding-left: 0; 122 | text-align: center; 123 | 124 | li { 125 | padding: 0; 126 | display: block; 127 | } 128 | } 129 | 130 | } 131 | 132 | .handBookCard { 133 | position: relative; 134 | 135 | .photoCard { 136 | position: absolute; 137 | left: 40px; 138 | top: 20px; 139 | 140 | img { 141 | width: 90px; 142 | height: 120px; 143 | } 144 | 145 | } 146 | 147 | a h4 { 148 | font-size: 18px !important; 149 | } 150 | 151 | .removeBlog { 152 | top: 58px !important; 153 | 154 | a { 155 | 156 | padding: 0px; 157 | } 158 | } 159 | 160 | a { 161 | padding: 0 130px; 162 | display: inline-block; 163 | min-height: 140px; 164 | 165 | p { 166 | display: -webkit-box; 167 | -webkit-box-orient: vertical; 168 | -webkit-line-clamp: 2; 169 | overflow: hidden; 170 | height: 46px; 171 | padding-top: 4px; 172 | color: #777; 173 | margin-bottom: 0; 174 | } 175 | } 176 | } 177 | 178 | .handBookCreate { 179 | width: 100%; 180 | position: relative; 181 | display: flex; 182 | 183 | .partList { 184 | width: 20%; 185 | box-shadow: 0 0px 5px 0 rgba(0, 0, 0, 0.1); 186 | position: fixed; 187 | top: 50px; 188 | height: 100%; 189 | overflow-y: auto; 190 | z-index: 1; 191 | background-color: #fff; 192 | padding-bottom: 100px; 193 | .title { 194 | font-size: 18px; 195 | text-align: center; 196 | border-bottom: 1px dashed #ccc; 197 | padding: 15px; 198 | color: #e59133; 199 | font-size: 26px; 200 | overflow: hidden; 201 | text-overflow: ellipsis; 202 | white-space: nowrap; 203 | } 204 | 205 | .listBody { 206 | padding: 10px 0; 207 | 208 | .listCard { 209 | padding: 15px 20px; 210 | display: flex; 211 | cursor: pointer; 212 | 213 | &:last-child .step .step-btn:after { 214 | height: 0; 215 | } 216 | 217 | &:first-child .step .step-btn:before { 218 | height: 0; 219 | } 220 | 221 | &:hover { 222 | background-color: #ebebeb; 223 | 224 | .step .step-btn { 225 | border-color: @mainColor; 226 | color: @mainColor; 227 | 228 | &:before { 229 | background-color: @mainColor 230 | } 231 | 232 | &:after { 233 | background-color: @mainColor 234 | } 235 | } 236 | } 237 | 238 | .step { 239 | position: relative; 240 | margin: -20px 0; 241 | 242 | .step-btn { 243 | width: 40px; 244 | height: 40px; 245 | border-radius: 50%; 246 | text-align: center; 247 | line-height: 36px; 248 | border: 2px solid #b5b7ba; 249 | font-size: 22px; 250 | margin-top: 20px; 251 | background-color: #fff; 252 | z-index: 1; 253 | position: relative; 254 | color: #b5b7ba; 255 | 256 | &:before { 257 | z-index: 0; 258 | position: absolute; 259 | left: 50%; 260 | transform: translateX(-50%); 261 | width: 2px; 262 | background-color: #b5b7ba; 263 | height: 50%; 264 | content: ""; 265 | top: -50%; 266 | } 267 | 268 | &:after { 269 | z-index: 0; 270 | position: absolute; 271 | left: 50%; 272 | transform: translateX(-50%); 273 | width: 2px; 274 | background-color: #b5b7ba; 275 | height: 50%; 276 | content: ""; 277 | top: 100%; 278 | } 279 | } 280 | } 281 | 282 | .center { 283 | flex: 1; 284 | height: 40px; 285 | line-height: 40px; 286 | padding-left: 20px; 287 | font-size: 16px; 288 | overflow: hidden; 289 | text-overflow: ellipsis; 290 | white-space: nowrap; 291 | } 292 | } 293 | 294 | .active { 295 | background-color: #ebebeb; 296 | 297 | .step .step-btn { 298 | border-color: @mainColor; 299 | color: @mainColor; 300 | 301 | &:before { 302 | background-color: @mainColor 303 | } 304 | 305 | &:after { 306 | background-color: @mainColor 307 | } 308 | } 309 | } 310 | } 311 | 312 | .btn { 313 | width: 80%; 314 | height: 44px; 315 | margin-left: 10%; 316 | font-size: 18px; 317 | margin-top: 20px; 318 | } 319 | } 320 | 321 | .concent { 322 | flex: 1; 323 | padding-left: 20%; 324 | } 325 | 326 | .addPart { 327 | display: none; 328 | padding: 0 20px; 329 | 330 | .btn { 331 | width: 30%; 332 | height: 40px; 333 | font-size: 16px; 334 | display: inline-block; 335 | } 336 | } 337 | } 338 | 339 | #handBookPart { 340 | max-width: 800px; 341 | margin-left: auto; 342 | margin-right: auto; 343 | padding: 30px; 344 | box-shadow: 1px 1px 8px rgba(0, 0, 0, .15); 345 | background-color: #fff; 346 | border-radius: 2px; 347 | box-sizing: border-box; 348 | min-height: 100%; 349 | } 350 | 351 | .handBookPartView { 352 | position: fixed; 353 | width: 100%; 354 | height: 100%; 355 | top: 0; 356 | padding: 80px 0 40px; 357 | overflow-y: auto; 358 | } 359 | 360 | .main-body .vditor-toolbar label input { 361 | width: 32px; 362 | height: 32px; 363 | opacity: 0.001; 364 | overflow: hidden; 365 | top: -8px; 366 | left: -10px; 367 | cursor: pointer; 368 | z-index: 3; 369 | 370 | } 371 | 372 | .main-body .vditor-toolbar label{ 373 | 374 | } 375 | 376 | .moveBtn{ 377 | cursor: move; 378 | cursor: -webkit-grabbing; 379 | height: 40px; 380 | line-height: 40px; 381 | color: #888; 382 | i{ 383 | font-size: 20px; 384 | } 385 | &:hover{ 386 | i{ 387 | color:red; 388 | } 389 | } 390 | } -------------------------------------------------------------------------------- /utils/spider/SpiderApi.go: -------------------------------------------------------------------------------- 1 | package spider 2 | 3 | import ( 4 | "log" 5 | "movie_spider/config" 6 | "movie_spider/database" 7 | "movie_spider/model" 8 | "movie_spider/utils" 9 | "regexp" 10 | "runtime" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/go-redis/redis/v7" 17 | "github.com/panjf2000/ants/v2" 18 | "github.com/spf13/viper" 19 | "github.com/valyala/fasthttp" 20 | "go.mongodb.org/mongo-driver/bson/primitive" 21 | ) 22 | 23 | // 爬取网站的域名,如访问不了,以下几个域名建议重复替换使用 24 | //const host = "http://www.jisudhw.com" 25 | //const host = "http://www.okzy.co" 26 | //const host = "http://www.okzyw.com" 27 | 28 | const ApiHost = "https://api.okzy.tv/api.php/provide/vod" 29 | const AcList = "list" 30 | const AcDetail = "detail" 31 | 32 | type SpiderApi struct { 33 | utils.SpiderTask 34 | } 35 | 36 | type Lists struct { 37 | VodId int `json:"vod_id"` // 如果json中vod_id不是“1”,而是 1 ,这里一定要声明为 int !!!fuck 不愧是静态强类型 38 | VodName string `json:"vod_name"` 39 | TypeId int `json:"type_id"` 40 | TypeName string `json:"type_name"` 41 | VodEn string `json:"vod_en"` 42 | VodTime string `json:"vod_time"` 43 | VodRemarks string `json:"vod_remarks"` 44 | VodPlayFrom string `json:"vod_play_from"` 45 | VodPlayUrl string `json:"vod_play_url"` 46 | VodPDownUrl string `json:"vod_down_url"` 47 | VodPic string `json:"vod_pic"` 48 | VodArea string `json:"vod_area"` 49 | VodDirector string `json:"vod_director"` 50 | VodLang string `json:"vod_lang"` 51 | VodYear string `json:"vod_year"` 52 | VodSub string `json:"vod_sub"` 53 | VodDuration string `json:"vod_duration"` 54 | VodActor string `json:"vod_actor"` 55 | VodContent string `json:"vod_content"` 56 | VodPointsPlay int `json:"vod_points_play"` 57 | VodScore string `json:"vod_score"` 58 | } 59 | 60 | type ResData struct { 61 | Msg string `json:"msg"` 62 | Code int `json:"code"` 63 | Page string `json:"page"` 64 | PageCount int `json:"pagecount"` 65 | Limit string `json:"limit"` 66 | Total int `json:"total"` 67 | List []Lists `json:"list"` 68 | } 69 | 70 | type Categories struct { 71 | Link string `json:"link"` 72 | Name string `json:"name"` 73 | TypeId string `json:"type_id"` 74 | Sub []Categories `json:"sub"` 75 | } 76 | 77 | type FastHttp struct { 78 | f fasthttp.Client 79 | req *fasthttp.Request 80 | resp *fasthttp.Response 81 | } 82 | 83 | type CatePageCount struct { 84 | categoryId string 85 | PageCount int 86 | } 87 | 88 | var ( 89 | Smutex sync.Mutex 90 | wg sync.WaitGroup 91 | timeOut = 10 * time.Second // 请求超时时间 92 | ) 93 | 94 | func (spiderApi *SpiderApi) Start() { 95 | go StartApi() 96 | } 97 | 98 | func (spiderApi *SpiderApi) PageDetail(id string) { 99 | go Detail(id, 0) 100 | } 101 | 102 | // 初始化 fasthttp GET 的请求与响应 103 | // @deprecated 104 | func initFastHttp() FastHttp { 105 | req := fasthttp.AcquireRequest() 106 | resp := fasthttp.AcquireResponse() 107 | defer func() { 108 | // 用完需要释放资源 109 | fasthttp.ReleaseResponse(resp) 110 | fasthttp.ReleaseRequest(req) 111 | }() 112 | 113 | req.Header.SetMethod("GET") 114 | 115 | // 不验证https证书 todo 这里根据实际情况是否选择 116 | //f := fasthttp.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}} 117 | f := fasthttp.Client{} 118 | 119 | return FastHttp{f: f, req: req, resp: resp} 120 | } 121 | 122 | func StartApi() { 123 | log.Println("开始执行爬虫...") 124 | list(1) 125 | } 126 | 127 | func list(pg int) { 128 | // 执行时间标记 129 | log.Println(pg) 130 | log.Println("执行爬虫 开始 list...") 131 | startTime := time.Now() 132 | defer ants.Release() 133 | antPool, _ := ants.NewPool(100) 134 | 135 | //_f := initFastHttp() 136 | 137 | catePageCounts := getCategoryPageCount() 138 | 139 | log.Println(catePageCounts) 140 | 141 | for _, catePageCount := range catePageCounts { 142 | wg.Add(1) 143 | categoryId := catePageCount.categoryId 144 | PageCount := catePageCount.PageCount 145 | 146 | antPool.Submit(func() { 147 | // 这里不能直接使用 catePageCount.categoryId 、catePageCount.PageCount 148 | // 在 submit 之前赋值变量传进来 149 | actionList(categoryId, pg, PageCount) 150 | wg.Done() 151 | }) 152 | 153 | } 154 | 155 | wg.Wait() 156 | 157 | // 结束时间标记 158 | endTime := time.Since(startTime) 159 | 160 | ExecSecondsS := strconv.FormatFloat(endTime.Seconds(), 'f', -1, 64) 161 | ExecMinutesS := strconv.FormatFloat(endTime.Minutes(), 'f', -1, 64) 162 | ExecHoursS := strconv.FormatFloat(endTime.Hours(), 'f', -1, 64) 163 | 164 | log.Println("执行完成......") 165 | 166 | // 删除已缓存的页面 167 | go DelAllListCacheKey() 168 | 169 | // 钉钉通知 170 | SendDingMsg("本次爬虫执行时间为:" + ExecSecondsS + "秒 \n 即" + ExecMinutesS + "分钟 \n 即" + ExecHoursS + "小时 \n " + runtime.GOOS) 171 | 172 | } 173 | 174 | func actionList(subCategoryId string, pg int, pageCount int) { 175 | 176 | //return 177 | for j := pg; j <= pageCount; j++ { 178 | 179 | url := ApiHost + "?ac=" + AcList + "&t=" + subCategoryId + "&pg=" + strconv.Itoa(j) 180 | req := fasthttp.AcquireRequest() 181 | resp := fasthttp.AcquireResponse() 182 | defer func() { 183 | // 用完需要释放资源 184 | fasthttp.ReleaseResponse(resp) 185 | fasthttp.ReleaseRequest(req) 186 | }() 187 | 188 | req.Header.SetMethod("GET") 189 | 190 | // log.Println("当前page"+strconv.Itoa(j), url, pageCount) 191 | 192 | RandomUserAgent := RandomUserAgent() 193 | req.Header.SetBytesKV([]byte("User-Agent"), []byte(RandomUserAgent)) 194 | 195 | req.SetRequestURI(url) 196 | 197 | if err := fasthttp.Do(req, resp); err != nil { 198 | log.Println("actionList 请求失败:", err.Error()) 199 | return 200 | } 201 | 202 | body := resp.Body() 203 | 204 | var nav ResData 205 | err := utils.Json.Unmarshal(body, &nav) 206 | if err != nil { 207 | log.Println(err) 208 | } 209 | 210 | for _, value := range nav.List { 211 | // 模板时间 212 | timeTemplate := "2006-01-02 15:04:05" 213 | stamp1, _ := time.ParseInLocation(timeTemplate, value.VodTime, time.Local) 214 | 215 | utils.RedisDB.ZAdd("detail_links:id:"+strconv.Itoa(value.TypeId), &redis.Z{ 216 | Score: float64(stamp1.Unix()), 217 | Member: `/?m=vod-detail-id-` + strconv.Itoa(value.VodId) + `.html`, 218 | }) 219 | 220 | film := []int{6, 7, 8, 9, 10, 11, 12, 20, 21, 37} 221 | tv := []int{13, 14, 15, 16, 22, 23, 24} 222 | cartoon := []int{29, 30, 31, 32, 33} 223 | 224 | if inType(value.TypeId, film) { 225 | utils.RedisDB.ZAdd("detail_links:id:1", &redis.Z{ 226 | Score: float64(stamp1.Unix()), 227 | Member: `/?m=vod-detail-id-` + strconv.Itoa(value.VodId) + `.html`, 228 | }) 229 | } 230 | 231 | if inType(value.TypeId, tv) { 232 | utils.RedisDB.ZAdd("detail_links:id:2", &redis.Z{ 233 | Score: float64(stamp1.Unix()), 234 | Member: `/?m=vod-detail-id-` + strconv.Itoa(value.VodId) + `.html`, 235 | }) 236 | } 237 | 238 | if inType(value.TypeId, cartoon) { 239 | utils.RedisDB.ZAdd("detail_links:id:4", &redis.Z{ 240 | Score: float64(stamp1.Unix()), 241 | Member: `/?m=vod-detail-id-` + strconv.Itoa(value.VodId) + `.html`, 242 | }) 243 | } 244 | 245 | // 获取详情 246 | Detail(strconv.Itoa(value.VodId), 0) 247 | 248 | } 249 | } 250 | 251 | } 252 | 253 | func pageCount(subCategoryId string) (int, string) { 254 | url := ApiHost + "?ac=" + AcList + "&t=" + subCategoryId + "&pg=1" 255 | 256 | req := fasthttp.AcquireRequest() 257 | resp := fasthttp.AcquireResponse() 258 | defer func() { 259 | // 用完需要释放资源 260 | fasthttp.ReleaseResponse(resp) 261 | fasthttp.ReleaseRequest(req) 262 | }() 263 | 264 | req.Header.SetMethod("GET") 265 | 266 | RandomUserAgent := RandomUserAgent() 267 | req.Header.SetBytesKV([]byte("User-Agent"), []byte(RandomUserAgent)) 268 | 269 | req.SetRequestURI(url) 270 | 271 | if err := fasthttp.Do(req, resp); err != nil { 272 | log.Println("pageCount 请求失败:", url, err.Error()) 273 | pageCount(subCategoryId) 274 | //return 0, subCategoryId 275 | } 276 | 277 | body := resp.Body() 278 | 279 | var nav ResData 280 | err := utils.Json.Unmarshal(body, &nav) 281 | if err != nil { 282 | log.Println(err) 283 | } 284 | 285 | PageCount := nav.PageCount 286 | log.Println("获取总页数", url, "PageCount", PageCount, "subCategoryId", subCategoryId) 287 | return PageCount, subCategoryId 288 | } 289 | 290 | // id与旧的网页爬虫对应不上 291 | func Detail(id string, retry int) { 292 | // movies_detail:/?m=vod-detail-id-10051.html:movie_name:第102次相亲 293 | url := ApiHost + "?ac=" + AcDetail + "&ids=" + id + "&pg=1" 294 | 295 | retryMax := 3 296 | if retry >= retryMax { 297 | log.Println("重试已结束", url, retry) 298 | return 299 | } 300 | 301 | req := fasthttp.AcquireRequest() 302 | resp := fasthttp.AcquireResponse() 303 | defer func() { 304 | // 用完需要释放资源 305 | fasthttp.ReleaseResponse(resp) 306 | fasthttp.ReleaseRequest(req) 307 | }() 308 | 309 | req.Header.SetMethod("GET") 310 | 311 | RandomUserAgent := RandomUserAgent() 312 | req.Header.SetBytesKV([]byte("User-Agent"), []byte(RandomUserAgent)) 313 | 314 | req.SetRequestURI(url) 315 | 316 | if err := fasthttp.Do(req, resp); err != nil { 317 | log.Println("Detail 请求失败:", err.Error()) 318 | return 319 | } 320 | 321 | body := resp.Body() 322 | 323 | var nav ResData 324 | err := utils.Json.Unmarshal(body, &nav) 325 | if err != nil { 326 | log.Println(err) 327 | } 328 | 329 | if len(nav.List) <= 0 { 330 | log.Println("没有list", url) 331 | 332 | // 重试 333 | for { 334 | if retry > retryMax { 335 | log.Println("超过最大重试次数,重试机制已跳出", url, retry) 336 | break 337 | } 338 | retry++ 339 | Detail(id, retry) 340 | log.Println("正在重试...", url, retry) 341 | } 342 | 343 | return 344 | } 345 | listDetail := nav.List[0] 346 | 347 | var _moviesInfo model.Movie 348 | 349 | var kuyunAry []map[string]string 350 | 351 | var ckm3u8Ary []map[string]string 352 | // 353 | var downloadAry []map[string]string 354 | 355 | kuyun, ckm3u8 := FormatVodPlayUrl(listDetail.VodPlayUrl) 356 | 357 | mp4 := FormatVodPDownUrl(listDetail.VodPDownUrl) 358 | 359 | for ik, kuyunValue := range kuyun { 360 | k := map[string]string{ 361 | "episode": strconv.Itoa(ik + 1), 362 | "play_link": kuyunValue} 363 | Smutex.Lock() 364 | kuyunAry = append(kuyunAry, k) 365 | Smutex.Unlock() 366 | } 367 | 368 | for ic, ckm3u8Value := range ckm3u8 { 369 | c := map[string]string{ 370 | "episode": strconv.Itoa(ic + 1), 371 | "play_link": ckm3u8Value} 372 | Smutex.Lock() 373 | ckm3u8Ary = append(ckm3u8Ary, c) 374 | Smutex.Unlock() 375 | } 376 | 377 | for im, mp4Value := range mp4 { 378 | m := map[string]string{ 379 | "episode": strconv.Itoa(im + 1), 380 | "play_link": mp4Value} 381 | Smutex.Lock() 382 | downloadAry = append(downloadAry, m) 383 | Smutex.Unlock() 384 | } 385 | 386 | // log.Println(listDetail) 387 | 388 | link := `/?m=vod-detail-id-` + strconv.Itoa(listDetail.VodId) + `.html` 389 | _moviesInfo.ID = primitive.NewObjectID() 390 | _moviesInfo.Link = link 391 | _moviesInfo.Cover = listDetail.VodPic 392 | _moviesInfo.Name = listDetail.VodName 393 | _moviesInfo.Quality = listDetail.VodRemarks 394 | _moviesInfo.Score = listDetail.VodScore 395 | _moviesInfo.Kuyun = kuyunAry 396 | _moviesInfo.Ckm3u8 = ckm3u8Ary 397 | _moviesInfo.Download = downloadAry 398 | _moviesInfo.TypeID = listDetail.TypeId 399 | _moviesInfo.Released = listDetail.VodYear 400 | _moviesInfo.Area = listDetail.VodArea 401 | _moviesInfo.Language = listDetail.VodLang 402 | _moviesInfo.UpdateTime = listDetail.VodTime 403 | 404 | mDetail := make(map[string]interface{}) 405 | mDetail["alias"] = listDetail.VodSub 406 | mDetail["director"] = listDetail.VodDirector 407 | mDetail["starring"] = listDetail.VodActor 408 | mDetail["type"] = listDetail.TypeName 409 | mDetail["typeId"] = listDetail.TypeId 410 | mDetail["area"] = listDetail.VodArea 411 | mDetail["language"] = listDetail.VodLang 412 | mDetail["released"] = listDetail.VodYear 413 | mDetail["length"] = listDetail.VodDuration 414 | mDetail["update"] = listDetail.VodTime 415 | mDetail["total_playback"] = listDetail.VodPointsPlay 416 | mDetail["vod_play_info"] = listDetail.VodContent 417 | 418 | _moviesInfo.Detail = mDetail 419 | insteadMovie(&_moviesInfo) 420 | 421 | //redisDB 只存储字符串 422 | // kuyunAryJSON, _ := utils.Json.MarshalIndent(kuyunAry, "", " ") 423 | // ckm3u8AryJSON, _ := utils.Json.MarshalIndent(ckm3u8Ary, "", " ") 424 | // downloadAryJSON, _ := utils.Json.MarshalIndent(downloadAry, "", " ") 425 | // _moviesInfo["kuyun"] = string(kuyunAryJSON) 426 | // _moviesInfo["ckm3u8"] = string(ckm3u8AryJSON) 427 | // _moviesInfo["download"] = string(downloadAryJSON) 428 | // _detail, _ := utils.Json.MarshalIndent(mDetail, "", " ") 429 | // _moviesInfo["detail"] = string(_detail) 430 | // t := utils.RedisDB.HMSet("movies_detail:"+link+":movie_name:"+listDetail.VodName, _moviesInfo).Err() 431 | // log.Println(t) 432 | } 433 | 434 | //储存电影到数据库 435 | func insteadMovie(_moviesInfo *model.Movie) { 436 | //电影存储 437 | conf := config.Get() 438 | db, err := database.New(conf.Database.Connection, conf.Database.Dbname, conf.Database.Username, conf.Database.Password) 439 | if err != nil { 440 | log.Fatal(err) 441 | } 442 | defer db.Close() 443 | movie := db.GetMovieByName(_moviesInfo.Name) 444 | if movie != nil { 445 | log.Println("该电影《" + movie.Name + "》已存在") 446 | err := db.FindOneAndReplace(_moviesInfo) 447 | if err != nil { 448 | log.Println(err) 449 | } 450 | } else { 451 | err := db.CreateMovie(_moviesInfo) 452 | if err == nil { 453 | log.Println("已存储电影:《" + _moviesInfo.Name + "》") 454 | } else { 455 | log.Println(err) 456 | } 457 | } 458 | } 459 | 460 | // 获取所有类别ID 461 | func subCategoryIds() []string { 462 | var nav []Categories 463 | categoriesStr := CategoriesStr() 464 | 465 | if utils.RedisDB.Exists("categories").Val() == 0 { 466 | utils.RedisDB.Set("categories", categoriesStr, 0).Err() 467 | log.Println("categories", categoriesStr) 468 | } 469 | 470 | err := utils.Json.Unmarshal([]byte(categoriesStr), &nav) 471 | if err != nil { 472 | log.Println(err) 473 | } 474 | 475 | CategoryIds := make([]string, 0) 476 | for _, value := range nav { 477 | for _, subValue := range value.Sub { 478 | Smutex.Lock() 479 | CategoryIds = append(CategoryIds, subValue.TypeId) 480 | Smutex.Unlock() 481 | } 482 | } 483 | 484 | return CategoryIds 485 | } 486 | 487 | // 获取每个类别对应的总数 488 | func getCategoryPageCount() []CatePageCount { 489 | subCategoryIds := subCategoryIds() 490 | 491 | var CatePageCounts []CatePageCount 492 | 493 | for _, subCategoryId := range subCategoryIds { 494 | 495 | pageCount, t := pageCount(subCategoryId) 496 | 497 | CatePageCount := CatePageCount{ 498 | categoryId: t, 499 | PageCount: pageCount, 500 | } 501 | 502 | Smutex.Lock() 503 | CatePageCounts = append(CatePageCounts, CatePageCount) 504 | Smutex.Unlock() 505 | } 506 | 507 | return CatePageCounts 508 | } 509 | 510 | func FormatVodPlayUrl(VodPlayUrl string) ([]string, []string) { 511 | 512 | SplitVodPlayUrl := strings.Split(VodPlayUrl, "$$$") 513 | 514 | r, _ := regexp.Compile("https?://([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?") 515 | 516 | // 这里剧集好像是 kuyun 在前面 [0] m3u8 在后面 [1] ,电影则是相反的。。。 517 | // 暂时先不处理,直接在播放列表通过播放地址的后缀区分 518 | kuyun := r.FindAllString(SplitVodPlayUrl[0], -1) 519 | 520 | ckm3u8 := []string{""} 521 | if len(SplitVodPlayUrl) >= 2 { 522 | ckm3u8 = r.FindAllString(SplitVodPlayUrl[1], -1) 523 | } 524 | 525 | return kuyun, ckm3u8 526 | } 527 | 528 | func FormatVodPDownUrl(VodPDownUrl string) []string { 529 | 530 | // todo: 对中文之后的直接过滤掉了,干! 531 | // (https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|] 532 | //r, _ := regexp.Compile("https?://([\\w-]+\\.)+[\\w-]+(/[\\w-./?%&=]*)?") 533 | // 534 | //mp4 := r.FindAllString(VodPDownUrl, -1) 535 | // 536 | //return mp4 537 | 538 | c := strings.Split(VodPDownUrl, "$") 539 | 540 | shift := c[1:] // 去掉第一个元素,一般是切割出来没用的 541 | 542 | urls := make([]string, 0) 543 | 544 | // http://xz3-7.okzyxz.com/20190524/23916_07fb2078/死亡地带S01E01.mp4#第02集 545 | // 处理链接后面的#号符 546 | for _, v := range shift { 547 | split := strings.Split(v, "#") 548 | Smutex.Lock() 549 | urls = append(urls, split[0]) 550 | Smutex.Unlock() 551 | } 552 | 553 | return urls 554 | } 555 | 556 | func inType(s int, d []int) bool { 557 | for _, k := range d { 558 | if s == k { 559 | return true 560 | } 561 | } 562 | return false 563 | } 564 | 565 | func SendDingMsg(msg string) { 566 | accessToken := viper.GetString(`ding.access_token`) 567 | if accessToken == "" { 568 | return 569 | } 570 | webhook := "https://oapi.dingtalk.com/robot/send?access_token=" + accessToken 571 | robot := utils.NewRobot(webhook) 572 | 573 | title := "goMovies 爬虫通知API" 574 | text := "#### goMovies 爬虫通知API \n " + msg 575 | atMobiles := []string{""} 576 | isAtAll := true 577 | 578 | err := robot.SendMarkdown(title, text, atMobiles, isAtAll) 579 | if err != nil { 580 | log.Println(err) 581 | return 582 | } 583 | 584 | log.Println("已发送钉钉通知") 585 | } 586 | 587 | func DelAllListCacheKey() { 588 | 589 | AllListCacheKey := utils.RedisDB.Keys("movie_lists_key:detail_links:*").Val() 590 | 591 | // 删除已经缓存的数据 592 | for _, val := range AllListCacheKey { 593 | utils.RedisDB.Del(val) 594 | } 595 | } 596 | -------------------------------------------------------------------------------- /utils/Spider.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/go-redis/redis/v7" 13 | "github.com/gocolly/colly" 14 | "github.com/gocolly/colly/extensions" 15 | "github.com/panjf2000/ants/v2" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | // 爬取网站的域名,如访问不了,以下几个域名建议重复替换使用 20 | //const host = "http://www.jisudhw.com" 21 | const host = "http://www.okzy.co" 22 | 23 | //const host = "http://www.okzyw.com" 24 | 25 | // redis key 26 | 27 | // 分类key 28 | const CategoriesKey = "categories" 29 | 30 | // 电影详情 31 | const moviesDetail = "movies_detail:" 32 | 33 | type Categories struct { 34 | Link string `json:"link"` 35 | Name string `json:"name"` 36 | Sub []Categories `json:"sub"` 37 | } 38 | 39 | type Movies struct { 40 | Link string `json:"link"` 41 | Name string `json:"name"` 42 | Category string `json:"category"` 43 | Cover string `json:"cover"` 44 | UpdatedAt string `json:"updated_at"` 45 | } 46 | 47 | type MoviesDetail struct { 48 | Link string `json:"link"` 49 | Name string `json:"name"` 50 | Cover string `json:"cover"` 51 | Quality string `json:"quality"` 52 | Score string `json:"score"` 53 | KuYun string `json:"ku_yun"` 54 | CK string `json:"ckm3u8"` 55 | Download string `json:"download"` 56 | Detail map[string]interface{} `json:"detail"` 57 | } 58 | 59 | type Spider struct { 60 | SpiderTask 61 | } 62 | 63 | var ( 64 | Smutex sync.Mutex 65 | wg sync.WaitGroup 66 | ) 67 | 68 | func (spider *Spider) Start() { 69 | go StartSpider() 70 | } 71 | 72 | func (spider *Spider) PageDetail(id string) { 73 | // /?m=vod-detail-id-56275.html 74 | url := "/?m=vod-detail-id-" + id + ".html" 75 | go MoviesInfo(url) 76 | } 77 | 78 | func StartSpider() { 79 | star := time.Now() 80 | defer ants.Release() 81 | 82 | log.Println("开始执行爬虫:获取所有分类") 83 | // 获取所有分类 84 | Categories := SpiderOKCategories() 85 | 86 | antPoolStartSpiderSubCate, _ := ants.NewPool(25) 87 | 88 | antPoolStartSpider := antPoolStartSpiderSubCate 89 | 90 | // 爬取所有主类 91 | SpiderCategories(Categories, antPoolStartSpider) 92 | // 爬取主类对应的子类 93 | SpiderSubCategories(Categories, antPoolStartSpiderSubCate) 94 | 95 | wg.Wait() 96 | 97 | end := time.Since(star) 98 | 99 | ExecSecondsS := strconv.FormatFloat(end.Seconds(), 'f', -1, 64) 100 | ExecMinutesS := strconv.FormatFloat(end.Minutes(), 'f', -1, 64) 101 | ExecHoursS := strconv.FormatFloat(end.Hours(), 'f', -1, 64) 102 | 103 | // 清楚缓存的页面数据 104 | go DelAllListCacheKey() 105 | 106 | log.Println("本次爬虫执行时间为:" + ExecSecondsS + "秒 \n 即" + ExecMinutesS + "分钟 \n 即" + ExecHoursS + "小时 \n ") 107 | 108 | // 钉钉通知 109 | sendDingMsg("本次爬虫执行时间为:" + ExecSecondsS + "秒 \n 即" + ExecMinutesS + "分钟 \n 即" + ExecHoursS + "小时 \n " + runtime.GOOS) 110 | 111 | } 112 | 113 | func SpiderCategories(Categories []Categories, antPoolStartSpider *ants.Pool) { 114 | for _, v := range Categories { 115 | cateUrl := v.Link 116 | wg.Add(1) 117 | 118 | antPoolStartSpider.Submit(func() { 119 | SpiderOKMovies(cateUrl) 120 | wg.Done() 121 | }) 122 | } 123 | } 124 | 125 | func SpiderSubCategories(Categories []Categories, antPoolStartSpiderSubCate *ants.Pool) { 126 | for _, v := range Categories { 127 | childrenCates := v.Sub 128 | 129 | for _, childrenCate := range childrenCates { 130 | wg.Add(1) 131 | childrenCateUrl := childrenCate.Link 132 | antPoolStartSpiderSubCate.Submit(func() { 133 | SpiderOKMovies(childrenCateUrl) 134 | wg.Done() 135 | }) 136 | } 137 | } 138 | } 139 | 140 | // 爬取所有类别 141 | func SpiderOKCategories() []Categories { 142 | 143 | log.Println("爬取所有分类") 144 | 145 | c := colly.NewCollector( 146 | colly.Async(true), 147 | ) 148 | 149 | extensions.RandomUserAgent(c) 150 | extensions.Referer(c) 151 | 152 | c.WithTransport(&http.Transport{ 153 | DisableKeepAlives: true, 154 | }) 155 | 156 | retryCount := 0 157 | 158 | c.OnRequest(func(r *colly.Request) { 159 | log.Println("Visiting", r.URL) 160 | }) 161 | 162 | c.OnError(func(res *colly.Response, err error) { 163 | log.Println("Something went wrong:", err) 164 | if retryCount < 3 { 165 | retryCount += 1 166 | _retryErr := res.Request.Retry() 167 | log.Println("retry wrong:", _retryErr) 168 | } 169 | }) 170 | 171 | // 主类 172 | Cate := make([]Categories, 0) 173 | 174 | // 导航栏、分类 175 | c.OnHTML("ul#sddm li", func(e *colly.HTMLElement) { 176 | 177 | categoryLink := e.ChildAttr("a", "href") 178 | 179 | categoryName := e.ChildText("a[onmouseout]") 180 | 181 | // 子类 182 | SubCate := make([]Categories, 0) 183 | 184 | e.ForEach("div a", func(i int, element *colly.HTMLElement) { 185 | 186 | subCategoryLink := element.Attr("href") 187 | subCategoryName := element.Text 188 | 189 | _subCate := Categories{ 190 | Link: subCategoryLink, 191 | Name: subCategoryName, 192 | } 193 | 194 | if subCategoryName != categoryName { 195 | // 追加 196 | Smutex.Lock() 197 | SubCate = append(SubCate, _subCate) 198 | Smutex.Unlock() 199 | } 200 | 201 | }) 202 | 203 | // 主类别 204 | _cate := Categories{ 205 | Link: categoryLink, 206 | Name: categoryName, 207 | Sub: SubCate, 208 | } 209 | 210 | // 去掉首页、福利、综艺片、解说 链接 211 | if categoryName != "" && categoryName != "福利片" && categoryName != "综艺片" && categoryName != "解说" { 212 | // 追加 213 | Smutex.Lock() 214 | Cate = append(Cate, _cate) 215 | Smutex.Unlock() 216 | } 217 | 218 | }) 219 | 220 | // 在OnHTML之后被调用 221 | c.OnScraped(func(_ *colly.Response) { 222 | 223 | categories, _ := Json.MarshalIndent(Cate, "", " ") 224 | 225 | Smutex.Lock() 226 | err := RedisDB.Set(CategoriesKey, string(categories), 0).Err() 227 | Smutex.Unlock() 228 | log.Println(err) 229 | 230 | // collection := Client.Database("users_db").Collection("CategoriesKey") 231 | // insertResult, err2 := collection.InsertOne(context.TODO(), CategoriesKey) 232 | // log.Println("Inserted a single document: ", insertResult.InsertedID) 233 | // Client.Disconnect(context.TODO()) 234 | // if err2 != nil { 235 | // log.Fatal(err2) 236 | // } 237 | 238 | }) 239 | 240 | visitError := c.Visit(host) 241 | 242 | log.Println(visitError) 243 | 244 | c.Wait() 245 | 246 | return Cate 247 | } 248 | 249 | // 爬取所有类别的电影 250 | func SpiderOKMovies(cateUrl string) { 251 | 252 | c := colly.NewCollector( 253 | colly.Async(true), 254 | ) 255 | 256 | extensions.RandomUserAgent(c) 257 | extensions.Referer(c) 258 | 259 | c.WithTransport(&http.Transport{ 260 | DisableKeepAlives: true, 261 | }) 262 | 263 | c.OnRequest(func(r *colly.Request) { 264 | log.Println("Visiting", r.URL) 265 | }) 266 | 267 | retryCount := 0 268 | c.OnError(func(res *colly.Response, err error) { 269 | log.Println("Something went wrong:", err) 270 | if retryCount < 3 { 271 | retryCount += 1 272 | _retryErr := res.Request.Retry() 273 | log.Println("retry wrong:", _retryErr) 274 | } 275 | }) 276 | 277 | var lastPageInt int 278 | 279 | c.OnHTML(".pages input[type=button]", func(e *colly.HTMLElement) { 280 | 281 | lastPageStr := e.Attr("onclick") 282 | 283 | lastPageStrSplit := strings.Split(lastPageStr, ",")[1] 284 | 285 | // 最后一页 286 | lastPage, _ := strconv.Atoi(strings.Split(lastPageStrSplit, ")")[0]) 287 | 288 | lastPageInt = lastPage // todo lastPage 289 | 290 | for j := 1; j <= lastPageInt; j++ { 291 | pageUrl := CategoryToPageUrl(cateUrl, strconv.Itoa(j)) 292 | // 爬取所有主类下面的电影 293 | ForeachPage(cateUrl, pageUrl) 294 | } 295 | }) 296 | 297 | visitError := c.Visit(host + cateUrl) 298 | 299 | log.Println(visitError) 300 | 301 | c.Wait() 302 | } 303 | 304 | // 获取电影详情信息 305 | func ForeachPage(cateUrl string, url string) { 306 | 307 | c := colly.NewCollector( 308 | colly.Async(true), 309 | ) 310 | 311 | extensions.RandomUserAgent(c) 312 | extensions.Referer(c) 313 | 314 | c.WithTransport(&http.Transport{ 315 | DisableKeepAlives: true, 316 | }) 317 | 318 | c.OnRequest(func(r *colly.Request) { 319 | log.Println("Visiting", r.URL) 320 | }) 321 | 322 | retryCount := 0 323 | c.OnError(func(res *colly.Response, err error) { 324 | log.Println("Something went wrong:", err) 325 | if retryCount < 3 { 326 | retryCount += 1 327 | _retryErr := res.Request.Retry() 328 | log.Println("retry wrong:", _retryErr) 329 | } 330 | }) 331 | 332 | // 导航栏、分类 333 | c.OnHTML(".xing_vb li", func(e *colly.HTMLElement) { 334 | 335 | spanClass := e.ChildAttr("span", "class") 336 | 337 | // 列表数据 338 | if spanClass == "tt" { 339 | link := e.ChildAttr("a", "href") 340 | updateAt := e.ChildText(".xing_vb6") 341 | 342 | // 模板时间 343 | timeTemplate := "2006-01-02" 344 | stamp1, _ := time.ParseInLocation(timeTemplate, updateAt, time.Local) 345 | 346 | Smutex.Lock() 347 | //存储分类数据 348 | RedisDB.ZAdd("detail_links:id:"+TransformId(cateUrl), &redis.Z{ 349 | Score: float64(stamp1.Unix()), 350 | Member: link, 351 | }) 352 | Smutex.Unlock() 353 | 354 | // 获取详情 355 | MoviesInfo(link) 356 | } 357 | }) 358 | 359 | visitError := c.Visit(host + url) 360 | 361 | log.Println(visitError) 362 | log.Println("当前页面") 363 | log.Println(url) 364 | c.Wait() 365 | } 366 | 367 | func MoviesInfo(url string) MoviesDetail { 368 | 369 | c := colly.NewCollector( 370 | colly.Async(true), 371 | ) 372 | 373 | extensions.RandomUserAgent(c) 374 | extensions.Referer(c) 375 | 376 | c.WithTransport(&http.Transport{ 377 | DisableKeepAlives: true, 378 | }) 379 | 380 | c.OnRequest(func(r *colly.Request) { 381 | log.Println("Visiting", r.URL) 382 | }) 383 | 384 | retryCount := 0 385 | c.OnError(func(res *colly.Response, err error) { 386 | log.Println("Something went wrong:", err) 387 | if retryCount < 3 { 388 | retryCount += 1 389 | _retryErr := res.Request.Retry() 390 | log.Println("retry wrong:", _retryErr) 391 | } 392 | }) 393 | 394 | // 所有电影 395 | md := MoviesDetail{} 396 | 397 | detail := make(map[string]interface{}) 398 | 399 | var kuyunAry []map[string]string 400 | 401 | var ckm3u8Ary []map[string]string 402 | 403 | var downloadAry []map[string]string 404 | 405 | c.OnHTML(".warp", func(e *colly.HTMLElement) { 406 | 407 | cover := e.ChildAttr("div .vodImg>img", "src") 408 | name := e.ChildText("div .vodh>h2") 409 | quality := e.ChildText("div .vodh span") 410 | score := e.ChildText("div .vodh label") 411 | 412 | _type := "" 413 | e.ForEach("div .vodinfobox ul li", func(i int, element *colly.HTMLElement) { 414 | if i == 3 { 415 | _type = element.ChildText("span") 416 | } 417 | }) 418 | 419 | // 有些页面 1 是 ckm3u8 2 是 kuyun wtf! 420 | 421 | e.ForEach("div #1 ul li", func(i int, element *colly.HTMLElement) { 422 | 423 | playLink := element.ChildAttr("input", "value") 424 | 425 | Episode := strconv.Itoa(i + 1) 426 | Episode = transformEpisode(_type, Episode, element.Text) 427 | 428 | if strings.Index(playLink, "m3u8") == -1 { 429 | kuyun := map[string]string{ 430 | "episode": Episode, 431 | "play_link": playLink} 432 | 433 | Smutex.Lock() 434 | kuyunAry = append(kuyunAry, kuyun) 435 | Smutex.Unlock() 436 | } else { 437 | ckm3u8 := map[string]string{ 438 | "episode": Episode, 439 | "play_link": playLink} 440 | Smutex.Lock() 441 | ckm3u8Ary = append(ckm3u8Ary, ckm3u8) 442 | Smutex.Unlock() 443 | } 444 | 445 | }) 446 | 447 | e.ForEach("div #2 ul li", func(i int, element *colly.HTMLElement) { 448 | 449 | playLink := element.ChildAttr("input", "value") 450 | 451 | Episode := strconv.Itoa(i + 1) 452 | Episode = transformEpisode(_type, Episode, element.Text) 453 | 454 | if strings.Index(playLink, "m3u8") == -1 { 455 | kuyun := map[string]string{ 456 | "episode": Episode, 457 | "play_link": playLink} 458 | 459 | Smutex.Lock() 460 | kuyunAry = append(kuyunAry, kuyun) 461 | Smutex.Unlock() 462 | } else { 463 | ckm3u8 := map[string]string{ 464 | "episode": Episode, 465 | "play_link": playLink} 466 | Smutex.Lock() 467 | ckm3u8Ary = append(ckm3u8Ary, ckm3u8) 468 | Smutex.Unlock() 469 | } 470 | }) 471 | 472 | e.ForEach("div #down_1 ul li", func(i int, element *colly.HTMLElement) { 473 | 474 | playLink := element.ChildAttr("input", "value") 475 | 476 | Episode := strconv.Itoa(i + 1) 477 | Episode = transformEpisode(_type, Episode, element.Text) 478 | 479 | download := map[string]string{ 480 | "episode": Episode, 481 | "play_link": playLink} 482 | 483 | Smutex.Lock() 484 | downloadAry = append(downloadAry, download) 485 | Smutex.Unlock() 486 | }) 487 | 488 | kuyunAryJson, _ := Json.MarshalIndent(kuyunAry, "", " ") 489 | ckm3u8AryJson, _ := Json.MarshalIndent(ckm3u8Ary, "", " ") 490 | downloadAryJson, _ := Json.MarshalIndent(downloadAry, "", " ") 491 | 492 | // detail["alias"] = e.ChildText("div .vodinfobox>ul>li:eq(0)") // WTF 不支持这样的选择器 493 | // xpath 还是靠谱 494 | // 别名 495 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[1]/span", func(e *colly.XMLElement) { 496 | detail["alias"] = e.Text 497 | }) 498 | 499 | // 导演 500 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[2]/span", func(e *colly.XMLElement) { 501 | detail["director"] = e.Text 502 | }) 503 | 504 | // 主演 505 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[3]/span", func(e *colly.XMLElement) { 506 | detail["starring"] = e.Text 507 | }) 508 | 509 | // 类型 510 | detail["type"] = _type 511 | 512 | // 地区 513 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[5]/span", func(e *colly.XMLElement) { 514 | detail["area"] = e.Text 515 | }) 516 | 517 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[6]/span", func(e *colly.XMLElement) { 518 | detail["language"] = e.Text 519 | }) 520 | 521 | // 上映时间 522 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[7]/span", func(e *colly.XMLElement) { 523 | detail["released"] = e.Text 524 | }) 525 | 526 | // 片长 527 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[8]/span", func(e *colly.XMLElement) { 528 | detail["length"] = e.Text 529 | }) 530 | 531 | // 更新时间 532 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[9]/span", func(e *colly.XMLElement) { 533 | detail["update"] = e.Text 534 | }) 535 | 536 | // 总播放量 537 | c.OnXML("/html/body/div[5]/div[1]/div/div/div[2]/div[2]/ul/li[10]/span", func(e *colly.XMLElement) { 538 | detail["total_playback"] = e.Text 539 | }) 540 | 541 | // 剧情简介 542 | c.OnXML("/html/body/div[5]/div[3]/div[2]", func(e *colly.XMLElement) { 543 | detail["vod_play_info"] = e.Text 544 | }) 545 | 546 | if detail["vod_play_info"] == "" || detail["vod_play_info"] == nil { 547 | c.OnXML("/html/body/div[5]/div[2]/div[2]/text()", func(e *colly.XMLElement) { 548 | detail["vod_play_info"] = e.Text 549 | }) 550 | } 551 | 552 | md = MoviesDetail{ 553 | Link: url, 554 | Name: name, 555 | Cover: cover, 556 | Quality: quality, 557 | Score: score, 558 | Detail: detail, 559 | KuYun: string(kuyunAryJson), 560 | CK: string(ckm3u8AryJson), 561 | Download: string(downloadAryJson), 562 | } 563 | 564 | }) 565 | 566 | // 在OnHTML之后被调用 567 | c.OnScraped(func(_ *colly.Response) { 568 | 569 | _moviesInfo := make(map[string]interface{}) 570 | 571 | _moviesInfo["link"] = md.Link 572 | _moviesInfo["cover"] = md.Cover 573 | _moviesInfo["name"] = md.Name 574 | _moviesInfo["quality"] = md.Quality 575 | _moviesInfo["score"] = md.Score 576 | _moviesInfo["kuyun"] = md.KuYun 577 | _moviesInfo["ckm3u8"] = md.CK 578 | _moviesInfo["download"] = md.Download 579 | 580 | _detail, _ := Json.MarshalIndent(md.Detail, "", " ") 581 | 582 | _moviesInfo["detail"] = string(_detail) 583 | 584 | if md.Name != "" { 585 | Smutex.Lock() 586 | //redisDB 存储电影详情数据 587 | t := RedisDB.HMSet(moviesDetail+url+":movie_name:"+md.Name, _moviesInfo).Err() 588 | 589 | log.Println(t) 590 | Smutex.Unlock() 591 | } 592 | 593 | }) 594 | 595 | visitError := c.Visit(host + url) 596 | 597 | log.Println(visitError) 598 | 599 | c.Wait() 600 | 601 | return md 602 | } 603 | 604 | // /?m=vod-type-id-1.html => /?m=vod-type-id-1-pg-1 605 | func CategoryToPageUrl(categoryUrl string, page string) string { 606 | // 主类链接: /?m=vod-type-id-1.html 607 | // 主类的页面链接 /?m=vod-type-id-1-pg- 608 | categoryUrlStrSplit := strings.Split(categoryUrl, ".html")[0] 609 | 610 | pageUrl := categoryUrlStrSplit + "-pg-" + page + ".html" 611 | 612 | return pageUrl 613 | } 614 | 615 | // 获取url中的链接 616 | func TransformId(Url string) string { 617 | UrlStrSplit := strings.Split(Url, "-id-")[1] 618 | 619 | return strings.TrimRight(UrlStrSplit, ".html") 620 | } 621 | 622 | func DelAllListCacheKey() { 623 | 624 | AllListCacheKey := RedisDB.Keys("movie_lists_key:detail_links:*").Val() 625 | 626 | // 删除已经缓存的数据 627 | for _, val := range AllListCacheKey { 628 | RedisDB.Del(val) 629 | } 630 | } 631 | 632 | func isFilm(_type string) bool { 633 | return strings.Contains(_type, "片") 634 | } 635 | 636 | // 电影只处理国语跟广东话、其他语言暂不处理 637 | func transformEpisode(_type, episode, linkName string) string { 638 | 639 | if isFilm(_type) == true { 640 | if strings.Contains(linkName, "粤语") == true { 641 | episode = "粤语" 642 | } 643 | if strings.Contains(linkName, "国语") == true { 644 | episode = "国语" 645 | } 646 | } 647 | 648 | return episode 649 | } 650 | 651 | func sendDingMsg(msg string) { 652 | accessToken := viper.GetString(`ding.access_token`) 653 | if accessToken == "" { 654 | return 655 | } 656 | webhook := "https://oapi.dingtalk.com/robot/send?access_token=" + accessToken 657 | robot := NewRobot(webhook) 658 | 659 | title := "goMovies 爬虫通知API" 660 | text := "#### goMovies 爬虫通知API \n " + msg 661 | atMobiles := []string{""} 662 | isAtAll := true 663 | 664 | err := robot.SendMarkdown(title, text, atMobiles, isAtAll) 665 | if err != nil { 666 | log.Println(err) 667 | return 668 | } 669 | 670 | log.Println("已发送钉钉通知") 671 | } 672 | -------------------------------------------------------------------------------- /static/style/home.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f4f5f5; 3 | } 4 | .navbar-default { 5 | background-color: #ffffff; 6 | border-color: #e7e7e7; 7 | } 8 | .btn-primary { 9 | background-color: #e59233; 10 | border-color: #e59233; 11 | } 12 | .btn-primary:hover { 13 | background-color: #c67e2b; 14 | border-color: #c67e2b; 15 | } 16 | .pingpu { 17 | padding: 0; 18 | margin: 0; 19 | } 20 | .pingpu li { 21 | display: inline-block; 22 | height: 50px; 23 | line-height: 50px; 24 | padding: 0 15px; 25 | cursor: pointer; 26 | } 27 | .pingpu li a { 28 | color: #555; 29 | } 30 | .pingpu li a:hover { 31 | color: #e59233; 32 | text-decoration: none; 33 | } 34 | .pingpu .active a { 35 | color: #e59233; 36 | } 37 | .sepcaliColor-zi { 38 | color: #b71ed7 !important; 39 | font-weight: 600; 40 | } 41 | .sepcaliColor-red { 42 | color: red !important; 43 | font-weight: 600; 44 | } 45 | .sepcaliColor-cheng { 46 | color: #ff8800 !important; 47 | font-weight: 600; 48 | } 49 | .navbar { 50 | font-size: 16px; 51 | z-index: 10; 52 | padding: 0 20px; 53 | border: 0; 54 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 55 | } 56 | .navbarCreateBlog { 57 | z-index: 11; 58 | } 59 | .shuxian { 60 | display: inline-block; 61 | height: 14px; 62 | width: 1px; 63 | background-color: #999; 64 | margin: -1px 5px; 65 | } 66 | .nav-left { 67 | float: left; 68 | } 69 | .nav-right { 70 | height: 50px; 71 | line-height: 50px; 72 | float: right; 73 | color: #e59233; 74 | cursor: pointer; 75 | } 76 | .nav-right .sxBtn { 77 | border: 1px solid #e59233; 78 | padding: 2px; 79 | border-radius: 5px; 80 | padding-right: 8px; 81 | } 82 | .nav-right .zcBtn { 83 | border: 1px solid #888; 84 | cursor: default; 85 | color: #888; 86 | } 87 | .nav-right a { 88 | color: #e59233; 89 | text-decoration: none; 90 | } 91 | .nav-right .navbar-item { 92 | display: inline; 93 | } 94 | .nav-right .btn-group { 95 | vertical-align: top; 96 | } 97 | .nav-right .btn-group a { 98 | color: #777; 99 | } 100 | .nav-right .btn-group .dropdown-menu { 101 | text-align: center; 102 | border: 0; 103 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.175); 104 | } 105 | .nav-right .btn-group.open .dropdown-toggle { 106 | color: #e59233; 107 | box-shadow: 0 0 0; 108 | } 109 | .nav-right .btn-group.open .dropdown-toggle a { 110 | color: #e59233; 111 | } 112 | .logoDiv { 113 | float: left; 114 | height: 50px; 115 | line-height: 42px; 116 | padding: 4px; 117 | margin-right: 30px; 118 | } 119 | .logoDiv a { 120 | text-decoration: none; 121 | } 122 | .logoDiv img { 123 | width: 40px; 124 | height: 40px; 125 | display: inline-block; 126 | position: relative; 127 | top: -2px; 128 | } 129 | .logoDiv span { 130 | color: #6f6457; 131 | font-size: 18px; 132 | } 133 | a { 134 | color: #0071d2; 135 | } 136 | #login, 137 | .addClassification { 138 | width: 100%; 139 | height: 100%; 140 | background-color: rgba(0, 0, 0, 0.4) !important; 141 | position: fixed; 142 | z-index: 100; 143 | top: 0; 144 | left: 0; 145 | } 146 | #login .loginBody, 147 | .addClassification .loginBody { 148 | width: 320px; 149 | height: 330px; 150 | position: absolute; 151 | top: 50%; 152 | left: 50%; 153 | transform: translate(-50%, -50%); 154 | padding: 20px; 155 | background-color: #fff; 156 | border-radius: 8px; 157 | } 158 | #login .loginBody h4, 159 | .addClassification .loginBody h4 { 160 | color: #e59233; 161 | } 162 | #login .loginBody input, 163 | .addClassification .loginBody input { 164 | width: 280px; 165 | height: 40px; 166 | margin-top: 12px; 167 | } 168 | #login .loginBody .btn, 169 | .addClassification .loginBody .btn { 170 | margin-top: 20px; 171 | } 172 | #login .loginBody .closeLogin, 173 | .addClassification .loginBody .closeLogin { 174 | position: fixed; 175 | top: 5px; 176 | right: 5px; 177 | font-size: 24px; 178 | color: #888; 179 | cursor: pointer; 180 | } 181 | #login .singinBody, 182 | .addClassification .singinBody { 183 | width: 320px; 184 | height: 284px; 185 | } 186 | #login .singupBody, 187 | .addClassification .singupBody { 188 | width: 320px; 189 | height: 284px; 190 | } 191 | #login .loginHref, 192 | .addClassification .loginHref { 193 | margin-top: 10px; 194 | } 195 | #login .loginHref, 196 | .addClassification .loginHref { 197 | cursor: pointer; 198 | } 199 | .addClassification .loginBody { 200 | height: 330px; 201 | } 202 | .notifications { 203 | position: fixed; 204 | top: 60px; 205 | width: 280px; 206 | right: 10px; 207 | z-index: 100; 208 | } 209 | .notifications i { 210 | position: absolute; 211 | right: 6px; 212 | color: #fff; 213 | font-size: 18px; 214 | top: 8px; 215 | cursor: pointer; 216 | } 217 | .notification { 218 | padding: 10px; 219 | padding-right: 20px; 220 | border-radius: 4px; 221 | color: #fff; 222 | } 223 | .is-success { 224 | background-color: rgba(92, 149, 46, 0.68); 225 | } 226 | .is-warning { 227 | background-color: rgba(219, 145, 18, 0.68); 228 | } 229 | .main-body { 230 | padding-top: 60px; 231 | margin-bottom: 30px; 232 | } 233 | a { 234 | color: #555; 235 | text-decoration: none; 236 | } 237 | a:hover { 238 | color: #e59233; 239 | text-decoration: none; 240 | } 241 | a.thumbnail:hover { 242 | border-color: #e59233; 243 | box-shadow: 0px 0px 6px #e59233; 244 | } 245 | .movieRd { 246 | background-color: rgba(199, 162, 119, 0.78039216); 247 | position: absolute; 248 | bottom: 95px; 249 | padding: 5px 10px 5px 5px; 250 | color: #fff !important; 251 | margin: 0px; 252 | } 253 | .thumbnail { 254 | border: 0; 255 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 256 | border-radius: 0; 257 | padding: 8px; 258 | position: relative; 259 | } 260 | .topDiv { 261 | background-color: #fff; 262 | padding: 10px 15px 10px; 263 | margin-bottom: 20px; 264 | margin-top: 10px; 265 | } 266 | .movieList { 267 | padding: 0 5px; 268 | } 269 | .movieCard { 270 | padding: 0 10px; 271 | } 272 | .movieCard img { 273 | width: 100%; 274 | height: 370px; 275 | } 276 | .movieCard .movieHeader { 277 | padding: 5px; 278 | } 279 | .movieCard .movieHeader h4 { 280 | overflow: hidden; 281 | text-overflow: ellipsis; 282 | white-space: nowrap; 283 | font-size: 16px; 284 | font-weight: 600; 285 | } 286 | .movieCard .movieHeader h6 { 287 | height: 40px; 288 | overflow: hidden; 289 | text-overflow: ellipsis; 290 | display: -webkit-box; 291 | -webkit-line-clamp: 2; 292 | -webkit-box-orient: vertical; 293 | font-size: 12px; 294 | font-weight: 500; 295 | color: #888; 296 | line-height: 20px; 297 | margin-bottom: 0; 298 | } 299 | .change-Pages { 300 | text-align: center; 301 | } 302 | .change-Pages .pagination { 303 | margin: 0; 304 | margin-top: 5px; 305 | } 306 | .change-Pages .pagination li .active { 307 | background-color: #e59233; 308 | color: #fff; 309 | border-color: #e59233; 310 | } 311 | .change-Pages .allPage { 312 | height: 36px; 313 | line-height: 36px; 314 | display: inline-block; 315 | margin: 0 10px; 316 | } 317 | .allCount { 318 | display: inline-block; 319 | padding: 10px; 320 | font-size: 13px; 321 | } 322 | .allCount span { 323 | color: #b71ed7 !important; 324 | font-weight: 600; 325 | } 326 | .selectHeader { 327 | padding: 0 10px; 328 | border-radius: 10px; 329 | padding-bottom: 1px; 330 | display: flex; 331 | align-items: center; 332 | } 333 | .selectHeader .active { 334 | color: #e59233; 335 | font-weight: 600; 336 | } 337 | .selectHeader h5 { 338 | margin: 12px 0; 339 | color: #999; 340 | float: right; 341 | width: 60px; 342 | } 343 | .selectHeader h4 { 344 | font-size: 16px; 345 | font-weight: 600; 346 | width: 50px; 347 | } 348 | .selectHeader div { 349 | display: inline-block; 350 | } 351 | .selectHeader ul { 352 | display: inline-block; 353 | margin: 0; 354 | padding-left: 0px; 355 | } 356 | .selectHeader ul li { 357 | display: inline-block; 358 | padding: 2px 12px 0 10px; 359 | margin: 0; 360 | border-right: 1px solid #ccc; 361 | } 362 | .selectHeader ul li:last-child { 363 | border-right: 0; 364 | } 365 | .breadcrumb { 366 | background-color: #fff; 367 | margin-bottom: 0; 368 | color: #888; 369 | padding-left: 0; 370 | } 371 | .breadcrumbBlack { 372 | background-color: rgba(0, 0, 0, 0); 373 | margin-bottom: 0; 374 | color: #666; 375 | padding-left: 0; 376 | } 377 | .movieDetail { 378 | margin-bottom: 20px; 379 | padding: 25px; 380 | background-color: #fff; 381 | } 382 | .movieDetail hr { 383 | margin-top: 0px; 384 | } 385 | .movieDetail .imgCard img { 386 | width: 100%; 387 | } 388 | .movieDetail .baseDiv { 389 | position: relative; 390 | margin-bottom: 20px; 391 | } 392 | .movieDetail .movieJj { 393 | bottom: 10px; 394 | width: 100%; 395 | } 396 | .movieDetail .movieJj h3 { 397 | color: #e59233; 398 | font-weight: 600; 399 | } 400 | .movieDetail .movieJj h3 .score { 401 | color: red; 402 | margin-right: 40px; 403 | font-size: 28px; 404 | float: right; 405 | } 406 | .movieDetail .movieJj h5 { 407 | font-size: 14px; 408 | color: #999; 409 | margin-bottom: 57px; 410 | } 411 | .movieDetail .movieJj p { 412 | font-size: 14px; 413 | color: #666; 414 | } 415 | .movieDetail .movieJj p span { 416 | color: #333; 417 | font-weight: 600; 418 | } 419 | .movieDetail .movieJj .downLoadCard { 420 | background: #efefef; 421 | padding: 10px; 422 | border-radius: 10px; 423 | border: 1px dashed #e59233; 424 | margin-top: 30px; 425 | } 426 | .movieDetail .movieJj .downLoadCard h6 { 427 | margin: 0; 428 | font-weight: 600; 429 | color: #e59233; 430 | font-size: 14px; 431 | } 432 | .movieDetail .movieJj .downLoadCard p { 433 | margin: 0; 434 | padding: 6px 10px; 435 | background-color: #fff; 436 | border-radius: 6px; 437 | margin-top: 10px; 438 | } 439 | .movieDetail .detailCard h4 { 440 | border-left: 4px solid #2d9cfc; 441 | padding-left: 10px; 442 | font-weight: 600; 443 | color: #555; 444 | margin-top: 30px; 445 | } 446 | .movieDetail .detailCard .linkList a { 447 | display: block; 448 | height: 30px; 449 | width: 100%; 450 | border-radius: 5px; 451 | border: 1px solid #ccc; 452 | background-color: #f4f4f4; 453 | line-height: 30px; 454 | margin-bottom: 10px; 455 | } 456 | .movieDetail .detailCard .linkList a span { 457 | display: inline-block; 458 | width: 30px ; 459 | height: 18px; 460 | line-height: 18px; 461 | text-align: center; 462 | background: coral; 463 | color: #fff; 464 | border-radius: 20px; 465 | margin: 0 5px; 466 | } 467 | .movieDetail .detailCard p:first-child { 468 | font-weight: 600; 469 | font-size: 18px; 470 | } 471 | .h100 { 472 | height: 100%; 473 | } 474 | .searchTop { 475 | height: 40px; 476 | margin: 4px 0 0; 477 | } 478 | .searchTop .movieTypeList { 479 | display: inline-block; 480 | } 481 | .searchTop .movieTypeList .movieTypeItem { 482 | display: inline-block; 483 | font-size: 18px; 484 | font-weight: 600; 485 | cursor: pointer; 486 | margin: 5px 10px; 487 | } 488 | .searchTop .movieTypeList .movieTypeItem:hover { 489 | color: #e59233; 490 | } 491 | .searchTop .movieTypeList .active { 492 | font-size: 22px; 493 | font-weight: 600; 494 | color: #333; 495 | color: #e59233; 496 | } 497 | .searchTop .form-inline { 498 | width: 300px; 499 | float: right; 500 | height: 40px; 501 | margin: 0; 502 | } 503 | .searchTop .form-inline input { 504 | width: 240px; 505 | border-color: #ddd; 506 | box-shadow: 0 0 0; 507 | border-radius: 20px; 508 | float: left; 509 | margin-right: 5px; 510 | padding-right: 34px; 511 | } 512 | .searchTop .form-inline button { 513 | float: right; 514 | } 515 | .page-404 { 516 | height: 100vh; 517 | overflow: hidden; 518 | text-align: center; 519 | margin-top: 50px; 520 | } 521 | .page-404 pre { 522 | width: 300px; 523 | height: 300px; 524 | display: inline-block; 525 | border-radius: 50%; 526 | padding: 30px 20px 0 0; 527 | } 528 | .page-404 button { 529 | padding: 8px 20px; 530 | margin: 10px; 531 | } 532 | .page-404 button a { 533 | color: #fff; 534 | } 535 | .page-404 .subtitle { 536 | margin: 0 0 10px; 537 | font-size: 18px; 538 | color: #777; 539 | } 540 | .page-404 p { 541 | color: #888; 542 | } 543 | .blogCreate { 544 | padding: 10px 20px; 545 | } 546 | .blogCreate .header-left { 547 | position: relative; 548 | } 549 | .blogCreate .header-left hr { 550 | margin-bottom: 0; 551 | } 552 | .blogCreate .header-left .selTypes { 553 | position: absolute; 554 | right: 30px; 555 | top: 4px; 556 | color: #888; 557 | cursor: pointer; 558 | } 559 | .blogCreate .header-left .selTypes .dropdown-toggle { 560 | font-size: 18px; 561 | } 562 | .blogCreate .header-left .selTypes .dropdown-toggle i { 563 | font-size: 20px; 564 | position: relative; 565 | top: 1px; 566 | right: -4px; 567 | } 568 | .blogCreate .header-left .selTypes .dropdown-toggle i:hover { 569 | color: #e59233; 570 | } 571 | .blogCreate .header-left .selTypes:hover { 572 | color: #555; 573 | } 574 | .blogCreate .header-left .open .dropdown-toggle { 575 | box-shadow: 0 0 0; 576 | } 577 | .blogCreate .header-left .dropdown-menu { 578 | left: auto; 579 | right: 0; 580 | padding: 10px 0; 581 | border: 0; 582 | } 583 | .blogCreate .header-left .dropdown-menu li { 584 | height: 30px; 585 | line-height: 30px; 586 | text-align: center; 587 | } 588 | .blogCreate .header-left .dropdown-menu li:hover { 589 | background-color: #eee; 590 | } 591 | .blogCreate .header-left .dropdown-menu .divider { 592 | height: 1px; 593 | line-height: 1px; 594 | } 595 | .blogCreate .header .header-left input { 596 | border: 0; 597 | box-shadow: 0 0 0; 598 | font-size: 22px; 599 | } 600 | .blogCreate .markDown { 601 | width: 100%; 602 | display: flex; 603 | } 604 | .blogCreate .markDown #md-area { 605 | width: 50%; 606 | border: 0; 607 | min-height: 800px; 608 | background-color: #ededed; 609 | border-right: 1px solid #ccc; 610 | height: 100%; 611 | padding: 10px; 612 | } 613 | .blogCreate .markDown #show-area { 614 | width: 50%; 615 | margin-left: -1px; 616 | padding: 10px; 617 | border-left: 1px solid #ccc; 618 | } 619 | .selectedEditor { 620 | position: relative; 621 | top: -1px; 622 | margin-left: 15px; 623 | } 624 | .selectedEditor .btn-default { 625 | background-color: #999; 626 | color: #fff; 627 | border: 0; 628 | } 629 | .selectedEditor .btn-default:hover { 630 | color: #fff; 631 | } 632 | .selectedEditor .active { 633 | background-color: #e59233 !important; 634 | } 635 | .classificationHeader { 636 | position: fixed; 637 | width: 100%; 638 | height: 42px; 639 | line-height: 42px; 640 | font-size: 14px; 641 | background-color: #fff; 642 | left: 0; 643 | top: 50px; 644 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 645 | border-top: 1px solid #e6e6e6; 646 | z-index: 9; 647 | } 648 | .classificationHeader ul li { 649 | display: inline-block; 650 | padding: 0 10px; 651 | } 652 | .blogList { 653 | margin-top: 50px; 654 | background-color: #fff; 655 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 656 | } 657 | .blogList .blogList-header { 658 | height: 44px; 659 | line-height: 44px; 660 | border-bottom: 1px solid #f6f6f6; 661 | cursor: pointer; 662 | } 663 | .blogList .blogList-header ul { 664 | padding: 0; 665 | padding-left: 10px; 666 | color: #777; 667 | } 668 | .blogList .blogList-header ul li { 669 | font-size: 14px; 670 | display: inline-block; 671 | padding: 0 10px; 672 | border-right: 1px solid #ccc; 673 | height: 14px; 674 | line-height: 14px; 675 | padding-right: 12px; 676 | } 677 | .blogList .blogList-header ul li:last-child { 678 | border-right: 0; 679 | } 680 | .blogList .blogList-header ul li:hover { 681 | color: #e59233; 682 | font-weight: 600; 683 | } 684 | .blogList .blogList-header ul li:hover a { 685 | color: #e59233; 686 | } 687 | .blogList .blogList-header ul li.active { 688 | color: #e59233; 689 | font-weight: 600; 690 | } 691 | .blogList .blogList-header ul li.active a { 692 | color: #e59233; 693 | } 694 | .blogList .blogCard { 695 | margin: 0 15px; 696 | padding: 10px 20px; 697 | border-bottom: 1px solid #f6f6f6; 698 | cursor: pointer; 699 | position: relative; 700 | } 701 | .blogList .blogCard .removeBlog { 702 | display: none; 703 | position: absolute; 704 | z-index: 2; 705 | right: 20px; 706 | top: 30px; 707 | } 708 | .blogList .blogCard .removeBlog i { 709 | color: red; 710 | font-size: 30px; 711 | } 712 | .blogList .blogCard:hover { 713 | background-color: #fafafa; 714 | } 715 | .blogList .blogCard:hover h4 { 716 | color: #e59233; 717 | } 718 | .blogList .blogCard:hover .removeBlog { 719 | display: block; 720 | } 721 | .blogList .blogCard:last-child { 722 | margin-bottom: 20px; 723 | } 724 | .blogList .blogCard .jian { 725 | color: red; 726 | font-weight: 600; 727 | } 728 | .blogList .blogCard .re { 729 | color: #b71ed7; 730 | font-weight: 600; 731 | } 732 | .blogList .blogCard .createType { 733 | color: #4e87dc; 734 | } 735 | .blogList .blogCard .lei { 736 | color: #f70; 737 | font-weight: 600; 738 | } 739 | .blogList .blogCard h6 { 740 | color: #888; 741 | font-size: 12px; 742 | } 743 | .blogList .blogCard h6 i { 744 | font-size: 12px; 745 | } 746 | .blogList .blogCard h4 { 747 | font-size: 16px; 748 | } 749 | .blogList .blogCard .dzBtnGroup { 750 | color: #777; 751 | } 752 | .blogList .blogCard .dzBtnGroup span { 753 | display: inline-block; 754 | padding: 2px 7px 2px 3px; 755 | margin-right: -5px; 756 | } 757 | .blogList .blogCard .dzBtnGroup span i { 758 | font-size: 14px; 759 | } 760 | .headerImg { 761 | height: 50px; 762 | line-height: 50px; 763 | display: inline-block; 764 | margin: 0 5px; 765 | } 766 | .headerImg img { 767 | display: inline-block; 768 | height: 30px; 769 | width: 30px; 770 | position: relative; 771 | top: -2px; 772 | } 773 | .headerImg span { 774 | display: inline-block; 775 | height: 30px; 776 | width: 30px; 777 | line-height: 30px; 778 | padding: 0; 779 | margin: 0; 780 | border-radius: 50%; 781 | border: 1px solid #999; 782 | color: #999; 783 | text-align: center; 784 | position: relative; 785 | top: 4px; 786 | } 787 | .headerImg span i { 788 | font-size: 24px; 789 | margin: 0; 790 | } 791 | .blogItem { 792 | background-color: #fff; 793 | padding: 30px; 794 | margin-bottom: 30px; 795 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 796 | margin-bottom: 20px; 797 | } 798 | .blogItem .blogTitle { 799 | text-align: center; 800 | } 801 | .blogItem .blog-header { 802 | position: relative; 803 | border-bottom: 1px dashed #ccc; 804 | padding-bottom: 10px; 805 | } 806 | .blogItem .blog-header .headerImg { 807 | vertical-align: top; 808 | } 809 | .blogItem .blog-header .headerImg img { 810 | width: 50px; 811 | height: 50px; 812 | top: 4px; 813 | } 814 | .blogItem .blog-header .headerImg span { 815 | width: 50px; 816 | height: 50px; 817 | padding-top: 10px; 818 | } 819 | .blogItem .blog-header .headerImg span i { 820 | font-size: 44px; 821 | } 822 | .blogItem .blog-header .author { 823 | display: inline-block; 824 | vertical-align: top; 825 | } 826 | .blogItem .blog-header .author h4 { 827 | margin: 10px; 828 | } 829 | .blogItem .blog-header .author h5 { 830 | margin: -5px 10px 10px; 831 | color: #888; 832 | } 833 | .blogItem .blog-header button { 834 | position: absolute; 835 | top: 10px; 836 | right: 10px; 837 | } 838 | .blogItem .blog-header .btn { 839 | border-radius: 0; 840 | border-color: #e59233; 841 | color: #e59233; 842 | } 843 | .classificationHeader ul { 844 | padding: 0; 845 | padding-left: 10px; 846 | float: left; 847 | } 848 | .classificationHeader ul li { 849 | font-size: 14px; 850 | display: inline-block; 851 | padding: 0 10px; 852 | padding-right: 12px; 853 | color: #777; 854 | cursor: pointer; 855 | } 856 | .classificationHeader ul li:last-child { 857 | border-right: 0; 858 | } 859 | .classificationHeader ul li:hover { 860 | color: #e59233; 861 | font-weight: 600; 862 | } 863 | .classificationHeader ul li:hover a { 864 | color: #e59233; 865 | } 866 | .classificationHeader ul li.active { 867 | color: #e59233; 868 | font-weight: 600; 869 | } 870 | .classificationHeader ul li.active a { 871 | color: #e59233; 872 | } 873 | .blogSearch { 874 | float: right; 875 | position: relative; 876 | } 877 | .blogSearch .form-inline { 878 | width: 240px; 879 | } 880 | .blogSearch input { 881 | float: right; 882 | width: 240px; 883 | height: 28px; 884 | line-height: 28px; 885 | margin-top: 3px; 886 | } 887 | .blogSearch i { 888 | color: #999; 889 | font-size: 22px; 890 | position: absolute; 891 | right: 2px; 892 | top: -4px; 893 | } 894 | .userInfo { 895 | background-color: #fff; 896 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 897 | padding: 20px 40px; 898 | width: 70%; 899 | margin-top: 10px; 900 | } 901 | .userInfo .header { 902 | font-size: 22px; 903 | font-weight: 600; 904 | border-bottom: 1px solid #eee; 905 | height: 60px; 906 | line-height: 60px; 907 | } 908 | .userInfo .userInfoHeader { 909 | position: relative; 910 | } 911 | .userInfo .userInfoHeader .headerImg { 912 | position: absolute; 913 | top: -12px; 914 | } 915 | .userInfo .userInfoHeader .headerImg span { 916 | width: 52px; 917 | height: 52px; 918 | padding-top: 8px; 919 | } 920 | .userInfo .userInfoHeader .headerImg span i { 921 | font-size: 42px; 922 | } 923 | .userInfo .userInfoHeader .headerImg img { 924 | width: 52px; 925 | height: 52px; 926 | top: 4px; 927 | } 928 | .userInfo .form-group { 929 | border-bottom: 1px solid #eee; 930 | padding: 20px 0; 931 | margin: 0; 932 | } 933 | .userInfo .form-group input { 934 | border-radius: 0; 935 | border: 0; 936 | box-shadow: 0 0 0; 937 | } 938 | .userInfo .changeInfoBtn { 939 | width: 100%; 940 | text-align: center; 941 | padding: 30px 0 15px; 942 | } 943 | .userInfo .changeInfoBtn button { 944 | padding: 8px 50px; 945 | background-color: #e59233; 946 | color: #fff; 947 | border: 0px; 948 | font-size: 16px; 949 | } 950 | .comment .userHeader { 951 | width: 60px; 952 | float: left; 953 | } 954 | .comment .userHeader .headerImg { 955 | vertical-align: top; 956 | } 957 | .comment .userHeader .headerImg img { 958 | width: 40px; 959 | height: 40px; 960 | top: 4px; 961 | } 962 | .comment .userHeader .headerImg span { 963 | width: 40px; 964 | height: 40px; 965 | padding-top: 10px; 966 | } 967 | .comment .userHeader .headerImg span i { 968 | font-size: 35px; 969 | } 970 | .comment .commentInput { 971 | background-color: #fafbfc; 972 | padding: 15px 15px 10px 15px; 973 | margin-bottom: 30px; 974 | } 975 | .comment .commentInput .inputArea { 976 | width: 100%; 977 | padding-left: 60px; 978 | } 979 | .comment .commentInput .inputArea textarea { 980 | width: 100%; 981 | border-radius: 5px; 982 | border-color: #ccc; 983 | padding: 10px; 984 | } 985 | .comment .commentInput .inputArea textarea:focus { 986 | border-color: #e59233; 987 | } 988 | .comment .commentInput .control { 989 | width: 100%; 990 | padding-left: 60px; 991 | height: 40px; 992 | } 993 | .comment .commentInput .control .emjo { 994 | color: #888; 995 | float: left; 996 | position: relative; 997 | cursor: pointer; 998 | } 999 | .comment .commentInput .control .emjo:hover { 1000 | color: #e59233; 1001 | } 1002 | .comment .commentInput .control .emjo:hover i { 1003 | color: #e59233; 1004 | } 1005 | .comment .commentInput .control .emjo:hover .emjoArea { 1006 | display: flex; 1007 | display: -webkit-flex; 1008 | /* Safari */ 1009 | flex-wrap: wrap; 1010 | } 1011 | .comment .commentInput .control .emjo i { 1012 | font-size: 24px; 1013 | position: relative; 1014 | top: 3px; 1015 | color: #ffcd43; 1016 | } 1017 | .comment .commentInput .control .emjo .emjoArea { 1018 | display: none; 1019 | position: absolute; 1020 | z-index: 5; 1021 | width: 280px; 1022 | padding: 10px; 1023 | background-color: #fff; 1024 | box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0.16); 1025 | } 1026 | .comment .commentInput .control .emjo .emjoArea span { 1027 | display: inline-block; 1028 | height: 26px; 1029 | width: 26px; 1030 | line-height: 26px; 1031 | text-align: center; 1032 | font-size: 20px; 1033 | } 1034 | .comment .commentInput .control .emjo .emjoArea span:hover { 1035 | font-size: 24px; 1036 | } 1037 | .comment .commentInput .control .push { 1038 | float: right; 1039 | margin-top: 5px; 1040 | } 1041 | .comment h3 { 1042 | text-align: center; 1043 | font-size: 18px; 1044 | color: #777; 1045 | margin: 40px 20px 20px; 1046 | } 1047 | .comment .listCard { 1048 | padding: 10px 30px 10px 70px; 1049 | width: 100%; 1050 | position: relative; 1051 | } 1052 | .comment .listCard:hover .removeBlog { 1053 | display: block; 1054 | } 1055 | .comment .listCard:last-child .control { 1056 | border-bottom: 0; 1057 | } 1058 | .comment .listCard .removeBlog { 1059 | display: none; 1060 | position: absolute; 1061 | color: red; 1062 | right: 40px; 1063 | top: 10px; 1064 | cursor: pointer; 1065 | } 1066 | .comment .listCard .removeBlog i { 1067 | font-size: 30px; 1068 | } 1069 | .comment .listCard .comment-right { 1070 | padding-left: 50px; 1071 | } 1072 | .comment .listCard .comment-right .commentUserInfo { 1073 | color: #666; 1074 | } 1075 | .comment .listCard .comment-right .comment-content { 1076 | margin: 10px 0; 1077 | color: #555; 1078 | } 1079 | .comment .listCard .control { 1080 | padding: 0 10px; 1081 | height: 36px; 1082 | border-bottom: 1px solid #f1f1f1; 1083 | color: #999; 1084 | } 1085 | .comment .listCard .control .time { 1086 | float: left; 1087 | } 1088 | .comment .listCard .control .control-right { 1089 | float: right; 1090 | } 1091 | .fixedCard { 1092 | position: fixed; 1093 | margin-left: -12rem; 1094 | top: 24rem; 1095 | } 1096 | .fixedCard ul li { 1097 | list-style-type: none; 1098 | position: relative; 1099 | margin-bottom: 0.75rem; 1100 | width: 3rem; 1101 | height: 3rem; 1102 | line-height: 3rem; 1103 | background-color: #fff; 1104 | background-position: 50%; 1105 | background-repeat: no-repeat; 1106 | border-radius: 50%; 1107 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04); 1108 | cursor: pointer; 1109 | text-align: center; 1110 | color: #777; 1111 | } 1112 | .fixedCard ul li a { 1113 | color: #777; 1114 | } 1115 | .fixedCard ul li:hover { 1116 | color: #e59233; 1117 | } 1118 | .fixedCard ul li span { 1119 | position: absolute; 1120 | top: 0; 1121 | left: 75%; 1122 | padding: 0.1rem 0.4rem; 1123 | font-size: 1rem; 1124 | text-align: center; 1125 | line-height: 1; 1126 | white-space: nowrap; 1127 | color: #fff; 1128 | background-color: #b2bac2; 1129 | border-radius: 0.7rem; 1130 | transform-origin: left top; 1131 | transform: scale(0.75); 1132 | } 1133 | .fixedCard ul li i { 1134 | font-size: 1.9rem; 1135 | } 1136 | #pageList a { 1137 | cursor: pointer; 1138 | } 1139 | #movieTypeZiList { 1140 | cursor: pointer; 1141 | margin-top: 10px; 1142 | background-color: #f2f2f2; 1143 | margin-bottom: 4px; 1144 | } 1145 | #movieTypeZiList ul { 1146 | padding: 4px; 1147 | } 1148 | .yearList { 1149 | cursor: pointer; 1150 | } 1151 | --------------------------------------------------------------------------------