Add openapi API documentation (#294)

* Initial version

- Add OpenAPI documentation generator
- Update README.md to remove endpoint documentations

* Adds new endpoints

- Fixes build with swag
- Adds new endpoints

* Adds more endpoints documentation

- Also removes swag from Dockerfile and build script

* Finally adds all endpoints documentation

* Initial version

- Add OpenAPI documentation generator
- Update README.md to remove endpoint documentations

* Adds new endpoints

- Fixes build with swag
- Adds new endpoints

* Adds more endpoints documentation

- Also removes swag from Dockerfile and build script

* Finally adds all endpoints documentation

* Update README (#1)

* Update README

I completely redid the `README.md`. Now it's much easier to read and understand.

---------

Co-authored-by: cocool97 <34218602+cocool97@users.noreply.github.com>

* Improves documentation

* Delete server/config.db

* Update README.md

* Update README.md

* fix download in api docs

* add api docs to web

---------

Co-authored-by: Shadeov <144587546+shadeov@users.noreply.github.com>
Co-authored-by: nikk gitanes <tsynik@gmail.com>
This commit is contained in:
cocool97
2023-11-13 07:59:23 +01:00
committed by GitHub
parent aa061fd24c
commit b72c66d433
32 changed files with 5375 additions and 2024 deletions

View File

@@ -15,6 +15,18 @@ type cacheReqJS struct {
Hash string `json:"hash,omitempty"`
}
// cache godoc
//
// @Summary Return cache stats
// @Description Return cache stats.
//
// @Tags API
//
// @Param request body cacheReqJS true "Cache stats request"
//
// @Produce json
// @Success 200 {object} state.CacheState "Cache stats"
// @Router /cache [post]
func cache(c *gin.Context) {
var req cacheReqJS
err := c.ShouldBindJSON(&req)

View File

@@ -40,6 +40,18 @@ func (f *fileReader) Seek(offset int64, whence int) (int64, error) {
return f.pos, nil
}
// download godoc
//
// @Summary Generates test file of given size
// @Description Download the test file of given size (for speed testing purpose).
//
// @Tags API
//
// @Param size path string true "Test file size"
//
// @Produce application/octet-stream
// @Success 200 {file} file
// @Router /download/{size} [get]
func download(c *gin.Context) {
szStr := c.Param("size")
sz, err := strconv.Atoi(szStr)

View File

@@ -11,6 +11,19 @@ import (
"github.com/gin-gonic/gin"
)
// ffp godoc
//
// @Summary Gather informations using ffprobe
// @Description Gather informations using ffprobe.
//
// @Tags API
//
// @Param hash query string true "Torrent hash"
// @Param id query string true "File index in torrent"
//
// @Produce json
// @Success 200 "Data returned from ffprobe"
// @Router /ffp [get]
func ffp(c *gin.Context) {
hash := c.Param("hash")
indexStr := c.Param("id")

View File

@@ -22,6 +22,16 @@ import (
"github.com/pkg/errors"
)
// allPlayList godoc
//
// @Summary Get a M3U playlist with all torrents
// @Description Retrieve all torrents and generates a bundled M3U playlist.
//
// @Tags API
//
// @Produce audio/x-mpegurl
// @Success 200 {file} file
// @Router /playlistall/all.m3u [get]
func allPlayList(c *gin.Context) {
torrs := torr.ListTorrent()
@@ -38,9 +48,19 @@ func allPlayList(c *gin.Context) {
sendM3U(c, "all.m3u", hash, list)
}
// http://127.0.0.1:8090/playlist?hash=...
// http://127.0.0.1:8090/playlist?hash=...&fromlast
// http://127.0.0.1:8090/playlist/fname?hash=...
// playList godoc
//
// @Summary Get HTTP link of torrent in M3U list
// @Description Get HTTP link of torrent in M3U list.
//
// @Tags API
//
// @Param hash query string true "Torrent hash"
// @Param fromlast query bool false "From last play file"
//
// @Produce audio/x-mpegurl
// @Success 200 {file} file
// @Router /playlist [get]
func playList(c *gin.Context) {
hash, _ := c.GetQuery("hash")
_, fromlast := c.GetQuery("fromlast")

View File

@@ -12,6 +12,20 @@ import (
"server/web/api/utils"
)
// play godoc
//
// @Summary Play given torrent referenced by hash
// @Description Play given torrent referenced by hash.
//
// @Tags API
//
// @Param hash query string true "Torrent hash"
// @Param id query string true "File index in torrent"
// @Param not_auth query bool false "Not authenticated"
//
// @Produce application/octet-stream
// @Success 200 "Torrent data"
// @Router /play [get]
func play(c *gin.Context) {
hash := c.Param("hash")
indexStr := c.Param("id")
@@ -69,5 +83,4 @@ func play(c *gin.Context) {
}
tor.Stream(index, c.Request, c.Writer)
return
}

View File

@@ -1,12 +1,7 @@
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
sets "server/settings"
"server/torr"
)
type requestI struct {
@@ -36,7 +31,7 @@ func SetupRoute(route *gin.RouterGroup) {
route.GET("/playlistall/all.m3u", allPlayList)
route.GET("/playlist", playList)
route.GET("/playlist/*fname", playList)
route.GET("/playlist/*fname", playList) // Is this endpoint still needed ? `fname` is never used in handler
route.GET("/download/:size", download)
@@ -44,15 +39,3 @@ func SetupRoute(route *gin.RouterGroup) {
route.GET("/ffp/:hash/:id", ffp)
}
func shutdown(c *gin.Context) {
if sets.ReadOnly {
c.Status(http.StatusForbidden)
return
}
c.Status(200)
go func() {
time.Sleep(1000)
torr.Shutdown()
}()
}

View File

@@ -11,6 +11,18 @@ import (
sets "server/settings"
)
// rutorSearch godoc
//
// @Summary Makes a rutor search
// @Description Makes a rutor search.
//
// @Tags API
//
// @Param query query string true "Rutor query"
//
// @Produce json
// @Success 200 {array} models.TorrentDetails "Rutor torrent search result(s)"
// @Router /search [get]
func rutorSearch(c *gin.Context) {
if !sets.BTsets.EnableRutorSearch {
c.JSON(http.StatusBadRequest, []string{})

View File

@@ -5,9 +5,10 @@ import (
"server/rutor"
"server/dlna"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"server/dlna"
sets "server/settings"
"server/torr"
@@ -19,6 +20,19 @@ type setsReqJS struct {
Sets *sets.BTSets `json:"sets,omitempty"`
}
// settings godoc
//
// @Summary Get / Set server settings
// @Description Allow to get or set server settings.
//
// @Tags API
//
// @Param request body setsReqJS true "Settings request"
//
// @Accept json
// @Produce json
// @Success 200 {object} sets.BTSets "Depends on what action has been asked"
// @Router /settings [post]
func settings(c *gin.Context) {
var req setsReqJS
err := c.ShouldBindJSON(&req)

View File

@@ -0,0 +1,30 @@
package api
import (
"net/http"
sets "server/settings"
"server/torr"
"time"
"github.com/gin-gonic/gin"
)
// shutdown godoc
// @Summary Shuts down server
// @Description Gracefully shuts down server after 1 second.
//
// @Tags API
//
// @Success 200
// @Router /shutdown [get]
func shutdown(c *gin.Context) {
if sets.ReadOnly {
c.Status(http.StatusForbidden)
return
}
c.Status(200)
go func() {
time.Sleep(1000)
torr.Shutdown()
}()
}

View File

@@ -28,6 +28,28 @@ import (
// only save
// http://127.0.0.1:8090/stream/fname?link=...&save&title=...&poster=...
// stream godoc
//
// @Summary Multi usage endpoint
// @Description Multi usage endpoint.
//
// @Tags API
//
// @Param link query string true "Magnet/hash/link to torrent"
// @Param index query string false "File index in torrent"
// @Param preload query string false "Should preload torrent"
// @Param stat query string false "Get statistics from torrent"
// @Param save query string false "Should save torrent"
// @Param m3u query string false "Get torrent as M3U playlist"
// @Param fromlast query string false "Get m3u from last play"
// @Param play query string false "Start stream torrent"
// @Param title query string true "Set title of torrent"
// @Param poster query string true "File index in torrent"
// @Param not_auth query string true "Set poster link of torrent"
//
// @Produce application/octet-stream
// @Success 200 "Data returned according to query"
// @Router /stream [get]
func stream(c *gin.Context) {
link := c.Query("link")
indexStr := c.Query("index")

View File

@@ -26,6 +26,19 @@ type torrReqJS struct {
SaveToDB bool `json:"save_to_db,omitempty"`
}
// torrents godoc
//
// @Summary Handle torrents informations
// @Description Allow to add, get or set torrents to server. The action depends of what has been asked.
//
// @Tags API
//
// @Param request body torrReqJS true "Torrent request"
//
// @Accept json
// @Produce json
// @Success 200
// @Router /torrents [post]
func torrents(c *gin.Context) {
var req torrReqJS
err := c.ShouldBindJSON(&req)
@@ -59,7 +72,6 @@ func torrents(c *gin.Context) {
{
dropTorrent(req, c)
}
}
}

View File

@@ -3,12 +3,31 @@ package api
import (
"net/http"
"github.com/gin-gonic/gin"
"server/log"
"server/torr"
"server/web/api/utils"
"github.com/gin-gonic/gin"
)
// torrentUpload godoc
//
// @Summary Only one file support
// @Description Only one file support.
//
// @Tags API
//
// @Param file formData file true "Torrent file to insert"
// @Param save formData string false "Save to DB"
// @Param title formData string false "Torrent title"
// @Param poster formData string false "Torrent poster"
// @Param data formData string false "Torrent data"
//
// @Accept multipart/form-data
//
// @Produce json
// @Success 200 {object} state.TorrentStatus "Torrent status"
// @Router /torrent/upload [post]
func torrentUpload(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {

View File

@@ -3,8 +3,9 @@ package api
import (
"net/http"
"github.com/gin-gonic/gin"
sets "server/settings"
"github.com/gin-gonic/gin"
)
/*
@@ -17,6 +18,19 @@ type viewedReqJS struct {
*sets.Viewed
}
// viewed godoc
//
// @Summary Set / List / Remove viewed torrents
// @Description Allow to set, list or remove viewed torrents from server.
//
// @Tags API
//
// @Param request body viewedReqJS true "Viewed torrent request"
//
// @Accept json
// @Produce json
// @Success 200 {array} sets.Viewed
// @Router /viewed [post]
func viewed(c *gin.Context) {
var req viewedReqJS
err := c.ShouldBindJSON(&req)

View File

@@ -31,68 +31,108 @@ func asset(c *gin.Context, t string, d []byte) {
}
func SetupRoute(r *gin.RouterGroup) {
r.GET("/msx/:pth", func(c *gin.Context) {
js := []string{"http://msx.benzac.de/js/tvx-plugin.min.js"}
switch p := c.Param("pth"); p {
case "start.json":
c.JSON(200, map[string]string{
"name": "TorrServer",
"version": version.Version,
"parameter": "menu:request:interaction:init@{PREFIX}{SERVER}/msx/ts",
})
case "russian.json":
asset(c, "application/json", rus)
case "torrents.js":
asset(c, "text/javascript", trs)
case "torrent.js":
asset(c, "text/javascript", trn)
case "ts.js":
asset(c, "text/javascript", its)
case "torrents":
js = append(js, p+".js")
p = "torrent"
fallthrough
case "torrent":
if c.Query("platform") == "tizen" {
js = append(js, "http://msx.benzac.de/interaction/js/tizen-player.js")
}
fallthrough
case "ts":
b := []byte("<!DOCTYPE html>\n<html>\n<head>\n<title>TorrServer Plugin</title>\n<meta charset='UTF-8'>\n")
for _, j := range append(js, p+".js") {
b = append(b, "<script type='text/javascript' src='"+j+"'></script>\n"...)
}
c.Data(200, "text/html; charset=UTF-8", append(b, "</head>\n<body></body>\n</html>"...))
default:
c.AbortWithStatus(404)
r.GET("/msx/:pth", msxPTH)
r.GET("/msx/imdb", msxIMDB)
r.GET("/msx/imdb/:id", msxIMDBID)
}
// msxPTH godoc
//
// @Summary Multi usage endpoint
// @Description Multi usage endpoint.
//
// @Tags MSX
//
// @Param link query string true "Magnet/hash/link to torrent"
//
// @Produce json
// @Success 200 "Data returned according to query"
// @Router /msx [get]
func msxPTH(c *gin.Context) {
js := []string{"http://msx.benzac.de/js/tvx-plugin.min.js"}
switch p := c.Param("pth"); p {
case "start.json":
c.JSON(200, map[string]string{
"name": "TorrServer",
"version": version.Version,
"parameter": "menu:request:interaction:init@{PREFIX}{SERVER}/msx/ts",
})
case "russian.json":
asset(c, "application/json", rus)
case "torrents.js":
asset(c, "text/javascript", trs)
case "torrent.js":
asset(c, "text/javascript", trn)
case "ts.js":
asset(c, "text/javascript", its)
case "torrents":
js = append(js, p+".js")
p = "torrent"
fallthrough
case "torrent":
if c.Query("platform") == "tizen" {
js = append(js, "http://msx.benzac.de/interaction/js/tizen-player.js")
}
})
r.GET("/msx/imdb", func(c *gin.Context) {
idb.Lock()
defer idb.Unlock()
l := len(ids)
ids = make(map[string]string)
c.JSON(200, l)
})
r.GET("/msx/imdb/:id", func(c *gin.Context) {
idb.Lock()
defer idb.Unlock()
p := c.Param("id")
i, o := ids[p]
if !o {
if r, e := http.Get("https://v2.sg.media-imdb.com/suggestion/h/" + p + ".json"); e == nil {
defer r.Body.Close()
if r.StatusCode == 200 {
var j struct {
D []struct{ I struct{ ImageUrl string } }
}
if e = json.NewDecoder(r.Body).Decode(&j); e == nil && len(j.D) > 0 {
i = j.D[0].I.ImageUrl
}
fallthrough
case "ts":
b := []byte("<!DOCTYPE html>\n<html>\n<head>\n<title>TorrServer Plugin</title>\n<meta charset='UTF-8'>\n")
for _, j := range append(js, p+".js") {
b = append(b, "<script type='text/javascript' src='"+j+"'></script>\n"...)
}
c.Data(200, "text/html; charset=UTF-8", append(b, "</head>\n<body></body>\n</html>"...))
default:
c.AbortWithStatus(404)
}
}
// msxIMDB godoc
//
// @Summary Get MSX IMDB informations
// @Description Get MSX IMDB informations.
//
// @Tags MSX
//
// @Produce json
// @Success 200 "JSON MSX IMDB informations"
// @Router /msx/imdb [get]
func msxIMDB(c *gin.Context) {
idb.Lock()
defer idb.Unlock()
l := len(ids)
ids = make(map[string]string)
c.JSON(200, l)
}
// msxIMDB godoc
//
// @Summary Get MSX IMDB informations
// @Description Get MSX IMDB informations.
//
// @Tags MSX
//
// @Param id path string true "IMDB ID"
//
// @Produce json
// @Success 200 "JSON MSX IMDB informations"
// @Router /msx/imdb/:id [get]
func msxIMDBID(c *gin.Context) {
idb.Lock()
defer idb.Unlock()
p := c.Param("id")
i, o := ids[p]
if !o {
if r, e := http.Get("https://v2.sg.media-imdb.com/suggestion/h/" + p + ".json"); e == nil {
defer r.Body.Close()
if r.StatusCode == 200 {
var j struct {
D []struct{ I struct{ ImageUrl string } }
}
if e = json.NewDecoder(r.Body).Decode(&j); e == nil && len(j.D) > 0 {
i = j.D[0].I.ImageUrl
}
}
ids[p] = i
}
c.JSON(200, i)
})
ids[p] = i
}
c.JSON(200, i)
}

View File

@@ -15,11 +15,31 @@ func SetupRoute(route *gin.RouterGroup) {
route.GET("/magnets", getTorrents)
}
// stat godoc
//
// @Summary Stat server
// @Description Stat server.
//
// @Tags Pages
//
// @Produce text/plain
// @Success 200 "Stats"
// @Router /stat [get]
func statPage(c *gin.Context) {
torr.WriteStatus(c.Writer)
c.Status(200)
}
// getTorrents godoc
//
// @Summary Get HTML of magnet links
// @Description Get HTML of magnet links.
//
// @Tags Pages
//
// @Produce text/html
// @Success 200 "Magnet links"
// @Router /magnets [get]
func getTorrents(c *gin.Context) {
list := settings.ListTorrent()
http := "<div>"

View File

@@ -23,6 +23,9 @@ import (
"server/web/blocker"
"server/web/pages"
"server/web/sslcerts"
swaggerFiles "github.com/swaggo/files" // swagger embed files
ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware
)
var (
@@ -30,6 +33,18 @@ var (
waitChan = make(chan error)
)
// @title Swagger Torrserver API
// @version {version.Version}
// @description Torrent streaming server.
// @license.name GPL 3.0
// @BasePath /
// @securityDefinitions.basic BasicAuth
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func Start() {
log.TLogln("Start TorrServer " + version.Version + " torrent " + version.GetTorrentVersion())
ips := getLocalIps()
@@ -72,6 +87,8 @@ func Start() {
if settings.BTsets.EnableDLNA {
dlna.Start()
}
route.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
//check if https enabled
if settings.Ssl {
@@ -113,6 +130,16 @@ func Stop() {
waitChan <- nil
}
// echo godoc
//
// @Summary Tests server status
// @Description Tests whether server is alive or not
//
// @Tags API
//
// @Produce plain
// @Success 200 {string} string "Server version"
// @Router /echo [get]
func echo(c *gin.Context) {
c.String(200, "%v", version.Version)
}