refactor and to go mod

This commit is contained in:
YouROK
2021-02-18 16:56:55 +03:00
parent 0e49a98626
commit 94f212fa75
50 changed files with 13 additions and 29 deletions

51
server/web/api/cache.go Normal file
View File

@@ -0,0 +1,51 @@
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"`
}
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)
}
}

41
server/web/api/doc.go Normal file
View File

@@ -0,0 +1,41 @@
package api
/*
API
respnose JSON{
}
request JSON{
Action string
}
echo version
{
add torrent
remove torrent
list all torrents
get torrent
stream torrent
} all in one function
{
get viewed files mark in torrent
set viewed files mark in torrent
rem viewed files mark in torrent
} all in one
{
get m3u list of one torrent
get m3u list of one torrent and resume play
get m3u list of all torrent
}
{
get settings
set settings
}
???
*/

125
server/web/api/m3u.go Normal file
View File

@@ -0,0 +1,125 @@
package api
import (
"bytes"
"fmt"
"net/http"
"net/url"
"path/filepath"
"sort"
"time"
"github.com/anacrolix/missinggo/httptoo"
sets "server/settings"
"server/torr"
"server/torr/state"
"server/utils"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
func allPlayList(c *gin.Context) {
torrs := torr.ListTorrent()
host := "http://" + c.Request.Host
list := "#EXTM3U\n"
hash := ""
// fn=file.m3u fix forkplayer bug with end .m3u in link
for _, tr := range torrs {
list += "#EXTINF:0 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)
}
// http://127.0.0.1:8090/playlist?hash=...
// http://127.0.0.1:8090/playlist?hash=...&fromlast
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.WaitInfo() {
c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent info"))
return
}
host := "http://" + c.Request.Host
list := getM3uList(tor.Status(), host, fromlast)
list = "#EXTM3U\n" + list
sendM3U(c, tor.Name()+".m3u", 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 != "" {
c.Header("ETag", httptoo.EncodeQuotedString(fmt.Sprintf("%s/%s", hash, name)))
}
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)))
c.Status(200)
}
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"
name := filepath.Base(f.Path)
m3u += host + "/stream/" + url.PathEscape(name) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(f.Id) + "&play\n"
}
}
}
return m3u
}
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
}

55
server/web/api/route.go Normal file
View File

@@ -0,0 +1,55 @@
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
sets "server/settings"
"server/torr"
"server/version"
)
type requestI struct {
Action string `json:"action,omitempty"`
}
func SetupRoute(route *gin.Engine) {
route.GET("/echo", echo)
route.GET("/shutdown", shutdown)
route.POST("/settings", settings)
route.POST("/torrents", torrents)
route.POST("/torrent/upload", torrentUpload)
route.POST("/cache", cache)
route.HEAD("/stream", stream)
route.HEAD("/stream/*fname", stream)
route.GET("/stream", stream)
route.GET("/stream/*fname", stream)
route.POST("/viewed", viewed)
route.GET("/playlistall/all.m3u", allPlayList)
route.GET("/playlist", playList)
route.GET("/playlist/*fname", playList)
}
func echo(c *gin.Context) {
c.String(200, "%v", version.Version)
}
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

@@ -0,0 +1,37 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
sets "server/settings"
"server/torr"
)
//Action: get, set
type setsReqJS struct {
requestI
Sets *sets.BTSets `json:"sets,omitempty"`
}
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
}
if req.Action == "set" {
torr.SetSettings(req.Sets)
c.Status(200)
return
}
c.AbortWithError(http.StatusBadRequest, errors.New("action is empty"))
}

113
server/web/api/stream.go Normal file
View File

@@ -0,0 +1,113 @@
package api
import (
"net/http"
"net/url"
"strconv"
"strings"
"server/torr"
"server/web/api/utils"
"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&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=...
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")
if link == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty"))
return
}
if title == "" {
title = c.Param("fname")
title, _ = url.PathUnescape(title)
title = strings.TrimLeft(title, "/")
} else {
title, _ = url.QueryUnescape(title)
}
link, _ = url.QueryUnescape(link)
poster, _ = url.QueryUnescape(poster)
spec, err := utils.ParseLink(link)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
tor, err := torr.AddTorrent(spec, title, poster, "")
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if !tor.GotInfo() {
c.AbortWithError(http.StatusInternalServerError, errors.New("timeout connection torrent"))
return
}
// 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 {
m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), "http://"+c.Request.Host, fromlast)
sendM3U(c, tor.Name()+".m3u", tor.Hash().HexString(), m3ulist)
return
} else
// return play if query
if play {
tor.Stream(index, c.Request, c.Writer)
return
}
}

