Files
TorrServerJellyfin/src/server/web/Torrent.go
2018-12-22 14:33:16 +03:00

661 lines
17 KiB
Go

package server
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"server/settings"
"server/torr"
"server/utils"
"server/web/helpers"
"github.com/anacrolix/missinggo/httptoo"
"github.com/anacrolix/torrent/metainfo"
"github.com/labstack/echo"
)
func initTorrent(e *echo.Echo) {
e.POST("/torrent/add", torrentAdd)
e.POST("/torrent/upload", torrentUpload)
e.POST("/torrent/get", torrentGet)
e.POST("/torrent/rem", torrentRem)
e.POST("/torrent/list", torrentList)
e.POST("/torrent/stat", torrentStat)
e.POST("/torrent/cache", torrentCache)
e.POST("/torrent/drop", torrentDrop)
e.GET("/torrent/restart", torrentRestart)
e.GET("/torrent/playlist.m3u", torrentPlayListAll)
e.GET("/torrent/play", torrentPlay)
e.HEAD("/torrent/play", torrentPlay)
e.GET("/torrent/view/:hash/:file", torrentView)
e.HEAD("/torrent/view/:hash/:file", torrentView)
e.GET("/torrent/preload/:hash/:file", torrentPreload)
e.GET("/torrent/preload/:size/:hash/:file", torrentPreloadSize)
}
type TorrentJsonRequest struct {
Link string `json:",omitempty"`
Hash string `json:",omitempty"`
Title string `json:",omitempty"`
Info string `json:",omitempty"`
DontSave bool `json:",omitempty"`
}
type TorrentJsonResponse struct {
Name string
Magnet string
Hash string
AddTime int64
Length int64
Status torr.TorrentStatus
Playlist string
Info string
Files []TorFile `json:",omitempty"`
}
type TorFile struct {
Name string
Link string
Preload string
Size int64
Viewed bool
}
func torrentAdd(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Link == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Link must be non-empty")
}
magnet, err := helpers.GetMagnet(jreq.Link)
if err != nil {
fmt.Println("Error get magnet:", jreq.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Title != "" {
magnet.DisplayName = jreq.Title
}
err = helpers.Add(bts, *magnet, !jreq.DontSave)
if err != nil {
fmt.Println("Error add torrent:", jreq.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Info != "" {
go func() {
utils.AddInfo(magnet.InfoHash.HexString(), jreq.Info)
}()
}
return c.String(http.StatusOK, magnet.InfoHash.HexString())
}
func torrentUpload(c echo.Context) error {
form, err := c.MultipartForm()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
defer form.RemoveAll()
_, dontSave := form.Value["DontSave"]
var magnets []metainfo.Magnet
for _, file := range form.File {
torrFile, err := file[0].Open()
if err != nil {
return err
}
defer torrFile.Close()
mi, err := metainfo.Load(torrFile)
if err != nil {
fmt.Println("Error upload torrent", err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
info, err := mi.UnmarshalInfo()
if err != nil {
fmt.Println("Error upload torrent", err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
magnet := mi.Magnet(info.Name, mi.HashInfoBytes())
magnets = append(magnets, magnet)
}
ret := make([]string, 0)
for _, magnet := range magnets {
er := helpers.Add(bts, magnet, !dontSave)
if er != nil {
err = er
fmt.Println("Error add torrent:", magnet.String(), er)
}
ret = append(ret, magnet.InfoHash.HexString())
}
return c.JSON(http.StatusOK, ret)
}
func torrentGet(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
tor, err := settings.LoadTorrentDB(jreq.Hash)
if err != nil {
fmt.Println("Error get torrent:", jreq.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
torrStatus := torr.TorrentAdded
if tor == nil {
hash := metainfo.NewHashFromHex(jreq.Hash)
ts := bts.GetTorrent(hash)
if ts != nil {
torrStatus = ts.Status()
tor = toTorrentDB(ts)
}
}
if tor == nil {
fmt.Println("Error get: torrent not found", jreq.Hash)
return echo.NewHTTPError(http.StatusBadRequest, "Error get: torrent not found "+jreq.Hash)
}
js, err := getTorrentJS(tor)
if err != nil {
fmt.Println("Error get torrent:", tor.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
js.Status = torrStatus
return c.JSON(http.StatusOK, js)
}
func torrentRem(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
settings.RemoveTorrentDB(jreq.Hash)
bts.RemoveTorrent(metainfo.NewHashFromHex(jreq.Hash))
return c.JSON(http.StatusOK, nil)
}
func torrentList(c echo.Context) error {
buf, _ := ioutil.ReadAll(c.Request().Body)
jsstr := string(buf)
decoder := json.NewDecoder(bytes.NewBufferString(jsstr))
jsreq := struct {
Request int
}{}
decoder.Decode(&jsreq)
reqType := jsreq.Request
js := make([]TorrentJsonResponse, 0)
list, _ := settings.LoadTorrentsDB()
for _, tor := range list {
jsTor, err := getTorrentJS(tor)
if err != nil {
fmt.Println("Error get torrent:", err)
} else {
js = append(js, *jsTor)
}
}
sort.Slice(js, func(i, j int) bool {
if js[i].AddTime == js[j].AddTime {
return js[i].Name < js[j].Name
}
return js[i].AddTime > js[j].AddTime
})
slist := bts.List()
find := func(tjs []TorrentJsonResponse, t *torr.Torrent) bool {
for _, j := range tjs {
if t.Hash().HexString() == j.Hash {
return true
}
}
return false
}
for _, st := range slist {
if !find(js, st) {
tdb := toTorrentDB(st)
jsTor, err := getTorrentJS(tdb)
if err != nil {
fmt.Println("Error get torrent:", err)
} else {
jsTor.Status = st.Status()
js = append(js, *jsTor)
}
}
}
if reqType == 1 {
ret := make([]TorrentJsonResponse, 0)
for _, r := range js {
if r.Status == torr.TorrentWorking || len(r.Files) > 0 {
ret = append(ret, r)
}
}
return c.JSON(http.StatusOK, ret)
} else if reqType == 2 {
ret := make([]TorrentJsonResponse, 0)
for _, r := range js {
if r.Status == torr.TorrentGettingInfo {
ret = append(ret, r)
}
}
return c.JSON(http.StatusOK, ret)
}
return c.JSON(http.StatusOK, js)
}
func torrentStat(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
hash := metainfo.NewHashFromHex(jreq.Hash)
tor := bts.GetTorrent(hash)
if tor == nil {
return echo.NewHTTPError(http.StatusNotFound)
}
stat := tor.Stats()
return c.JSON(http.StatusOK, stat)
}
func torrentCache(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
hash := metainfo.NewHashFromHex(jreq.Hash)
stat := bts.CacheState(hash)
if stat == nil {
return echo.NewHTTPError(http.StatusNotFound)
}
return c.JSON(http.StatusOK, stat)
}
func preload(hashHex, fileLink string, size int64) *echo.HTTPError {
if size > 0 {
hash := metainfo.NewHashFromHex(hashHex)
tor := bts.GetTorrent(hash)
if tor == nil {
torrDb, err := settings.LoadTorrentDB(hashHex)
if err != nil || torrDb == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Torrent not found: "+hashHex)
}
m, err := metainfo.ParseMagnetURI(torrDb.Magnet)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Error parser magnet in db: "+hashHex)
}
tor, err = bts.AddTorrent(m, nil)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
if !tor.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
file := helpers.FindFileLink(fileLink, tor.Torrent)
if file == nil {
return echo.NewHTTPError(http.StatusNotFound, "file in torrent not found: "+fileLink)
}
tor.Preload(file, size)
}
return nil
}
func torrentPreload(c echo.Context) error {
hashHex, err := url.PathUnescape(c.Param("hash"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
fileLink, err := url.PathUnescape(c.Param("file"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if hashHex == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
if fileLink == "" {
return echo.NewHTTPError(http.StatusBadRequest, "File link must be non-empty")
}
errHttp := preload(hashHex, fileLink, settings.Get().PreloadBufferSize)
if err != nil {
return errHttp
}
return c.NoContent(http.StatusOK)
}
func torrentPreloadSize(c echo.Context) error {
hashHex, err := url.PathUnescape(c.Param("hash"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
fileLink, err := url.PathUnescape(c.Param("file"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
szPreload, err := url.PathUnescape(c.Param("size"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if hashHex == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
if fileLink == "" {
return echo.NewHTTPError(http.StatusBadRequest, "File link must be non-empty")
}
var size = settings.Get().PreloadBufferSize
if szPreload != "" {
sz, err := strconv.Atoi(szPreload)
if err == nil && sz > 0 {
size = int64(sz) * 1024 * 1024
}
}
errHttp := preload(hashHex, fileLink, size)
if err != nil {
return errHttp
}
//redirectUrl := c.Scheme() + "://" + c.Request().Host + filepath.Join("/torrent/view/", hashHex, fileLink)
//return c.Redirect(http.StatusFound, redirectUrl)
return c.NoContent(http.StatusOK)
}
func torrentDrop(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
bts.RemoveTorrent(metainfo.NewHashFromHex(jreq.Hash))
return c.NoContent(http.StatusOK)
}
func torrentRestart(c echo.Context) error {
fmt.Println("Restart torrent engine")
err := bts.Reconnect()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.String(http.StatusOK, "Ok")
}
func torrentPlayListAll(c echo.Context) error {
list, err := settings.LoadTorrentsDB()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
m3u := helpers.MakeM3ULists(list, c.Scheme()+"://"+c.Request().Host)
c.Response().Header().Set("Content-Type", "audio/x-mpegurl")
c.Response().Header().Set("Connection", "close")
c.Response().Header().Set("Content-Disposition", `attachment; filename="playlist.m3u"`)
http.ServeContent(c.Response(), c.Request(), "playlist.m3u", time.Now(), bytes.NewReader([]byte(m3u)))
return c.NoContent(http.StatusOK)
}
func torrentPlay(c echo.Context) error {
link := c.QueryParam("link")
if link == "" {
return echo.NewHTTPError(http.StatusBadRequest, "link should not be empty")
}
fmt.Println("Play:", c.QueryParams())
qsave := c.QueryParam("save")
qpreload := c.QueryParam("preload")
qfile := c.QueryParam("file")
qstat := c.QueryParam("stat")
mm3u := c.QueryParam("m3u")
preload := int64(0)
stat := strings.ToLower(qstat) == "true"
if qpreload != "" {
preload, _ = strconv.ParseInt(qpreload, 10, 64)
if preload > 0 {
preload *= 1024 * 1024
}
}
magnet, err := helpers.GetMagnet(link)
if err != nil {
fmt.Println("Error get magnet:", link, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
tor := bts.GetTorrent(magnet.InfoHash)
if tor == nil {
tor, err = bts.AddTorrent(*magnet, nil)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
}
if stat {
return c.JSON(http.StatusOK, getTorPlayState(tor))
}
if !tor.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
if strings.ToLower(qsave) == "true" {
if t, err := settings.LoadTorrentDB(magnet.InfoHash.HexString()); t == nil && err == nil {
torrDb := toTorrentDB(tor)
if torrDb != nil {
settings.SaveTorrentDB(torrDb)
}
}
}
if strings.ToLower(mm3u) == "true" {
mt := tor.Torrent.Metainfo()
m3u := helpers.MakeM3UPlayList(tor.Stats(), mt.Magnet(tor.Name(), tor.Hash()).String(), c.Scheme()+"://"+c.Request().Host)
c.Response().Header().Set("Content-Type", "audio/x-mpegurl")
c.Response().Header().Set("Connection", "close")
name := utils.CleanFName(tor.Name()) + ".m3u"
c.Response().Header().Set("ETag", httptoo.EncodeQuotedString(fmt.Sprintf("%s/%s", tor.Hash().HexString(), name)))
c.Response().Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
http.ServeContent(c.Response(), c.Request(), name, time.Now(), bytes.NewReader([]byte(m3u)))
return c.NoContent(http.StatusOK)
}
files := helpers.GetPlayableFiles(tor.Stats())
if len(files) == 1 {
file := helpers.FindFile(files[0].Id, tor)
if file == nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprint("File", files[0], "not found in torrent", tor.Name()))
}
return bts.Play(tor, file, preload, c)
}
if qfile == "" && len(files) > 1 {
return c.JSON(http.StatusOK, getTorPlayState(tor))
}
fileInd, _ := strconv.Atoi(qfile)
file := helpers.FindFile(fileInd, tor)
if file == nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprint("File index ", fileInd, " not found in torrent ", tor.Name()))
}
return bts.Play(tor, file, preload, c)
}
func torrentView(c echo.Context) error {
hashHex, err := url.PathUnescape(c.Param("hash"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
fileLink, err := url.PathUnescape(c.Param("file"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
hash := metainfo.NewHashFromHex(hashHex)
tor := bts.GetTorrent(hash)
if tor == nil {
torrDb, err := settings.LoadTorrentDB(hashHex)
if err != nil || torrDb == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Torrent not found: "+hashHex)
}
m, err := metainfo.ParseMagnetURI(torrDb.Magnet)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Error parser magnet in db: "+hashHex)
}
tor, err = bts.AddTorrent(m, nil)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
if !tor.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
file := helpers.FindFileLink(fileLink, tor.Torrent)
if file == nil {
return echo.NewHTTPError(http.StatusNotFound, "File in torrent not found: "+fileLink)
}
return bts.View(tor, file, c)
}
func toTorrentDB(t *torr.Torrent) *settings.Torrent {
if t == nil {
return nil
}
tor := new(settings.Torrent)
tor.Name = t.Name()
tor.Hash = t.Hash().HexString()
tor.Timestamp = settings.StartTime.Unix()
mi := t.Torrent.Metainfo()
tor.Magnet = mi.Magnet(t.Name(), t.Torrent.InfoHash()).String()
tor.Size = t.Length()
files := t.Files()
sort.Slice(files, func(i, j int) bool {
return files[i].Path() < files[j].Path()
})
for _, f := range files {
tf := settings.File{
Name: f.Path(),
Size: f.Length(),
Viewed: false,
}
tor.Files = append(tor.Files, tf)
}
return tor
}
func getTorrentJS(tor *settings.Torrent) (*TorrentJsonResponse, error) {
js := new(TorrentJsonResponse)
mag, err := metainfo.ParseMagnetURI(tor.Magnet)
js.Name = tor.Name
if err == nil && len(tor.Name) < len(mag.DisplayName) {
js.Name = mag.DisplayName
}
js.Magnet = tor.Magnet
js.Hash = tor.Hash
js.AddTime = tor.Timestamp
js.Length = tor.Size
//fname is fake param for file name
js.Playlist = "/torrent/play?link=" + url.QueryEscape(tor.Magnet) + "&m3u=true&fname=" + utils.CleanFName(tor.Name+".m3u")
var size int64 = 0
for _, f := range tor.Files {
size += f.Size
tf := TorFile{
Name: f.Name,
Link: "/torrent/view/" + js.Hash + "/" + utils.CleanFName(f.Name),
Preload: "/torrent/preload/" + js.Hash + "/" + utils.CleanFName(f.Name),
Size: f.Size,
Viewed: f.Viewed,
}
js.Files = append(js.Files, tf)
}
if tor.Size == 0 {
js.Length = size
}
js.Info = settings.GetInfo(tor.Hash)
return js, nil
}
func getJsReqTorr(c echo.Context) (*TorrentJsonRequest, error) {
buf, _ := ioutil.ReadAll(c.Request().Body)
jsstr := string(buf)
decoder := json.NewDecoder(bytes.NewBufferString(jsstr))
js := new(TorrentJsonRequest)
err := decoder.Decode(js)
if err != nil {
if ute, ok := err.(*json.UnmarshalTypeError); ok {
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset))
} else if se, ok := err.(*json.SyntaxError); ok {
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error()))
} else {
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
return js, nil
}