Initial commit: docker compose config
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"server/torr"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Action: get
|
||||
type cacheReqJS struct {
|
||||
requestI
|
||||
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)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusBadRequest)
|
||||
switch req.Action {
|
||||
case "get":
|
||||
{
|
||||
getCache(req, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getCache(req cacheReqJS, c *gin.Context) {
|
||||
if req.Hash == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
|
||||
return
|
||||
}
|
||||
tor := torr.GetTorrent(req.Hash)
|
||||
|
||||
if tor != nil {
|
||||
st := tor.CacheState()
|
||||
if st == nil {
|
||||
c.JSON(200, struct{}{})
|
||||
} else {
|
||||
c.JSON(200, st)
|
||||
}
|
||||
} else {
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type fileReader struct {
|
||||
pos int64
|
||||
size int64
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
func newFR(size int64) *fileReader {
|
||||
return &fileReader{
|
||||
pos: 0,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fileReader) Read(p []byte) (n int, err error) {
|
||||
f.pos = f.pos + int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (f *fileReader) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case 0:
|
||||
f.pos = offset
|
||||
case 1:
|
||||
f.pos += offset
|
||||
case 2:
|
||||
f.pos = f.size + offset
|
||||
}
|
||||
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 (in MB)"
|
||||
//
|
||||
// @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)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, fmt.Sprintln(szStr)+"mb.bin", time.Now(), newFR(int64(sz*1024*1024)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"server/ffprobe"
|
||||
sets "server/settings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ffp godoc
|
||||
//
|
||||
// @Summary Gather informations using ffprobe
|
||||
// @Description Gather informations using ffprobe.
|
||||
//
|
||||
// @Tags API
|
||||
//
|
||||
// @Param hash path string true "Torrent hash"
|
||||
// @Param id path string true "File index in torrent"
|
||||
//
|
||||
// @Produce json
|
||||
// @Success 200 "Data returned from ffprobe"
|
||||
// @Router /ffp/{hash}/{id} [get]
|
||||
func ffp(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
indexStr := c.Param("id")
|
||||
|
||||
if hash == "" || indexStr == "" {
|
||||
c.AbortWithError(http.StatusNotFound, errors.New("link should not be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
link := "http://127.0.0.1:" + sets.Port + "/play/" + hash + "/" + indexStr
|
||||
if sets.Ssl {
|
||||
link = "https://127.0.0.1:" + sets.SslPort + "/play/" + hash + "/" + indexStr
|
||||
}
|
||||
|
||||
data, err := ffprobe.ProbeUrl(link)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("error getting data: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, data)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/missinggo/v2/httptoo"
|
||||
|
||||
sets "server/settings"
|
||||
"server/torr"
|
||||
"server/torr/state"
|
||||
"server/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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()
|
||||
|
||||
host := utils.GetScheme(c) + "://" + utils.GetHost(c)
|
||||
list := "#EXTM3U\n"
|
||||
hash := ""
|
||||
// fn=file.m3u fix forkplayer bug with end .m3u in link
|
||||
for _, tr := range torrs {
|
||||
list += "#EXTINF:0"
|
||||
if tr.Poster != "" {
|
||||
list += " tvg-logo=\"" + tr.Poster + "\""
|
||||
}
|
||||
list += " type=\"playlist\"," + tr.Title + "\n"
|
||||
list += host + "/stream/" + url.PathEscape(tr.Title) + ".m3u?link=" + tr.TorrentSpec.InfoHash.HexString() + "&m3u&fn=file.m3u\n"
|
||||
hash += tr.Hash().HexString()
|
||||
}
|
||||
|
||||
sendM3U(c, "all.m3u", hash, list)
|
||||
}
|
||||
|
||||
// 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")
|
||||
if hash == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
tor := torr.GetTorrent(hash)
|
||||
if tor == nil {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if tor.Stat == state.TorrentInDB {
|
||||
tor = torr.LoadTorrent(tor)
|
||||
if tor == nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent info"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
host := utils.GetScheme(c) + "://" + utils.GetHost(c)
|
||||
list := getM3uList(tor.Status(), host, fromlast)
|
||||
list = "#EXTM3U\n" + list
|
||||
name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param
|
||||
if name == "" {
|
||||
name = tor.Name() + ".m3u"
|
||||
} else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") {
|
||||
name += ".m3u"
|
||||
}
|
||||
|
||||
sendM3U(c, name, tor.Hash().HexString(), list)
|
||||
}
|
||||
|
||||
func sendM3U(c *gin.Context, name, hash string, m3u string) {
|
||||
c.Header("Content-Type", "audio/x-mpegurl")
|
||||
c.Header("Connection", "close")
|
||||
if hash != "" {
|
||||
etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", hash, name)))
|
||||
c.Header("ETag", httptoo.EncodeQuotedString(etag))
|
||||
}
|
||||
if name == "" {
|
||||
name = "playlist.m3u"
|
||||
}
|
||||
c.Header("Content-Disposition", `attachment; filename="`+name+`"`)
|
||||
http.ServeContent(c.Writer, c.Request, name, time.Now(), bytes.NewReader([]byte(m3u)))
|
||||
}
|
||||
|
||||
func getM3uList(tor *state.TorrentStatus, host string, fromLast bool) string {
|
||||
m3u := ""
|
||||
from := 0
|
||||
if fromLast {
|
||||
pos := searchLastPlayed(tor)
|
||||
if pos != -1 {
|
||||
from = pos
|
||||
}
|
||||
}
|
||||
for i, f := range tor.FileStats {
|
||||
if i >= from {
|
||||
if utils.GetMimeType(f.Path) != "*/*" {
|
||||
fn := filepath.Base(f.Path)
|
||||
if fn == "" {
|
||||
fn = f.Path
|
||||
}
|
||||
m3u += "#EXTINF:0," + fn + "\n"
|
||||
fileNamesakes := findFileNamesakes(tor.FileStats, f) // find external media with same name (audio/subtiles tracks)
|
||||
if fileNamesakes != nil {
|
||||
m3u += "#EXTVLCOPT:input-slave=" // include VLC option for external media
|
||||
for _, namesake := range fileNamesakes { // include play-links to external media, with # splitter
|
||||
sname := filepath.Base(namesake.Path)
|
||||
m3u += host + "/stream/" + url.PathEscape(sname) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(namesake.Id) + "&play#"
|
||||
}
|
||||
m3u += "\n"
|
||||
}
|
||||
name := filepath.Base(f.Path)
|
||||
m3u += host + "/stream/" + url.PathEscape(name) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(f.Id) + "&play\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return m3u
|
||||
}
|
||||
|
||||
func findFileNamesakes(files []*state.TorrentFileStat, file *state.TorrentFileStat) []*state.TorrentFileStat {
|
||||
// find files with the same name in torrent
|
||||
name := filepath.Base(strings.TrimSuffix(file.Path, filepath.Ext(file.Path)))
|
||||
var namesakes []*state.TorrentFileStat
|
||||
for _, f := range files {
|
||||
if strings.Contains(f.Path, name) { // external tracks always include name of videofile
|
||||
if f != file { // exclude itself
|
||||
namesakes = append(namesakes, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
return namesakes
|
||||
}
|
||||
|
||||
func searchLastPlayed(tor *state.TorrentStatus) int {
|
||||
viewed := sets.ListViewed(tor.Hash)
|
||||
if len(viewed) == 0 {
|
||||
return -1
|
||||
}
|
||||
sort.Slice(viewed, func(i, j int) bool {
|
||||
return viewed[i].FileIndex > viewed[j].FileIndex
|
||||
})
|
||||
|
||||
lastViewedIndex := viewed[0].FileIndex
|
||||
|
||||
for i, stat := range tor.FileStats {
|
||||
if stat.Id == lastViewedIndex {
|
||||
if i >= len(tor.FileStats) {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"server/torr"
|
||||
"server/torr/state"
|
||||
"server/web/api/utils"
|
||||
)
|
||||
|
||||
// play godoc
|
||||
//
|
||||
// @Summary Play given torrent by infohash
|
||||
// @Description Play given torrent referenced by infohash and file id.
|
||||
//
|
||||
// @Tags API
|
||||
//
|
||||
// @Param hash path string true "Torrent infohash"
|
||||
// @Param id path string true "File index in torrent"
|
||||
//
|
||||
// @Produce application/octet-stream
|
||||
// @Success 200 "Torrent data"
|
||||
// @Router /play/{hash}/{id} [get]
|
||||
func play(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
indexStr := c.Param("id")
|
||||
notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == ""
|
||||
|
||||
if hash == "" || indexStr == "" {
|
||||
c.AbortWithError(http.StatusNotFound, errors.New("no infohash or file index in link"))
|
||||
return
|
||||
}
|
||||
|
||||
spec, err := utils.ParseLink(hash)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
tor := torr.GetTorrent(spec.InfoHash.HexString())
|
||||
if tor == nil && notAuth {
|
||||
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if tor == nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent"))
|
||||
return
|
||||
}
|
||||
|
||||
if tor.Stat == state.TorrentInDB {
|
||||
tor, err = torr.AddTorrent(spec, tor.Title, tor.Poster, tor.Data, tor.Category)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !tor.GotInfo() {
|
||||
c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout"))
|
||||
return
|
||||
}
|
||||
|
||||
// find file
|
||||
index := -1
|
||||
if len(tor.Files()) == 1 {
|
||||
index = 1
|
||||
} else {
|
||||
ind, err := strconv.Atoi(indexStr)
|
||||
if err == nil {
|
||||
index = ind
|
||||
}
|
||||
}
|
||||
if index == -1 { // if file index not set and play file exec
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("file \"index\" is wrong"))
|
||||
return
|
||||
}
|
||||
|
||||
tor.Stream(index, c.Request, c.Writer)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
config "server/settings"
|
||||
"server/web/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type requestI struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
}
|
||||
|
||||
func SetupRoute(route gin.IRouter) {
|
||||
authorized := route.Group("/", auth.CheckAuth())
|
||||
|
||||
authorized.GET("/shutdown", shutdown)
|
||||
authorized.GET("/shutdown/*reason", shutdown)
|
||||
|
||||
authorized.POST("/settings", settings)
|
||||
authorized.POST("/torznab/test", torznabTest)
|
||||
|
||||
authorized.POST("/torrents", torrents)
|
||||
|
||||
authorized.POST("/torrent/upload", torrentUpload)
|
||||
|
||||
authorized.POST("/cache", cache)
|
||||
|
||||
route.HEAD("/stream", stream)
|
||||
route.GET("/stream", stream)
|
||||
|
||||
route.HEAD("/stream/*fname", stream)
|
||||
route.GET("/stream/*fname", stream)
|
||||
|
||||
route.HEAD("/play/:hash/:id", play)
|
||||
route.GET("/play/:hash/:id", play)
|
||||
|
||||
authorized.POST("/viewed", viewed)
|
||||
|
||||
authorized.GET("/playlistall/all.m3u", allPlayList)
|
||||
|
||||
route.GET("/playlist", playList)
|
||||
route.GET("/playlist/*fname", playList)
|
||||
|
||||
authorized.GET("/download/:size", download)
|
||||
|
||||
if config.SearchWA {
|
||||
route.GET("/search/*query", rutorSearch)
|
||||
} else {
|
||||
authorized.GET("/search/*query", rutorSearch)
|
||||
}
|
||||
|
||||
if config.SearchWA {
|
||||
route.GET("/torznab/search/*query", torznabSearch)
|
||||
} else {
|
||||
authorized.GET("/torznab/search/*query", torznabSearch)
|
||||
}
|
||||
|
||||
// Add storage settings endpoints
|
||||
authorized.GET("/storage/settings", GetStorageSettings)
|
||||
authorized.POST("/storage/settings", UpdateStorageSettings)
|
||||
|
||||
// Add TMDB settings endpoint
|
||||
authorized.GET("/tmdb/settings", tmdbSettings)
|
||||
|
||||
authorized.GET("/ffp/:hash/:id", ffp)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"server/rutor"
|
||||
"server/rutor/models"
|
||||
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{})
|
||||
return
|
||||
}
|
||||
query := c.Query("query")
|
||||
query, _ = url.QueryUnescape(query)
|
||||
list := rutor.Search(query)
|
||||
if list == nil {
|
||||
list = []*models.TorrentDetails{}
|
||||
}
|
||||
c.JSON(200, list)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"server/rutor"
|
||||
|
||||
"server/dlna"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
sets "server/settings"
|
||||
"server/torr"
|
||||
)
|
||||
|
||||
// Action: get, set, def
|
||||
type setsReqJS struct {
|
||||
requestI
|
||||
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. Available params for action: get, set, def"
|
||||
//
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} sets.BTSets "Settings JSON or nothing. Depends on what action has been asked."
|
||||
// @Router /settings [post]
|
||||
func settings(c *gin.Context) {
|
||||
var req setsReqJS
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Action == "get" {
|
||||
c.JSON(200, sets.BTsets)
|
||||
return
|
||||
} else if req.Action == "set" {
|
||||
torr.SetSettings(req.Sets)
|
||||
dlna.Stop()
|
||||
if req.Sets.EnableDLNA {
|
||||
dlna.Start()
|
||||
}
|
||||
rutor.Stop()
|
||||
rutor.Start()
|
||||
c.Status(200)
|
||||
return
|
||||
} else if req.Action == "def" {
|
||||
torr.SetDefSettings()
|
||||
dlna.Stop()
|
||||
rutor.Stop()
|
||||
c.Status(200)
|
||||
return
|
||||
}
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("action is empty"))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sets "server/settings"
|
||||
"server/torr"
|
||||
|
||||
"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) {
|
||||
reasonStr := strings.ReplaceAll(c.Param("reason"), `/`, "")
|
||||
if sets.ReadOnly && reasonStr == "" {
|
||||
c.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Status(200)
|
||||
go func() {
|
||||
time.Sleep(1000)
|
||||
torr.Shutdown()
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
sets "server/settings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetStorageSettings godoc
|
||||
// @Summary Get storage configuration settings
|
||||
// @Description Retrieves the current storage preferences for settings and viewed history
|
||||
// @Tags API
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} map[string]interface{} "Storage preferences"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /storage/settings [get]
|
||||
func GetStorageSettings(c *gin.Context) {
|
||||
prefs := sets.GetStoragePreferences()
|
||||
c.JSON(http.StatusOK, prefs)
|
||||
}
|
||||
|
||||
// UpdateStorageSettings godoc
|
||||
// @Summary Update storage configuration settings
|
||||
// @Description Updates the storage preferences for settings and viewed history. Requires application restart for changes to take effect.
|
||||
// @Tags API
|
||||
// @Accept json,x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param request body map[string]interface{} true "Storage preferences to update"
|
||||
// @Param settings formData string false "Settings storage type" Enums(json,bbolt)
|
||||
// @Param viewed formData string false "Viewed history storage type" Enums(json,bbolt)
|
||||
// @Success 200 {object} map[string]string "Update successful"
|
||||
// @Failure 400 {object} map[string]string "Invalid input data"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Failure 403 {object} map[string]string "Read-only mode"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /storage/settings [post]
|
||||
func UpdateStorageSettings(c *gin.Context) {
|
||||
if sets.ReadOnly {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Read-only mode"})
|
||||
return
|
||||
}
|
||||
|
||||
var prefs map[string]interface{}
|
||||
|
||||
// Check Content-Type to handle both JSON and form data
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
|
||||
if contentType == "application/x-www-form-urlencoded" {
|
||||
// Handle form data
|
||||
settings := c.PostForm("settings")
|
||||
viewed := c.PostForm("viewed")
|
||||
|
||||
prefs = make(map[string]interface{})
|
||||
if settings != "" {
|
||||
prefs["settings"] = settings
|
||||
}
|
||||
if viewed != "" {
|
||||
prefs["viewed"] = viewed
|
||||
}
|
||||
} else {
|
||||
// Handle JSON (default)
|
||||
if err := c.ShouldBindJSON(&prefs); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate preferences - only validate if provided
|
||||
if settingsPref, ok := prefs["settings"].(string); ok && settingsPref != "" {
|
||||
if settingsPref != "json" && settingsPref != "bbolt" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid settings storage value"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if viewedPref, ok := prefs["viewed"].(string); ok && viewedPref != "" {
|
||||
if viewedPref != "json" && viewedPref != "bbolt" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid viewed storage value"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have at least one value to update
|
||||
if len(prefs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No preferences provided"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := sets.SetStoragePreferences(prefs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"server/log"
|
||||
"server/torrshash"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/torr"
|
||||
"server/torr/state"
|
||||
utils2 "server/utils"
|
||||
"server/web/api/utils"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// get stat
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&stat
|
||||
// get m3u
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u&fromlast
|
||||
// stream torrent
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&preload
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save
|
||||
// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save&title=...&poster=...
|
||||
// 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 played file"
|
||||
// @Param play query string false "Start stream torrent"
|
||||
// @Param title query string false "Set title of torrent"
|
||||
// @Param poster query string false "Set poster link of torrent"
|
||||
// @Param category query string false "Set category of torrent, used in web: movie, tv, music, other"
|
||||
//
|
||||
// @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")
|
||||
_, preload := c.GetQuery("preload")
|
||||
_, stat := c.GetQuery("stat")
|
||||
_, save := c.GetQuery("save")
|
||||
_, m3u := c.GetQuery("m3u")
|
||||
_, fromlast := c.GetQuery("fromlast")
|
||||
_, play := c.GetQuery("play")
|
||||
title := c.Query("title")
|
||||
poster := c.Query("poster")
|
||||
category := c.Query("category")
|
||||
|
||||
data := ""
|
||||
|
||||
notAuth := c.GetBool("auth_required") && c.GetString(gin.AuthUserKey) == ""
|
||||
|
||||
if notAuth {
|
||||
err := utils.TestLink(link, !notAuth)
|
||||
if err != nil {
|
||||
log.TLogln("Wrong link:", err)
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("wrong link"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if notAuth && (play || m3u) {
|
||||
streamNoAuth(c)
|
||||
return
|
||||
}
|
||||
if notAuth {
|
||||
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if link == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
link, _ = url.QueryUnescape(link)
|
||||
title, _ = url.QueryUnescape(title)
|
||||
poster, _ = url.QueryUnescape(poster)
|
||||
category, _ = url.QueryUnescape(category)
|
||||
|
||||
var spec *torrent.TorrentSpec
|
||||
var torrsHash *torrshash.TorrsHash
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(link, "torrs://") || (len(link) > 45 && torrshash.IsBase62(link)) {
|
||||
spec, torrsHash, err = utils.ParseTorrsHash(link)
|
||||
if err != nil {
|
||||
log.TLogln("error parse torrshash:", err)
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if title == "" {
|
||||
title = torrsHash.Title()
|
||||
}
|
||||
if poster == "" {
|
||||
poster = torrsHash.Poster()
|
||||
}
|
||||
if category == "" {
|
||||
category = torrsHash.Category()
|
||||
}
|
||||
} else {
|
||||
spec, err = utils.ParseLink(link)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tor := torr.GetTorrent(spec.InfoHash.HexString())
|
||||
if tor != nil {
|
||||
title = tor.Title
|
||||
poster = tor.Poster
|
||||
data = tor.Data
|
||||
category = tor.Category
|
||||
}
|
||||
if tor == nil || tor.Stat == state.TorrentInDB {
|
||||
tor, err = torr.AddTorrent(spec, title, poster, data, category)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !tor.GotInfo() {
|
||||
c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout"))
|
||||
return
|
||||
}
|
||||
|
||||
if tor.Title == "" {
|
||||
tor.Title = tor.Name()
|
||||
}
|
||||
|
||||
// save to db
|
||||
if save {
|
||||
torr.SaveTorrentToDB(tor)
|
||||
c.Status(200) // only set status, not return
|
||||
}
|
||||
|
||||
// find file
|
||||
index := -1
|
||||
if len(tor.Files()) == 1 {
|
||||
index = 1
|
||||
} else {
|
||||
ind, err := strconv.Atoi(indexStr)
|
||||
if err == nil {
|
||||
index = ind
|
||||
}
|
||||
}
|
||||
if index == -1 && play { // if file index not set and play file exec
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is empty or wrong"))
|
||||
return
|
||||
}
|
||||
// preload torrent
|
||||
if preload {
|
||||
torr.Preload(tor, index)
|
||||
}
|
||||
// return stat if query
|
||||
if stat {
|
||||
c.JSON(200, tor.Status())
|
||||
return
|
||||
} else
|
||||
// return m3u if query
|
||||
if m3u {
|
||||
name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param
|
||||
if name == "" {
|
||||
name = tor.Name() + ".m3u"
|
||||
} else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") {
|
||||
name += ".m3u"
|
||||
}
|
||||
m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+utils2.GetHost(c), fromlast)
|
||||
sendM3U(c, name, tor.Hash().HexString(), m3ulist)
|
||||
return
|
||||
} else
|
||||
// return play if query
|
||||
if play {
|
||||
tor.Stream(index, c.Request, c.Writer)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func streamNoAuth(c *gin.Context) {
|
||||
link := c.Query("link")
|
||||
indexStr := c.Query("index")
|
||||
_, preload := c.GetQuery("preload")
|
||||
_, m3u := c.GetQuery("m3u")
|
||||
_, fromlast := c.GetQuery("fromlast")
|
||||
_, play := c.GetQuery("play")
|
||||
title := c.Query("title")
|
||||
poster := c.Query("poster")
|
||||
category := c.Query("category")
|
||||
|
||||
if link == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
link, _ = url.QueryUnescape(link)
|
||||
title, _ = url.QueryUnescape(title)
|
||||
poster, _ = url.QueryUnescape(poster)
|
||||
category, _ = url.QueryUnescape(category)
|
||||
|
||||
var spec *torrent.TorrentSpec
|
||||
var torrsHash *torrshash.TorrsHash
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(link, "torrs://") || (len(link) > 45 && torrshash.IsBase62(link)) {
|
||||
spec, torrsHash, err = utils.ParseTorrsHash(link)
|
||||
if err != nil {
|
||||
log.TLogln("error parse torrshash:", err)
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if title == "" {
|
||||
title = torrsHash.Title()
|
||||
}
|
||||
if poster == "" {
|
||||
poster = torrsHash.Poster()
|
||||
}
|
||||
if category == "" {
|
||||
category = torrsHash.Category()
|
||||
}
|
||||
} else {
|
||||
spec, err = utils.ParseLink(link)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tor := torr.GetTorrent(spec.InfoHash.HexString())
|
||||
if tor == nil {
|
||||
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = tor.Title
|
||||
}
|
||||
|
||||
if poster == "" {
|
||||
poster = tor.Poster
|
||||
}
|
||||
|
||||
if category == "" {
|
||||
category = tor.Category
|
||||
}
|
||||
|
||||
data := tor.Data
|
||||
|
||||
if tor.Stat == state.TorrentInDB {
|
||||
tor, err = torr.AddTorrent(spec, title, poster, data, category)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !tor.GotInfo() {
|
||||
c.AbortWithError(http.StatusInternalServerError, errors.New("torrent connection timeout"))
|
||||
return
|
||||
}
|
||||
|
||||
// find file
|
||||
index := -1
|
||||
if len(tor.Files()) == 1 {
|
||||
index = 1
|
||||
} else {
|
||||
ind, err := strconv.Atoi(indexStr)
|
||||
if err == nil {
|
||||
index = ind
|
||||
}
|
||||
}
|
||||
if index == -1 && play { // if file index not set and play file exec
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is empty or wrong"))
|
||||
return
|
||||
}
|
||||
// preload torrent
|
||||
if preload {
|
||||
torr.Preload(tor, index)
|
||||
}
|
||||
// return m3u if query
|
||||
if m3u {
|
||||
name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param
|
||||
if name == "" {
|
||||
name = tor.Name() + ".m3u"
|
||||
} else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") {
|
||||
name += ".m3u"
|
||||
}
|
||||
m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+utils2.GetHost(c), fromlast)
|
||||
sendM3U(c, name, tor.Hash().HexString(), m3ulist)
|
||||
return
|
||||
} else
|
||||
// return play if query
|
||||
if play {
|
||||
tor.Stream(index, c.Request, c.Writer)
|
||||
return
|
||||
}
|
||||
c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
sets "server/settings"
|
||||
)
|
||||
|
||||
// tmdbSettings godoc
|
||||
//
|
||||
// @Summary Get TMDB settings
|
||||
// @Description Get TMDB API configuration
|
||||
//
|
||||
// @Tags API
|
||||
//
|
||||
// @Produce json
|
||||
// @Success 200 {object} sets.TMDBConfig "TMDB settings"
|
||||
// @Router /tmdb/settings [get]
|
||||
func tmdbSettings(c *gin.Context) {
|
||||
if sets.BTsets == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Settings not initialized"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, sets.BTsets.TMDBSettings)
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"server/torrshash"
|
||||
"strings"
|
||||
|
||||
"server/dlna"
|
||||
"server/log"
|
||||
set "server/settings"
|
||||
"server/torr"
|
||||
"server/torr/state"
|
||||
"server/web/api/utils"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Action: add, get, set, rem, list, drop
|
||||
type torrReqJS struct {
|
||||
requestI
|
||||
Link string `json:"link,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Poster string `json:"poster,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
SaveToDB bool `json:"save_to_db,omitempty"`
|
||||
}
|
||||
|
||||
// torrents godoc
|
||||
//
|
||||
// @Summary Handle torrents informations
|
||||
// @Description Allow to list, add, remove, get, set, drop, wipe torrents on server. The action depends of what has been asked.
|
||||
//
|
||||
// @Tags API
|
||||
//
|
||||
// @Param request body torrReqJS true "Torrent request. Available params for action: add, get, set, rem, list, drop, wipe. link required for add, hash required for get, set, rem, drop."
|
||||
//
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Router /torrents [post]
|
||||
func torrents(c *gin.Context) {
|
||||
var req torrReqJS
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusBadRequest)
|
||||
switch req.Action {
|
||||
case "add":
|
||||
{
|
||||
addTorrent(req, c)
|
||||
}
|
||||
case "get":
|
||||
{
|
||||
getTorrent(req, c)
|
||||
}
|
||||
case "set":
|
||||
{
|
||||
setTorrent(req, c)
|
||||
}
|
||||
case "rem":
|
||||
{
|
||||
remTorrent(req, c)
|
||||
}
|
||||
case "list":
|
||||
{
|
||||
listTorrents(c)
|
||||
}
|
||||
case "drop":
|
||||
{
|
||||
dropTorrent(req, c)
|
||||
}
|
||||
case "wipe":
|
||||
{
|
||||
wipeTorrents(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addTorrent(req torrReqJS, c *gin.Context) {
|
||||
if req.Link == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("link is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
log.TLogln("add torrent", req.Link)
|
||||
req.Link = strings.ReplaceAll(req.Link, "&", "&")
|
||||
|
||||
var torrSpec *torrent.TorrentSpec
|
||||
var torrsHash *torrshash.TorrsHash
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(req.Link, "torrs://") {
|
||||
torrSpec, torrsHash, err = utils.ParseTorrsHash(req.Link)
|
||||
if err != nil {
|
||||
log.TLogln("error parse torrshash:", err)
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if req.Title == "" {
|
||||
req.Title = torrsHash.Title()
|
||||
}
|
||||
if req.Poster == "" {
|
||||
req.Poster = torrsHash.Poster()
|
||||
}
|
||||
if req.Category == "" {
|
||||
req.Category = torrsHash.Category()
|
||||
}
|
||||
} else {
|
||||
torrSpec, err = utils.ParseLink(req.Link)
|
||||
if err != nil {
|
||||
log.TLogln("error parse link:", err)
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tor, err := torr.AddTorrent(torrSpec, req.Title, req.Poster, req.Data, req.Category)
|
||||
if err != nil {
|
||||
log.TLogln("error add torrent:", err)
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if !tor.GotInfo() {
|
||||
log.TLogln("error add torrent:", "timeout connection get torrent info")
|
||||
return
|
||||
}
|
||||
|
||||
if tor.Title == "" {
|
||||
tor.Title = torrSpec.DisplayName // prefer dn over name
|
||||
tor.Title = strings.ReplaceAll(tor.Title, "rutor.info", "")
|
||||
tor.Title = strings.ReplaceAll(tor.Title, "_", " ")
|
||||
tor.Title = strings.Trim(tor.Title, " ")
|
||||
if tor.Title == "" {
|
||||
tor.Title = tor.Name()
|
||||
}
|
||||
}
|
||||
|
||||
if req.SaveToDB {
|
||||
torr.SaveTorrentToDB(tor)
|
||||
}
|
||||
}()
|
||||
|
||||
if set.BTsets.EnableDLNA {
|
||||
dlna.Stop()
|
||||
dlna.Start()
|
||||
}
|
||||
c.JSON(200, tor.Status())
|
||||
}
|
||||
|
||||
func getTorrent(req torrReqJS, c *gin.Context) {
|
||||
if req.Hash == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
|
||||
return
|
||||
}
|
||||
tor := torr.GetTorrent(req.Hash)
|
||||
|
||||
if tor != nil {
|
||||
st := tor.Status()
|
||||
c.JSON(200, st)
|
||||
} else {
|
||||
c.Status(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func setTorrent(req torrReqJS, c *gin.Context) {
|
||||
if req.Hash == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
|
||||
return
|
||||
}
|
||||
torr.SetTorrent(req.Hash, req.Title, req.Poster, req.Category, req.Data)
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
func remTorrent(req torrReqJS, c *gin.Context) {
|
||||
if req.Hash == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
|
||||
return
|
||||
}
|
||||
torr.RemTorrent(req.Hash)
|
||||
// TODO: remove
|
||||
if set.BTsets.EnableDLNA {
|
||||
dlna.Stop()
|
||||
dlna.Start()
|
||||
}
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
func listTorrents(c *gin.Context) {
|
||||
list := torr.ListTorrent()
|
||||
if len(list) == 0 {
|
||||
c.JSON(200, []*state.TorrentStatus{})
|
||||
return
|
||||
}
|
||||
var stats []*state.TorrentStatus
|
||||
for _, tr := range list {
|
||||
stats = append(stats, tr.Status())
|
||||
}
|
||||
c.JSON(200, stats)
|
||||
}
|
||||
|
||||
func dropTorrent(req torrReqJS, c *gin.Context) {
|
||||
if req.Hash == "" {
|
||||
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
|
||||
return
|
||||
}
|
||||
torr.DropTorrent(req.Hash)
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
func wipeTorrents(c *gin.Context) {
|
||||
torrents := torr.ListTorrent()
|
||||
for _, t := range torrents {
|
||||
torr.RemTorrent(t.TorrentSpec.InfoHash.HexString())
|
||||
}
|
||||
// TODO: remove (copied todo from remTorrent())
|
||||
if set.BTsets.EnableDLNA {
|
||||
dlna.Stop()
|
||||
dlna.Start()
|
||||
}
|
||||
c.Status(200)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"server/rutor/models"
|
||||
sets "server/settings"
|
||||
"server/torznab"
|
||||
)
|
||||
|
||||
// torznabSearch godoc
|
||||
//
|
||||
// @Summary Makes a torznab search
|
||||
// @Description Makes a torznab search.
|
||||
//
|
||||
// @Tags API
|
||||
//
|
||||
// @Param query query string true "Torznab query"
|
||||
//
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.TorrentDetails "Torznab torrent search result(s)"
|
||||
// @Router /torznab/search [get]
|
||||
func torznabSearch(c *gin.Context) {
|
||||
if !sets.BTsets.EnableTorznabSearch {
|
||||
c.JSON(http.StatusBadRequest, []string{})
|
||||
return
|
||||
}
|
||||
query := c.Query("query")
|
||||
indexStr := c.DefaultQuery("index", "-1")
|
||||
index := -1
|
||||
if i, err := strconv.Atoi(indexStr); err == nil {
|
||||
index = i
|
||||
}
|
||||
|
||||
query, _ = url.QueryUnescape(query)
|
||||
list := torznab.Search(query, index)
|
||||
if list == nil {
|
||||
list = []*models.TorrentDetails{}
|
||||
}
|
||||
c.JSON(200, list)
|
||||
}
|
||||
|
||||
type torznabTestReq struct {
|
||||
Host string `json:"host"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func torznabTest(c *gin.Context) {
|
||||
var req torznabTestReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := torznab.Test(req.Host, req.Key); err != nil {
|
||||
c.JSON(200, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"success": true})
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"server/log"
|
||||
set "server/settings"
|
||||
"server/torr"
|
||||
"server/torr/state"
|
||||
"server/web/api/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// torrentUpload godoc
|
||||
//
|
||||
// @Summary Add .torrent files
|
||||
// @Description Supports multiple files. Returns array of statuses.
|
||||
//
|
||||
// @Tags API
|
||||
//
|
||||
// @Param file formData file true "Torrent file(s) to insert"
|
||||
// @Param save formData string false "Save to DB"
|
||||
// @Param title formData string false "Torrent title (single file only)"
|
||||
// @Param category formData string false "Torrent category"
|
||||
// @Param poster formData string false "Torrent poster (single file only)"
|
||||
// @Param data formData string false "Torrent data"
|
||||
//
|
||||
// @Accept multipart/form-data
|
||||
//
|
||||
// @Produce json
|
||||
// @Success 200 {array} state.TorrentStatus "Torrent statuses"
|
||||
// @Router /torrent/upload [post]
|
||||
func torrentUpload(c *gin.Context) {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer form.RemoveAll()
|
||||
|
||||
save := len(form.Value["save"]) > 0
|
||||
title := ""
|
||||
if len(form.Value["title"]) > 0 {
|
||||
title = form.Value["title"][0]
|
||||
}
|
||||
category := ""
|
||||
if len(form.Value["category"]) > 0 {
|
||||
category = form.Value["category"][0]
|
||||
}
|
||||
poster := ""
|
||||
if len(form.Value["poster"]) > 0 {
|
||||
poster = form.Value["poster"][0]
|
||||
}
|
||||
data := ""
|
||||
if len(form.Value["data"]) > 0 {
|
||||
data = form.Value["data"][0]
|
||||
}
|
||||
|
||||
var files []*multipart.FileHeader
|
||||
for _, fh := range form.File {
|
||||
files = append(files, fh...)
|
||||
}
|
||||
|
||||
var stats []*state.TorrentStatus
|
||||
for _, fh := range files {
|
||||
log.TLogln("add .torrent", fh.Filename)
|
||||
|
||||
torrFile, err := fh.Open()
|
||||
if err != nil {
|
||||
log.TLogln("error upload torrent:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
spec, err := utils.ParseFile(torrFile)
|
||||
torrFile.Close()
|
||||
if err != nil {
|
||||
log.TLogln("error upload torrent:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
tor, err := torr.AddTorrent(spec, title, poster, data, category)
|
||||
if err != nil {
|
||||
log.TLogln("error upload torrent:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if tor.Data != "" && set.BTsets.EnableDebug {
|
||||
log.TLogln("torrent data:", tor.Data)
|
||||
}
|
||||
if tor.Category != "" && set.BTsets.EnableDebug {
|
||||
log.TLogln("torrent category:", tor.Category)
|
||||
}
|
||||
|
||||
go func(t *torr.Torrent) {
|
||||
if !t.GotInfo() {
|
||||
log.TLogln("error add torrent:", "torrent connection timeout")
|
||||
return
|
||||
}
|
||||
|
||||
if t.Title == "" {
|
||||
t.Title = t.Name()
|
||||
}
|
||||
|
||||
if save {
|
||||
torr.SaveTorrentToDB(t)
|
||||
}
|
||||
}(tor)
|
||||
|
||||
stats = append(stats, tor.Status())
|
||||
}
|
||||
c.JSON(200, stats)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"server/torrshash"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
)
|
||||
|
||||
func ParseFromBytes(data []byte) (*torrent.TorrentSpec, error) {
|
||||
minfo, err := metainfo.Load(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := minfo.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mag := minfo.Magnet(nil, &info)
|
||||
return &torrent.TorrentSpec{
|
||||
InfoBytes: minfo.InfoBytes,
|
||||
Trackers: [][]string{mag.Trackers},
|
||||
DisplayName: info.Name,
|
||||
InfoHash: minfo.HashInfoBytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseFile(file multipart.File) (*torrent.TorrentSpec, error) {
|
||||
minfo, err := metainfo.Load(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := minfo.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
|
||||
mag := minfo.Magnet(nil, &info)
|
||||
return &torrent.TorrentSpec{
|
||||
InfoBytes: minfo.InfoBytes,
|
||||
Trackers: [][]string{mag.Trackers},
|
||||
DisplayName: info.Name,
|
||||
InfoHash: minfo.HashInfoBytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseLink(link string) (*torrent.TorrentSpec, error) {
|
||||
urlLink, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(urlLink.Scheme) {
|
||||
case "magnet":
|
||||
return fromMagnet(urlLink.String())
|
||||
case "http", "https":
|
||||
return fromHttp(urlLink.String())
|
||||
case "":
|
||||
return fromMagnet("magnet:?xt=urn:btih:" + urlLink.Path)
|
||||
case "file":
|
||||
return fromFile(urlLink.Path)
|
||||
default:
|
||||
err = fmt.Errorf("unknown scheme:", urlLink, urlLink.Scheme)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func fromMagnet(link string) (*torrent.TorrentSpec, error) {
|
||||
mag, err := metainfo.ParseMagnetUri(link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var trackers [][]string
|
||||
if len(mag.Trackers) > 0 {
|
||||
trackers = [][]string{mag.Trackers}
|
||||
}
|
||||
|
||||
return &torrent.TorrentSpec{
|
||||
InfoBytes: nil,
|
||||
Trackers: trackers,
|
||||
DisplayName: mag.DisplayName,
|
||||
InfoHash: mag.InfoHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseTorrsHash(token string) (*torrent.TorrentSpec, *torrshash.TorrsHash, error) {
|
||||
if strings.HasPrefix(token, "torrs://") {
|
||||
token = strings.TrimPrefix(token, "torrs://")
|
||||
}
|
||||
th, err := torrshash.Unpack(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var trackers [][]string
|
||||
if len(th.Trackers()) > 0 {
|
||||
trackers = [][]string{th.Trackers()}
|
||||
}
|
||||
|
||||
return &torrent.TorrentSpec{
|
||||
InfoBytes: nil,
|
||||
Trackers: trackers,
|
||||
DisplayName: th.Title(),
|
||||
InfoHash: metainfo.NewHashFromHex(th.Hash),
|
||||
}, th, nil
|
||||
}
|
||||
|
||||
func fromHttp(link string) (*torrent.TorrentSpec, error) {
|
||||
req, err := http.NewRequest("GET", link, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := new(http.Client)
|
||||
client.Timeout = time.Duration(time.Second * 60)
|
||||
req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if er, ok := err.(*url.Error); ok {
|
||||
if strings.HasPrefix(er.URL, "magnet:") {
|
||||
return fromMagnet(er.URL)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New(resp.Status)
|
||||
}
|
||||
|
||||
minfo, err := metainfo.Load(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := minfo.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
|
||||
mag := minfo.Magnet(nil, &info)
|
||||
|
||||
return &torrent.TorrentSpec{
|
||||
InfoBytes: minfo.InfoBytes,
|
||||
Trackers: [][]string{mag.Trackers},
|
||||
DisplayName: info.Name,
|
||||
InfoHash: minfo.HashInfoBytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fromFile(path string) (*torrent.TorrentSpec, error) {
|
||||
if runtime.GOOS == "windows" && strings.HasPrefix(path, "/") {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
}
|
||||
minfo, err := metainfo.LoadFromFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := minfo.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
|
||||
mag := minfo.Magnet(nil, &info)
|
||||
return &torrent.TorrentSpec{
|
||||
InfoBytes: minfo.InfoBytes,
|
||||
Trackers: [][]string{mag.Trackers},
|
||||
DisplayName: info.Name,
|
||||
InfoHash: minfo.HashInfoBytes(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func TestLink(link string, auth bool) error {
|
||||
link, err := url.QueryUnescape(link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ur, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ur.Scheme == "magnet" || ur.Scheme == "torrs" || ur.Scheme == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !auth {
|
||||
return fmt.Errorf("auth required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
sets "server/settings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/*
|
||||
file index starts from 1
|
||||
*/
|
||||
|
||||
// Action: set, rem, list
|
||||
type viewedReqJS struct {
|
||||
requestI
|
||||
*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. Available params for action: set, rem, list"
|
||||
//
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} sets.Viewed
|
||||
// @Router /viewed [post]
|
||||
func viewed(c *gin.Context) {
|
||||
var req viewedReqJS
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Action {
|
||||
case "set":
|
||||
{
|
||||
setViewed(req, c)
|
||||
}
|
||||
case "rem":
|
||||
{
|
||||
remViewed(req, c)
|
||||
}
|
||||
case "list":
|
||||
{
|
||||
listViewed(req, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setViewed(req viewedReqJS, c *gin.Context) {
|
||||
sets.SetViewed(req.Viewed)
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
func remViewed(req viewedReqJS, c *gin.Context) {
|
||||
sets.RemViewed(req.Viewed)
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
func listViewed(req viewedReqJS, c *gin.Context) {
|
||||
list := sets.ListViewed(req.Hash)
|
||||
c.JSON(200, list)
|
||||
}
|
||||
Reference in New Issue
Block a user