137
server/web/api/torrents.go Normal file
View File

@@ -0,0 +1,137 @@
package api
import (
"net/http"
"server/log"
"server/torr"
"server/torr/state"
"server/web/api/utils"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
//Action: add, get, rem, list, drop
type torrReqJS struct {
requestI
Link string `json:"link,omitempty"`
Hash string `json:"hash,omitempty"`
Title string `json:"title,omitempty"`
Poster string `json:"poster,omitempty"`
Data string `json:"data,omitempty"`
SaveToDB bool `json:"save_to_db,omitempty"`
}
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 "rem":
{
remTorrent(req, c)
}
case "list":
{
listTorrent(req, c)
}
case "drop":
{
dropTorrent(req, 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)
torrSpec, err := utils.ParseLink(req.Link)
if err != nil {
log.TLogln("error add torrent:", err)
c.AbortWithError(http.StatusBadRequest, err)
return
}
tor, err := torr.AddTorrent(torrSpec, req.Title, req.Poster, req.Data)
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 torrent")
return
}
if req.SaveToDB {
torr.SaveTorrentToDB(tor)
}
}()
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 remTorrent(req torrReqJS, c *gin.Context) {
if req.Hash == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty"))
return
}
torr.RemTorrent(req.Hash)
c.Status(200)
}
func listTorrent(req torrReqJS, c *gin.Context) {
list := torr.ListTorrent()
if list == nil {
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)
}

70
server/web/api/upload.go Normal file
View File

@@ -0,0 +1,70 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"server/log"
"server/torr"
"server/web/api/utils"
)
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]
}
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 tor *torr.Torrent
for name, file := range form.File {
log.TLogln("add torrent file", name)
torrFile, err := file[0].Open()
if err != nil {
log.TLogln("error upload torrent:", err)
continue
}
defer torrFile.Close()
spec, err := utils.ParseFile(torrFile)
if err != nil {
log.TLogln("error upload torrent:", err)
continue
}
tor, err = torr.AddTorrent(spec, title, poster, data)
if err != nil {
log.TLogln("error upload torrent:", err)
continue
}
go func() {
if !tor.GotInfo() {
log.TLogln("error add torrent:", "timeout connection torrent")
return
}
if save {
torr.SaveTorrentToDB(tor)
}
}()
break
}
c.JSON(200, tor.Status())
}

View File

@@ -0,0 +1,133 @@
package utils
import (
"errors"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"runtime"
"strings"
"time"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
)
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())
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 fromHttp(url string) (*torrent.TorrentSpec, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
client := new(http.Client)
client.Timeout = time.Duration(time.Second * 30)
req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)")
resp, err := client.Do(req)
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())
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())
return &torrent.TorrentSpec{
InfoBytes: minfo.InfoBytes,
Trackers: [][]string{mag.Trackers},
DisplayName: info.Name,
InfoHash: minfo.HashInfoBytes(),
}, nil
}

57
server/web/api/viewed.go Normal file
View File

@@ -0,0 +1,57 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
sets "server/settings"
)
/*
file index starts from 1
*/
// Action: set, rem, list
type viewedReqJS struct {
requestI
*sets.Viewed
}
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)
}

21
server/web/pages/route.go Normal file
View File

@@ -0,0 +1,21 @@
package pages
import (
"github.com/gin-gonic/gin"
"server/torr"
"server/web/pages/template"
)
func SetupRoute(route *gin.Engine) {
route.GET("/", mainPage)
route.GET("/stat", statPage)
}
func mainPage(c *gin.Context) {
c.Data(200, "text/html; charset=utf-8", template.IndexHtml)
}
func statPage(c *gin.Context) {
torr.WriteStatus(c.Writer)
c.Status(200)
}

File diff suppressed because one or more lines are too long

41
server/web/server.go Normal file
View File

@@ -0,0 +1,41 @@
package web
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"server/log"
"server/torr"
"server/version"
"server/web/api"
"server/web/pages"
)
var (
BTS = torr.NewBTS()
waitChan = make(chan error)
)
func Start(port string) {
log.TLogln("Start TorrServer", version.Version)
err := BTS.Connect()
if err != nil {
waitChan <- err
return
}
route := gin.New()
route.Use(gin.Recovery(), cors.Default())
api.SetupRoute(route)
pages.SetupRoute(route)
waitChan <- route.Run(":" + port)
}
func Wait() error {
return <-waitChan
}
func Stop() {
BTS.Disconnect()
waitChan <- nil
}