├── README.md ├── .gitignore ├── .idea ├── .gitignore ├── misc.xml ├── vcs.xml ├── modules.xml ├── pre-api.iml └── inspectionProfiles │ └── Project_Default.xml ├── mkdocs.yml ├── .env.example ├── mysql.go ├── go.mod ├── sphinx.go ├── logger.go ├── routes.go ├── team.go ├── main.go ├── nuke.go ├── LICENSE ├── router.go ├── go.sum ├── api.go ├── messages.go ├── websocket.go ├── pre.go ├── handlers.go └── docs └── index.md /README.md: -------------------------------------------------------------------------------- 1 | # pre-api -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Predb.ovh API 2 | theme: readthedocs 3 | nav: 4 | - Home: index.md 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HOSTNAME=predb.ovh 2 | AMQP_HOST=amqp://guest:guest@localhost:5672/ 3 | SEARCH_DATABASE=tcp(127.0.0.1:9306)/?interpolateParams=true 4 | NUKES_DATABASE=predb@tcp(127.0.0.1:3306)/predb?interpolateParams=true 5 | LISTEN_ADDRESS=127.0.0.1:8088 6 | -------------------------------------------------------------------------------- /mysql.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | ) 7 | 8 | func newMysql(dbString string) *sql.DB { 9 | mysql, err := sql.Open("mysql", dbString) 10 | if err != nil { 11 | log.Fatal(err) 12 | } 13 | 14 | return mysql 15 | } 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pre-api.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/predbdotovh/pre-api 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.6.0 7 | github.com/gorilla/feeds v1.1.1 8 | github.com/gorilla/mux v1.8.0 9 | github.com/gorilla/websocket v1.4.2 10 | github.com/joho/godotenv v1.4.0 11 | github.com/kr/pretty v0.2.1 // indirect 12 | github.com/streadway/amqp v1.0.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /sphinx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | const realTimeIndex = "pre_rt" 8 | const plainIndex = "pre_plain" 9 | const sphinxTable = plainIndex + ", " + realTimeIndex 10 | 11 | func sphinxMeta(tx *sql.Tx) (map[string]string, error) { 12 | sqlRows, err := tx.Query("SHOW META") 13 | if err != nil { 14 | return nil, err 15 | } 16 | defer sqlRows.Close() 17 | 18 | rows := make(map[string]string, 0) 19 | for sqlRows.Next() { 20 | var name, value string 21 | err := sqlRows.Scan(&name, &value) 22 | if err != nil { 23 | continue 24 | } 25 | rows[name] = value 26 | } 27 | 28 | return rows, nil 29 | } 30 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | ) 9 | 10 | var stdOutLog = log.New(os.Stdout, "", log.LstdFlags) 11 | 12 | func httpLogger(inner http.Handler, name string) http.Handler { 13 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | start := time.Now() 15 | 16 | inner.ServeHTTP(w, r) 17 | 18 | from := r.Header.Get("x-forwarded-for") 19 | if from == "" { 20 | from = r.RemoteAddr 21 | } 22 | 23 | stdOutLog.Printf( 24 | "%s %s %s %s %s", 25 | from, 26 | r.Method, 27 | r.RequestURI, 28 | name, 29 | time.Since(start), 30 | ) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type muxRoute struct { 8 | Name string 9 | Method string 10 | Pattern string 11 | Handler http.HandlerFunc 12 | } 13 | 14 | type muxRoutes []muxRoute 15 | type jsonMuxRoutes map[string]muxRoutes 16 | 17 | var jsonRoutes = jsonMuxRoutes{ 18 | "v1": muxRoutes{ 19 | muxRoute{"Root", "GET", "/", rootHandlerV1}, 20 | muxRoute{"Teams", "GET", "/teams", teamsHandlerV1}, 21 | muxRoute{"Stats", "GET", "/stats", statsHandlerV1}, 22 | muxRoute{"Live", "GET", "/live", liveHandlerV1}, 23 | muxRoute{"Rss", "GET", "/rss", rssHandlerV1}, 24 | muxRoute{"WS", "GET", "/ws", websocketHandlerV1}, 25 | }, 26 | } 27 | 28 | var triggerRoutes = muxRoutes{ 29 | muxRoute{"NukeTrigger", "POST", "/nuke", nukeTriggerHandlerV1}, 30 | muxRoute{"PreTrigger", "POST", "/{action}", preTriggerHandlerV1}, 31 | } 32 | -------------------------------------------------------------------------------- /team.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | type teamRow struct { 10 | Team string `json:"team"` 11 | FirstPre int `json:"firstPre"` 12 | LatestPre int `json:"latestPre"` 13 | Count int `json:"count"` 14 | } 15 | 16 | const maxListedTeams = 1000 17 | 18 | func listTeams(tx *sql.Tx) ([]teamRow, error) { 19 | sqlQuery := fmt.Sprintf("SELECT team, MIN(pre_at), MAX(pre_at), COUNT(*) AS count FROM %s GROUP BY team ORDER BY count DESC LIMIT %d", sphinxTable, maxListedTeams) 20 | 21 | rows, err := tx.Query(sqlQuery) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer rows.Close() 26 | 27 | res := make([]teamRow, 0) 28 | for rows.Next() { 29 | var r teamRow 30 | err := rows.Scan(&r.Team, &r.FirstPre, &r.LatestPre, &r.Count) 31 | if err != nil { 32 | log.Print(err) 33 | continue 34 | } 35 | 36 | res = append(res, r) 37 | } 38 | 39 | return res, nil 40 | } 41 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | _ "github.com/joho/godotenv/autoload" 10 | ) 11 | 12 | var sphinx *sql.DB 13 | var mysql *sql.DB 14 | var hostname string 15 | 16 | func main() { 17 | hostname := getEnv("HOSTNAME", "example.com") 18 | amqpHost := getEnv("AMQP_HOST", "") 19 | sphinxDatabase := getEnv("SEARCH_DATABASE", "tcp(127.0.0.1:9306)/?interpolateParams=true") 20 | nukesDatabase := getEnv("NUKES_DATABASE", "") 21 | listenAddr := getEnv("LISTEN_ADDRESS", "127.0.0.1:8088") 22 | 23 | sphinx = newMysql(sphinxDatabase) 24 | mysql = newMysql(nukesDatabase) 25 | 26 | router := newRouter(hostname) 27 | newMQ(amqpHost) 28 | 29 | log.Fatal(http.ListenAndServe(listenAddr, router)) 30 | } 31 | 32 | func getEnv(key, defaultValue string) string { 33 | value, exists := os.LookupEnv(key) 34 | if !exists { 35 | if defaultValue == "" { 36 | log.Fatal("Missing mandatory env variable : " + key) 37 | } 38 | 39 | return defaultValue 40 | } 41 | 42 | return value 43 | } 44 | -------------------------------------------------------------------------------- /nuke.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | const nukeTable = "nuke" 9 | 10 | type nuke struct { 11 | ID int `json:"id"` 12 | TypeID int `json:"typeId"` 13 | Type string `json:"type"` 14 | PreID int `json:"preId"` 15 | Reason string `json:"reason"` 16 | Net string `json:"net"` 17 | source string 18 | NukeAt int64 `json:"nukeAt"` 19 | } 20 | 21 | var nukeTypes = map[int]string{ 22 | 1: "nuke", 23 | 2: "unnuke", 24 | 3: "modnuke", 25 | 4: "delpre", 26 | 5: "undelpre", 27 | } 28 | 29 | func (n *nuke) setType() { 30 | if n.Type == "" { 31 | n.Type = nukeTypes[n.TypeID] 32 | } 33 | } 34 | 35 | func getNuke(db *sql.DB, preID int) (*nuke, error) { 36 | sqlQuery := fmt.Sprintf("SELECT id, pre_id, nuke_id, reason, net, source, UNIX_TIMESTAMP(set_at) FROM %s WHERE pre_id = ? ORDER BY id DESC LIMIT 1", nukeTable) 37 | 38 | var r nuke 39 | 40 | err := db.QueryRow(sqlQuery, preID).Scan(&r.ID, &r.PreID, &r.TypeID, &r.Reason, &r.Net, &r.source, &r.NukeAt) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return &r, nil 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 predbdotovh 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 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | func newRouter(hostname string) *mux.Router { 13 | backendUpdates = make(chan triggerAction) 14 | go backendPump() 15 | 16 | router := mux.NewRouter() 17 | 18 | publicRouter := router. 19 | StrictSlash(true). 20 | Host(hostname). 21 | PathPrefix("/api"). 22 | Subrouter() 23 | 24 | for ver, jRoutes := range jsonRoutes { 25 | versionRouter := publicRouter.PathPrefix("/" + ver).Subrouter() 26 | for _, r := range jRoutes { 27 | versionRouter. 28 | Methods(r.Method). 29 | Path(r.Pattern). 30 | Name(r.Name). 31 | Handler(httpLogger(r.Handler, r.Name)) 32 | } 33 | } 34 | 35 | privateRouter := router. 36 | StrictSlash(true). 37 | Host("localhost"). 38 | PathPrefix("/trigger"). 39 | Subrouter() 40 | 41 | for _, r := range triggerRoutes { 42 | privateRouter. 43 | Methods(r.Method). 44 | Path(r.Pattern). 45 | Name(r.Name). 46 | Handler(httpLogger(r.Handler, r.Name)) 47 | } 48 | 49 | router.NotFoundHandler = http.HandlerFunc(notFound) 50 | 51 | return router 52 | } 53 | 54 | func notFound(w http.ResponseWriter, _ *http.Request) { 55 | w.Header().Set("Cache-Control", "no-store") 56 | err := apiErr(w, errors.New("not Found")) 57 | if err != nil { 58 | log.Println(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 2 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 3 | github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= 4 | github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= 5 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 6 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 8 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 9 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 10 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 11 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 12 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 13 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 14 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 15 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 16 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 17 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 18 | github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= 19 | github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 20 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | type apiResponse struct { 10 | Status string `json:"status"` 11 | Message string `json:"message"` 12 | Data interface{} `json:"data"` 13 | } 14 | 15 | type apiRowData struct { 16 | RowCount int `json:"rowCount"` 17 | Rows []preRow `json:"rows"` 18 | Offset int `json:"offset"` 19 | ReqCount int `json:"reqCount"` 20 | Total int `json:"total"` 21 | Time float64 `json:"time"` 22 | } 23 | 24 | type apiStatsData struct { 25 | Total int `json:"total"` 26 | Date time.Time `json:"date"` 27 | Time float64 `json:"time"` 28 | } 29 | 30 | type apiTeamsData struct { 31 | RowCount int `json:"rowCount"` 32 | Rows []teamRow `json:"rows"` 33 | Offset int `json:"offset"` 34 | ReqCount int `json:"reqCount"` 35 | Total int `json:"total"` 36 | Time float64 `json:"time"` 37 | } 38 | 39 | const indent = " " 40 | 41 | func apiSuccess(w http.ResponseWriter, o interface{}) error { 42 | return apiSend(w, apiResponse{"success", "", o}) 43 | } 44 | 45 | func apiFail(w http.ResponseWriter, o interface{}) error { 46 | return apiSend(w, apiResponse{"error", "", o}) 47 | } 48 | 49 | func apiErr(w http.ResponseWriter, err error) error { 50 | return apiSend(w, apiResponse{"error", err.Error(), nil}) 51 | } 52 | 53 | func apiSend(w http.ResponseWriter, o interface{}) error { 54 | headers := w.Header() 55 | headers.Set("Content-Type", "application/json; charset=utf-8") 56 | headers.Set("Access-Control-Allow-Origin", "*") 57 | w.WriteHeader(http.StatusOK) 58 | 59 | enc := json.NewEncoder(w) 60 | enc.SetIndent("", indent) 61 | 62 | return enc.Encode(o) 63 | } 64 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/streadway/amqp" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func newMQ(amqpHost string) { 11 | if amqpHost == "" { 12 | return 13 | } 14 | 15 | conn, err := amqp.Dial(amqpHost) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | // defer conn.Close() 20 | 21 | ch, err := conn.Channel() 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | // defer ch.Close() 26 | 27 | err = ch.ExchangeDeclare( 28 | "predb", 29 | "fanout", 30 | true, 31 | false, 32 | false, 33 | false, 34 | nil, 35 | ) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | q, err := ch.QueueDeclare( 41 | "pre-releases", 42 | false, 43 | false, 44 | false, 45 | false, 46 | nil, 47 | ) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | err = ch.QueueBind( 53 | q.Name, 54 | "", 55 | "predb", 56 | false, 57 | nil, 58 | ) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | msgs, err := ch.Consume( 64 | q.Name, 65 | "pre-api", 66 | true, 67 | false, 68 | false, 69 | false, 70 | nil, 71 | ) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | go mqRun(msgs) 77 | } 78 | 79 | func mqRun(msgs <-chan amqp.Delivery) { 80 | for d := range msgs { 81 | log.Println(d.RoutingKey) 82 | switch d.RoutingKey { 83 | case "pre.insert": 84 | fallthrough 85 | case "pre.update": 86 | fallthrough 87 | case "pre.delete": 88 | var p preRow 89 | err := json.Unmarshal(d.Body, &p) 90 | if err != nil { 91 | log.Println(err) 92 | return 93 | } 94 | 95 | p.proc() 96 | if d.RoutingKey == "pre.delete" { 97 | _, err = p.deleteFromIndex() 98 | if err != nil { 99 | log.Println(err) 100 | return 101 | } 102 | } else { 103 | _, err = p.insertOrUpdateIndex() 104 | if err != nil { 105 | log.Println(err) 106 | return 107 | } 108 | } 109 | backendUpdates <- triggerAction{Action: strings.Split(d.RoutingKey, ".")[1], Row: &p} 110 | break 111 | case "nuke.insert": 112 | var n nuke 113 | err := json.Unmarshal(d.Body, &n) 114 | if err != nil { 115 | log.Println(err) 116 | return 117 | } 118 | 119 | p, err := getPre(sphinx, n.PreID, false) 120 | if err != nil { 121 | log.Println(err) 122 | return 123 | } 124 | 125 | p.setNuke(&n) 126 | backendUpdates <- triggerAction{Action: n.Type, Row: p} 127 | break 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /websocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | type triggerAction struct { 12 | Action string `json:"action"` 13 | Row *preRow `json:"row"` 14 | } 15 | 16 | type wsClient struct { 17 | conn *websocket.Conn 18 | send chan triggerAction 19 | } 20 | 21 | const ( 22 | writeTimeout = 10 * time.Second 23 | pongWait = 60 * time.Second 24 | pingPeriod = 50 * time.Second 25 | ) 26 | 27 | var upgrader = websocket.Upgrader{ 28 | CheckOrigin: func(r *http.Request) bool { return true }, 29 | } 30 | 31 | var backendUpdates chan triggerAction 32 | 33 | func websocketUpgrader(w http.ResponseWriter, r *http.Request) { 34 | ws, err := upgrader.Upgrade(w, r, nil) 35 | if err != nil { 36 | log.Println(err) 37 | return 38 | } 39 | defer ws.Close() 40 | 41 | c := &wsClient{conn: ws, send: make(chan triggerAction)} 42 | clients[c] = true 43 | go c.writePump() 44 | c.readPump() 45 | } 46 | 47 | var clients map[*wsClient]bool 48 | 49 | func backendPump() { 50 | backendUpdates = make(chan triggerAction) 51 | clients = make(map[*wsClient]bool) 52 | 53 | for { 54 | select { 55 | case rls := <-backendUpdates: 56 | for c := range clients { 57 | select { 58 | case c.send <- rls: 59 | default: 60 | log.Println("CLOSE") 61 | close(c.send) 62 | delete(clients, c) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | func (c *wsClient) writePump() { 70 | ticker := time.NewTicker(pingPeriod) 71 | 72 | defer ticker.Stop() 73 | for { 74 | select { 75 | case rls := <-c.send: 76 | err := c.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) 77 | if err != nil { 78 | log.Println("WRITE DEADLINE ERR", err) 79 | } 80 | err = c.conn.WriteJSON(rls) 81 | if err != nil { 82 | log.Println("WRITE ERR", err) 83 | return 84 | } 85 | case <-ticker.C: 86 | err := c.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) 87 | if err != nil { 88 | log.Println("PING DEADLINE ERR", err) 89 | } 90 | err = c.conn.WriteMessage(websocket.PingMessage, []byte{}) 91 | if err != nil { 92 | log.Println("PING ERR", err) 93 | return 94 | } 95 | } 96 | } 97 | } 98 | 99 | func (c *wsClient) readPump() { 100 | err := c.conn.SetReadDeadline(time.Now().Add(pongWait)) 101 | if err != nil { 102 | log.Println("READ DEADLINE ERR", err) 103 | return 104 | } 105 | c.conn.SetPongHandler(func(string) error { 106 | err = c.conn.SetReadDeadline(time.Now().Add(pongWait)) 107 | if err != nil { 108 | log.Println("READ DEADLINE ERR", err) 109 | } 110 | return nil 111 | }) 112 | for { 113 | _, _, err := c.conn.NextReader() 114 | if err != nil { 115 | log.Println("READ ERR", err) 116 | break 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pre.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type preRow struct { 12 | ID int `json:"id"` 13 | Name string `json:"name"` 14 | Team string `json:"team"` 15 | Cat string `json:"cat"` 16 | Genre string `json:"genre"` 17 | URL string `json:"url"` 18 | Size float64 `json:"size"` 19 | Files int `json:"files"` 20 | PreAt int `json:"preAt"` 21 | Nuke *nuke `json:"nuke"` 22 | } 23 | 24 | var replacer = strings.NewReplacer("(", "\\(", ")", "\\)") 25 | 26 | const preColumns = "id, name, team, cat, genre, url, size, files, pre_at" 27 | const defaultMaxMatches = 1000 28 | 29 | func (p *preRow) proc() { 30 | p.Size /= 1000 31 | } 32 | 33 | func scanPresRows(rows *sql.Rows, appendNukes bool) []preRow { 34 | res := make([]preRow, 0) 35 | for rows.Next() { 36 | var r preRow 37 | err := rows.Scan(&r.ID, &r.Name, &r.Team, &r.Cat, &r.Genre, &r.URL, &r.Size, &r.Files, &r.PreAt) 38 | if err != nil { 39 | log.Print(err) 40 | continue 41 | } 42 | 43 | r.proc() 44 | if appendNukes { 45 | r.fetchNuke(mysql) 46 | } 47 | 48 | res = append(res, r) 49 | } 50 | 51 | return res 52 | } 53 | 54 | func getPre(db *sql.DB, preID int, withNuke bool) (*preRow, error) { 55 | sqlQuery := fmt.Sprintf("SELECT %s FROM %s WHERE id = ?", preColumns, sphinxTable) 56 | 57 | var r preRow 58 | 59 | err := db.QueryRow(sqlQuery, preID).Scan(&r.ID, &r.Name, &r.Team, &r.Cat, &r.Genre, &r.URL, &r.Size, &r.Files, &r.PreAt) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | r.proc() 65 | if withNuke { 66 | r.fetchNuke(mysql) 67 | } 68 | 69 | return &r, nil 70 | } 71 | 72 | func getPresById(tx *sql.Tx, preID int, withNuke bool) ([]preRow, error) { 73 | sqlQuery := fmt.Sprintf("SELECT %s FROM %s WHERE id = ?", preColumns, sphinxTable) 74 | 75 | var r preRow 76 | 77 | err := tx.QueryRow(sqlQuery, preID).Scan(&r.ID, &r.Name, &r.Team, &r.Cat, &r.Genre, &r.URL, &r.Size, &r.Files, &r.PreAt) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | r.proc() 83 | if withNuke { 84 | r.fetchNuke(mysql) 85 | } 86 | 87 | return []preRow{r}, nil 88 | } 89 | 90 | func searchPres(tx *sql.Tx, q string, offsetInt, countInt int, withNukes bool) ([]preRow, error) { 91 | sqlQuery := fmt.Sprintf("SELECT %s FROM %s WHERE MATCH(?) ORDER BY id DESC LIMIT %d,%d", preColumns, sphinxTable, offsetInt, countInt) 92 | 93 | if offsetInt+countInt > defaultMaxMatches { 94 | sqlQuery += ", max_matches = " + strconv.Itoa(offsetInt+countInt) 95 | } 96 | 97 | sqlRows, err := tx.Query(sqlQuery, replacer.Replace(q)) 98 | if err != nil { 99 | return nil, err 100 | } 101 | defer sqlRows.Close() 102 | 103 | return scanPresRows(sqlRows, withNukes), nil 104 | } 105 | 106 | func latestPres(tx *sql.Tx, offsetInt, countInt int, withNukes bool) ([]preRow, error) { 107 | sqlQuery := fmt.Sprintf("SELECT %s FROM %s ORDER BY id DESC LIMIT %d,%d", preColumns, sphinxTable, offsetInt, countInt) 108 | 109 | if offsetInt+countInt > defaultMaxMatches { 110 | sqlQuery += ", max_matches = " + strconv.Itoa(offsetInt+countInt) 111 | } 112 | 113 | sqlRows, err := tx.Query(sqlQuery) 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer sqlRows.Close() 118 | 119 | return scanPresRows(sqlRows, withNukes), nil 120 | } 121 | 122 | func (p *preRow) fetchNuke(db *sql.DB) { 123 | n, err := getNuke(db, p.ID) 124 | if err != nil { 125 | return 126 | } 127 | p.setNuke(n) 128 | } 129 | 130 | func (p *preRow) setNuke(n *nuke) { 131 | if n != nil { 132 | n.setType() 133 | p.Nuke = n 134 | } 135 | } 136 | 137 | func (p *preRow) insertOrUpdateIndex() (int64, error) { 138 | sqlQuery := fmt.Sprintf("REPLACE INTO %s (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", realTimeIndex, preColumns) 139 | 140 | res, err := sphinx.Exec(sqlQuery, p.ID, p.Name, p.Team, p.Cat, p.Genre, p.URL, p.Size, p.Files, p.PreAt) 141 | if err != nil { 142 | return 0, err 143 | } 144 | 145 | return res.RowsAffected() 146 | } 147 | 148 | func (p *preRow) deleteFromIndex() (int64, error) { 149 | sqlQuery := fmt.Sprintf("DELETE FROM %s WHERE id = ?", realTimeIndex) 150 | 151 | res, err := sphinx.Exec(sqlQuery, p.ID) 152 | if err != nil { 153 | return 0, err 154 | } 155 | 156 | return res.RowsAffected() 157 | } 158 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/gorilla/feeds" 14 | "github.com/gorilla/mux" 15 | ) 16 | 17 | const defaultOffset = 0 18 | const defaultCount = 20 19 | const maxCount = 100 20 | 21 | func pagination(query url.Values) (int, int, error) { 22 | var err error 23 | count := defaultCount 24 | offset := defaultOffset 25 | 26 | countStr := query.Get("count") 27 | if countStr != "" { 28 | count, err = strconv.Atoi(countStr) 29 | if err != nil { 30 | return 0, 0, fmt.Errorf("incorrect parameters (count)") 31 | } 32 | 33 | } 34 | if count > maxCount { 35 | count = maxCount 36 | } 37 | if count < 0 { 38 | count = defaultCount 39 | } 40 | 41 | pageStr := query.Get("page") 42 | if pageStr != "" { 43 | page, err := strconv.Atoi(pageStr) 44 | if err != nil { 45 | return 0, 0, fmt.Errorf("incorrect parameters (page)") 46 | } 47 | 48 | offset = (page - 1) * count 49 | } 50 | 51 | offsetStr := query.Get("offset") 52 | if offsetStr != "" { 53 | offset, err = strconv.Atoi(offsetStr) 54 | if err != nil { 55 | return 0, 0, fmt.Errorf("incorrect parameters (offset)") 56 | } 57 | } 58 | if offset < 0 { 59 | offset = defaultOffset 60 | } 61 | 62 | return offset, count, nil 63 | } 64 | 65 | func handleQuery(r *http.Request) (*apiRowData, error) { 66 | t := time.Now() 67 | 68 | query := r.URL.Query() 69 | idStr := query.Get("id") 70 | q := query.Get("q") 71 | 72 | offset, count, err := pagination(query) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | tx, err := sphinx.Begin() 78 | if err != nil { 79 | return nil, err 80 | } 81 | defer tx.Commit() 82 | 83 | rows := make([]preRow, 0) 84 | if idStr != "" { 85 | id, err := strconv.Atoi(idStr) 86 | if err != nil { 87 | return nil, err 88 | } 89 | rows, err = getPresById(tx, id, true) 90 | } else if q == "" { 91 | rows, err = latestPres(tx, offset, count, true) 92 | } else { 93 | rows, err = searchPres(tx, q, offset, count, true) 94 | } 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | meta, err := sphinxMeta(tx) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | total, err := strconv.Atoi(meta["total_found"]) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | data := &apiRowData{ 110 | RowCount: len(rows), 111 | Rows: rows, 112 | Offset: offset, 113 | ReqCount: count, 114 | Total: total, 115 | Time: time.Since(t).Seconds(), 116 | } 117 | 118 | return data, nil 119 | } 120 | 121 | func rootHandlerV1(w http.ResponseWriter, r *http.Request) { 122 | data, err := handleQuery(r) 123 | if err != nil { 124 | err = apiErr(w, err) 125 | if err != nil { 126 | log.Println(err) 127 | } 128 | return 129 | } 130 | 131 | w.Header().Set("Cache-Control", "public, max-age=60") 132 | err = apiSuccess(w, data) 133 | if err != nil { 134 | log.Println(err) 135 | } 136 | } 137 | 138 | func teamsHandlerV1(w http.ResponseWriter, _ *http.Request) { 139 | t := time.Now() 140 | 141 | tx, err := sphinx.Begin() 142 | if err != nil { 143 | err = apiErr(w, err) 144 | if err != nil { 145 | log.Println(err) 146 | } 147 | return 148 | } 149 | defer tx.Commit() 150 | 151 | rows, err := listTeams(tx) 152 | if err != nil { 153 | err = apiErr(w, err) 154 | if err != nil { 155 | log.Println(err) 156 | } 157 | 158 | return 159 | } 160 | 161 | data := &apiTeamsData{ 162 | RowCount: len(rows), 163 | Rows: rows, 164 | Offset: 0, 165 | ReqCount: maxListedTeams, 166 | Total: len(rows), 167 | Time: time.Since(t).Seconds(), 168 | } 169 | 170 | w.Header().Set("Cache-Control", "public, max-age=3600") 171 | err = apiSuccess(w, data) 172 | if err != nil { 173 | log.Println(err) 174 | } 175 | } 176 | 177 | func liveHandlerV1(w http.ResponseWriter, r *http.Request) { 178 | data, err := handleQuery(r) 179 | if err != nil { 180 | err = apiErr(w, err) 181 | if err != nil { 182 | log.Println(err) 183 | } 184 | return 185 | } 186 | 187 | w.Header().Set("Cache-Control", "no-cache") 188 | err = apiSuccess(w, data) 189 | if err != nil { 190 | log.Println(err) 191 | } 192 | } 193 | 194 | func websocketHandlerV1(w http.ResponseWriter, r *http.Request) { 195 | websocketUpgrader(w, r) 196 | } 197 | 198 | func preTriggerHandlerV1(w http.ResponseWriter, r *http.Request) { 199 | if r.Header.Get("x-forwarded-for") != "" { 200 | err := apiErr(w, errors.New("not authorized")) 201 | if err != nil { 202 | log.Println(err) 203 | } 204 | return 205 | } 206 | 207 | var p preRow 208 | err := json.NewDecoder(r.Body).Decode(&p) 209 | if err != nil { 210 | err = apiFail(w, err) 211 | if err != nil { 212 | log.Println(err) 213 | } 214 | return 215 | } 216 | defer r.Body.Close() 217 | 218 | p.proc() 219 | 220 | backendUpdates <- triggerAction{Action: mux.Vars(r)["action"], Row: &p} 221 | err = apiSuccess(w, nil) 222 | if err != nil { 223 | log.Println(err) 224 | } 225 | } 226 | 227 | func nukeTriggerHandlerV1(w http.ResponseWriter, r *http.Request) { 228 | if r.Header.Get("x-forwarded-for") != "" { 229 | err := apiErr(w, errors.New("not authorized")) 230 | if err != nil { 231 | log.Println(err) 232 | } 233 | return 234 | } 235 | 236 | var n nuke 237 | err := json.NewDecoder(r.Body).Decode(&n) 238 | if err != nil { 239 | err = apiFail(w, err) 240 | if err != nil { 241 | log.Println(err) 242 | } 243 | return 244 | } 245 | defer r.Body.Close() 246 | 247 | p, err := getPre(sphinx, n.PreID, false) 248 | if err != nil { 249 | err = apiFail(w, err) 250 | if err != nil { 251 | log.Println(err) 252 | } 253 | return 254 | } 255 | 256 | p.setNuke(&n) 257 | 258 | backendUpdates <- triggerAction{Action: n.Type, Row: p} 259 | err = apiSuccess(w, nil) 260 | if err != nil { 261 | log.Println(err) 262 | } 263 | } 264 | 265 | func statsHandlerV1(w http.ResponseWriter, _ *http.Request) { 266 | t := time.Now() 267 | 268 | tx, err := sphinx.Begin() 269 | if err != nil { 270 | err = apiErr(w, err) 271 | if err != nil { 272 | log.Println(err) 273 | } 274 | return 275 | } 276 | defer tx.Commit() 277 | 278 | _, err = latestPres(tx, 0, 0, false) 279 | if err != nil { 280 | err = apiErr(w, err) 281 | if err != nil { 282 | log.Println(err) 283 | } 284 | return 285 | } 286 | 287 | meta, err := sphinxMeta(tx) 288 | if err != nil { 289 | err = apiErr(w, err) 290 | if err != nil { 291 | log.Println(err) 292 | } 293 | return 294 | } 295 | 296 | total, err := strconv.Atoi(meta["total_found"]) 297 | if err != nil { 298 | log.Println(err) 299 | } 300 | 301 | w.Header().Set("Cache-Control", "public, max-age=60") 302 | data := apiStatsData{ 303 | Total: total, 304 | Date: time.Now(), 305 | Time: time.Since(t).Seconds(), 306 | } 307 | err = apiSuccess(w, data) 308 | if err != nil { 309 | log.Println(err) 310 | } 311 | } 312 | 313 | func rssHandlerV1(w http.ResponseWriter, r *http.Request) { 314 | data, err := handleQuery(r) 315 | if err != nil { 316 | w.WriteHeader(http.StatusInternalServerError) 317 | return 318 | } 319 | 320 | feed := &feeds.Feed{ 321 | Title: "PreDB", 322 | Link: &feeds.Link{Href: fmt.Sprintf("https://%s/", hostname)}, 323 | Created: time.Now(), 324 | } 325 | 326 | for _, row := range data.Rows { 327 | feed.Items = append(feed.Items, &feeds.Item{ 328 | Title: row.Name, 329 | Link: &feeds.Link{Href: fmt.Sprintf("https://%s/?id=%d", hostname, row.ID)}, 330 | Description: fmt.Sprintf("Cat:%s | Genre:%s | Size:%0.fMB | Files:%d | ID:%d", row.Cat, row.Genre, row.Size, row.Files, row.ID), 331 | Created: time.Unix(int64(row.PreAt), 0), 332 | }) 333 | } 334 | 335 | rss, err := feed.ToRss() 336 | if err != nil { 337 | w.WriteHeader(http.StatusInternalServerError) 338 | return 339 | } 340 | 341 | _, err = w.Write([]byte(rss)) 342 | if err != nil { 343 | log.Println(err) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Predb.ovh API documentation 2 | 3 | Most pre databases websites do not allow data scapping, nor do they offer an API. 4 | 5 | Having a easily accessible API was a requirement for me, so I built this one using public pre sources. 6 | 7 | ## Disclaimers 8 | 9 | Disclaimer : This API has been built for my personal usage, and may not fit your needs. 10 | [Github issues](https://github.com/predbdotovh/pre-api/issues) can be filled, but don't expect too much. 11 | 12 | Disclaimer 2 : Once again, this API does **NOT** host any content, only metadata associated to scene releases. 13 | 14 | Disclaimer 3 : Don't know what scene releases are ? You're probably at the wrong place. 15 | 16 | ## Status 17 | 18 | This API is currently usable, and used by [demo website](https://predb.ovh/) which is 19 | also [open-sourced](https://github.com/predbdotovh/website-vuejs). 20 | 21 | As the API is fed by Sphinx, results are hard limited at 1000, and I don't expect to modify this behaviour in the 22 | future. 23 | 24 | A monthly-ish sql dump is available here : [https://predb.ovh/download/](https://predb.ovh/download/) 25 | 26 | ## API versions 27 | 28 | Current version is **v1**. This version is expected to be updated on each breaking change. 29 | 30 | Current base URL is : [https://predb.ovh/api/v1/](https://predb.ovh/api/v1/) 31 | 32 | ## Responses 33 | 34 | The API will always return HTTP 200 with application/json content (except for [RSS](#get-rss)). 35 | 36 | On success : 37 | 38 | ``` 39 | { 40 | "status": "success", 41 | "message": "", 42 | "data": "... See endpoints responses ..." 43 | } 44 | ``` 45 | 46 | On failure : 47 | 48 | ``` 49 | { 50 | "status": "error", 51 | "message": "Human readable error message", 52 | "data": null 53 | } 54 | ``` 55 | 56 | ## Endpoints 57 | 58 | ### GET / 59 | 60 | List releases matching a set of filters given in parameters 61 | 62 | - Cache : 60 seconds 63 | - Usage : Get releases info 64 | - Rate limit : 30/60s 65 | 66 | #### Parameters 67 | 68 | All parameters are optional 69 | 70 | | param key | type | content | 71 | | --------- | ------ | ------------------------------------ | 72 | | count | int | Maximum releases count expected | 73 | | page | int | Page offset | 74 | | offset | int | Row offset (overwrites page param) | 75 | | q | string | Query | 76 | | id | int | Specific pre ID (overwrites q param) | 77 | 78 | Query is directly fed to a SphinxSearch engine, 79 | allowing [specific syntax](http://sphinxsearch.com/docs/current/extended-syntax.html). Note: cat and team are indexed, 80 | allowing fast queries like 81 | [https://predb.ovh/api/v1/?q=@cat%20EBOOK](https://predb.ovh/api/v1/?q=@cat%20EBOOK) 82 | 83 | #### Response 84 | 85 | ##### Data 86 | 87 | | json key | type | content | 88 | | -------- | --------- | ----------------------------- | 89 | | rowCount | int | Count of rows returned | 90 | | offset | int | Row count offset requested | 91 | | reqCount | int | Row count requested | 92 | | total | int | Total matching rows | 93 | | time | float | Request internal duration | 94 | | rows | []release | Array of [releases](#release) | 95 | 96 | ##### Release 97 | 98 | | json key | type | content | 99 | | -------- | --------- | --------------------------------- | 100 | | id | int | Internal unique pre ID | 101 | | name | string | Release name | 102 | | team | string | Release group extracted from name | 103 | | cat | string | Category | 104 | | genre | string | Genre | 105 | | url | string | Info link | 106 | | size | float | Release size in kb | 107 | | files | int | Original file count | 108 | | preAt | int | Release pre timestamp | 109 | | nuke | nuke/null | [Nuke](#nuke) info if available | 110 | 111 | ##### Nuke 112 | 113 | | json key | type | content | 114 | | -------- | ------ | --------------------------- | 115 | | id | int | Internal unique nuke ID | 116 | | typeId | int | [Nuke type](#nuke-types) ID | 117 | | type | string | [Nuke type](#nuke-types) | 118 | | preId | int | Nuked pre ID | 119 | | reason | string | Nuke reason | 120 | | net | string | Nuke source net | 121 | | nukeAt | int | Nuke timestamp | 122 | 123 | ##### Nuke types 124 | 125 | Known nuke types and type ids 126 | 127 | | nuke type ID | nuke type | 128 | | ------------ | --------- | 129 | | 1 | nuke | 130 | | 2 | unnuke | 131 | | 3 | modnuke | 132 | | 4 | delpre | 133 | | 5 | undelpre | 134 | 135 | #### Example 136 | 137 | - [https://predb.ovh/api/v1/?q=bdrip](https://predb.ovh/api/v1/?q=bdrip) 138 | 139 | ``` 140 | { 141 | "status": "success", 142 | "message": "", 143 | "data": { 144 | "rowCount": 20, 145 | "rows": [ 146 | { 147 | "id": 7813747, 148 | "name": "Fortitude.S02E10.German.DUBBED.BDRip.x264-AIDA", 149 | "team": "AIDA", 150 | "cat": "TV-DVDRIP", 151 | "genre": "", 152 | "url": "http://imdb.com/title/tt3498622/", 153 | "size": 0, 154 | "files": 0, 155 | "preAt": 1493815560, 156 | "nuke": null 157 | }, 158 | { 159 | "snip": "Content snipped for lisibility" 160 | }, 161 | { 162 | "id": 7812994, 163 | "name": "Fortitude.S02E07.BDRip.x264-HAGGiS", 164 | "team": "HAGGiS", 165 | "cat": "TV-DVDRIP", 166 | "genre": "", 167 | "url": "http://imdb.com/title/tt3498622", 168 | "size": 258, 169 | "files": 19, 170 | "preAt": 1493762423, 171 | "nuke": null 172 | } 173 | ], 174 | "offset": 0, 175 | "reqCount": 20, 176 | "total": 70729, 177 | "time": 0.108095174 178 | } 179 | } 180 | ``` 181 | 182 | ### GET /live 183 | 184 | This method is the exact clone of [GET /](#get), without any HTTP cache. 185 | 186 | - Cache : None 187 | - Usage : Get fresh data before listening to websocket updates 188 | - Rate limit : 2/20s 189 | 190 | **To avoid abuse, this method is severly rate limited** 191 | 192 | #### Example 193 | 194 | - [https://predb.ovh/api/v1/live](https://predb.ovh/api/v1/live) 195 | 196 | ``` 197 | { 198 | "status": "success", 199 | "message": "", 200 | "data": { 201 | "rowCount": 20, 202 | "rows": [ 203 | { 204 | "id": 7814699, 205 | "name": "AllOver30.com_17.05.03.Drew.Jones.XXX.iMAGESET-YAPG", 206 | "team": "YAPG", 207 | "cat": "XXX-iMGSET", 208 | "genre": "Mature.Women", 209 | "url": "http://HTTP://AllOver30.com", 210 | "size": 211.9, 211 | "files": 45, 212 | "preAt": 1493881734, 213 | "nuke": null 214 | }, 215 | { 216 | "snip": "Content snipped for lisibility" 217 | }, 218 | { 219 | "id": 7814680, 220 | "name": "Robert_Abigail_Feat_13_Amps_-_Living_On_The_Right_Side-(AG_010)-WEB-2017-ZzZz", 221 | "team": "ZzZz", 222 | "cat": "MP3", 223 | "genre": "Dance", 224 | "url": "http://junodownload.com", 225 | "size": 7, 226 | "files": 1, 227 | "preAt": 1493880783, 228 | "nuke": null 229 | } 230 | ], 231 | "offset": 0, 232 | "reqCount": 20, 233 | "total": 7751247, 234 | "time": 0.898239015 235 | } 236 | } 237 | ``` 238 | 239 | ### GET /rss 240 | 241 | This method is the exact clone of [GET /](#get), formatted using RSS2.0 spec. 242 | 243 | - Cache : 60 seconds 244 | - Usage : Get releases info using a RSS reader 245 | - Rate limit : 30/60s 246 | 247 | #### Example 248 | 249 | - [https://predb.ovh/api/v1/rss](https://predb.ovh/api/v1/rss) 250 | 251 | ``` 252 | 253 | 254 | 255 | PreDB 256 | https://predb.ovh/ 257 | 258 | Sun, 15 Dec 2019 21:23:59 +0100 259 | 260 | Le.Steppe.Dell.Asia.La.Piu.Grande.Steppa.Della.Terra.iTALiAN.HDTV.x264-iDiB 261 | https://predb.ovh/?id=9643108 262 | Cat:TV-SD-X264 | Genre: | Size:0MB | Files:0 | ID:9643108 263 | Sun, 15 Dec 2019 21:23:49 +0100 264 | 265 | 266 | Le.Steppe.Dell.Asia.La.Piu.Grande.Steppa.Della.Terra.iTALiAN.720p.HDTV.x264-iDiB 267 | https://predb.ovh/?id=9643107 268 | Cat:TV-HD-X264 | Genre: | Size:0MB | Files:0 | ID:9643107 269 | Sun, 15 Dec 2019 21:23:48 +0100 270 | 271 | 272 | 273 | ``` 274 | 275 | ### GET /teams 276 | 277 | Teams stats with first and latest pre, and total recorded pre. 278 | 279 | This is currently hard limited to 1000 results, may be subject to change if necessary. 280 | 281 | - Cache : 3600 seconds 282 | - Usage : Teams statistics 283 | 284 | #### Parameters 285 | 286 | None 287 | 288 | #### Response 289 | 290 | ##### Data 291 | 292 | | json key | type | content | 293 | | -------- | --------- | ----------------------------- | 294 | | rowCount | int | Count of rows returned | 295 | | offset | int | Row count offset requested | 296 | | reqCount | int | Row count requested | 297 | | total | int | Total matching rows | 298 | | time | float | Request internal duration | 299 | | rows | []team | Array of [teams](#team) | 300 | 301 | ##### Team 302 | 303 | | json key | type | content | 304 | | --------- | --------- | ------------------------------------- | 305 | | team | string | Release group name | 306 | | firstPre | int | First recorded release pre timestamp | 307 | | latestPre | int | Latest recorded release pre timestamp | 308 | | count | int | Total team pre count | 309 | 310 | #### Example 311 | 312 | - [https://predb.ovh/api/v1/teams](https://predb.ovh/api/v1/teams) 313 | 314 | ``` 315 | { 316 | "status": "success", 317 | "message": "", 318 | "data": { 319 | "rowCount": 1000, 320 | "rows": [ 321 | { 322 | "team": "KTR", 323 | "firstPre": 1203561700, 324 | "latestPre": 1598341396, 325 | "count": 307933 326 | }, 327 | { 328 | "snip": "Content snipped for lisibility" 329 | }, 330 | ], 331 | "time": 5.465514844 332 | } 333 | } 334 | ``` 335 | 336 | ### GET /stats 337 | 338 | Basic stats about internal database health. 339 | 340 | - Cache : 60 seconds 341 | - Usage : Keep track of current database status and response times 342 | 343 | #### Parameters 344 | 345 | None 346 | 347 | #### Response 348 | 349 | ##### Data 350 | 351 | | json key | type | content | 352 | | -------- | ------ | ----------------------------------- | 353 | | total | int | Total indexed releases count | 354 | | date | string | Current RFC3339 timestamp of server | 355 | | time | int | Full index scan duration | 356 | 357 | #### Example 358 | 359 | - [https://predb.ovh/api/v1/stats](https://predb.ovh/api/v1/stats) 360 | 361 | ``` 362 | { 363 | "status": "success", 364 | "message": "", 365 | "data": { 366 | "total": 7751280, 367 | "date": "2017-05-04T09:49:42.527504724+02:00", 368 | "time": 0.893255737 369 | } 370 | } 371 | ``` 372 | 373 | ### GET /ws 374 | 375 | A websocket endpoint, sending near realtime updates. To use this, you need to bind using 376 | a [Websocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications). 377 | 378 | - Cache : None 379 | - Usage : Realtime updates 380 | 381 | #### Parameters 382 | 383 | There is no parameter, and any input on the websocket is discarded 384 | 385 | #### Response 386 | 387 | There is no response per se, but a range of frames sent over time 388 | 389 | ##### Frame 390 | 391 | Each frame represents an action towards a specific release. 392 | 393 | | json key | type | content | 394 | | -------- | ------- | ------------------- | 395 | | action | string | Action type | 396 | | row | release | [Release](#release) | 397 | 398 | ##### Action types 399 | 400 | Known action types 401 | 402 | | action | context | 403 | | -------- | --------------------------------------- | 404 | | insert | First release pre | 405 | | update | Any release field update | 406 | | delete | Erroneous release (should never happen) | 407 | | nuke | Release nuked by net | 408 | | unnuke | Release unnuked by net | 409 | | modnuke | Nuke reason modified by net | 410 | | delpre | Pre deleted by net | 411 | | undelpre | Pre undeleted by net | 412 | 413 | #### Example 414 | 415 | - [https://predb.ovh/api/v1/ws](https://predb.ovh/api/v1/ws) 416 | 417 | ``` 418 | { 419 | "action": "insert", 420 | "row": { 421 | "id": 7814727, 422 | "name": "Doom_Squad-Countdown_To_Doomsday_II-WEB-2016-ESG", 423 | "team": "ESG", 424 | "cat": "MP3", 425 | "genre": "", 426 | "url": "", 427 | "size": 0, 428 | "files": 0, 429 | "preAt": 1493884429, 430 | "nuke": null 431 | } 432 | } 433 | ``` 434 | --------------------------------------------------------------------------------