diff --git a/src/server/settings/db.go b/src/server/settings/db.go index 0159905..480b141 100644 --- a/src/server/settings/db.go +++ b/src/server/settings/db.go @@ -126,9 +126,9 @@ func (v *TDB) List(xpath string) []string { } } - buckt.ForEach(func(_, v []byte) error { - if len(v) > 0 { - ret = append(ret, string(v)) + buckt.ForEach(func(k, _ []byte) error { + if len(k) > 0 { + ret = append(ret, string(k)) } return nil }) diff --git a/src/server/settings/torrent.go b/src/server/settings/torrent.go index f9c5898..ae8153f 100644 --- a/src/server/settings/torrent.go +++ b/src/server/settings/torrent.go @@ -7,7 +7,7 @@ import ( "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" - "server/torr" + "server/torr/state" ) type TorrentDB struct { @@ -18,7 +18,7 @@ type TorrentDB struct { Timestamp int64 `json:"timestamp,omitempty"` - Files []torr.TorrentFileStat `json:"files,omitempty"` + Files []state.TorrentFileStat `json:"files,omitempty"` } type File struct { @@ -63,7 +63,7 @@ func ListTorrent() []*TorrentDB { buf := tdb.Get("Torrents", key) if len(buf) > 0 { var torr *TorrentDB - err := json.Unmarshal(buf, torr) + err := json.Unmarshal(buf, &torr) if err == nil { list = append(list, torr) } diff --git a/src/server/settings/viewed.go b/src/server/settings/viewed.go new file mode 100644 index 0000000..f8acb23 --- /dev/null +++ b/src/server/settings/viewed.go @@ -0,0 +1,79 @@ +package settings + +import ( + "encoding/json" + + "server/log" +) + +type Viewed struct { + Hash string `json:"hash"` + FileIndex int `json:"file_index"` +} + +func SetViewed(vv *Viewed) { + buf := tdb.Get("Viewed", vv.Hash) + var indeces map[int]struct{} + err := json.Unmarshal(buf, &indeces) + if err == nil { + indeces[vv.FileIndex] = struct{}{} + buf, err = json.Marshal(indeces) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } + if err != nil { + log.TLogln("Error set viewed:", err) + } +} + +func RemViewed(vv *Viewed) { + buf := tdb.Get("Viewed", vv.Hash) + var indeces map[int]struct{} + err := json.Unmarshal(buf, &indeces) + if err == nil { + delete(indeces, vv.FileIndex) + buf, err = json.Marshal(indeces) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } + if err != nil { + log.TLogln("Error rem viewed:", err) + } +} + +func ListViewed(hash string) []*Viewed { + var err error + if hash != "" { + buf := tdb.Get("Viewed", hash) + var indeces map[int]struct{} + err = json.Unmarshal(buf, &indeces) + if err == nil { + var ret []*Viewed + for i, _ := range indeces { + ret = append(ret, &Viewed{hash, i}) + } + return ret + } + } else { + var ret []*Viewed + keys := tdb.List("Viewed") + for _, key := range keys { + buf := tdb.Get("Viewed", key) + var indeces map[int]struct{} + err = json.Unmarshal(buf, &indeces) + if err == nil { + for i, _ := range indeces { + ret = append(ret, &Viewed{hash, i}) + } + } + } + return ret + } + + if err != nil { + log.TLogln("Error list viewed:", err) + } + return nil +} diff --git a/src/server/torr/btserver.go b/src/server/torr/btserver.go index bc9c21b..b8776b1 100644 --- a/src/server/torr/btserver.go +++ b/src/server/torr/btserver.go @@ -83,6 +83,9 @@ func (bt *BTServer) configure() { bt.config.HTTPUserAgent = userAgent bt.config.ExtendedHandshakeClientVersion = cliVers bt.config.EstablishedConnsPerTorrent = settings.BTsets.ConnectionsLimit + + bt.config.DefaultRequestStrategy = torrent.RequestStrategyFastest() + if settings.BTsets.DhtConnectionLimit > 0 { bt.config.ConnTracker.SetMaxEntries(settings.BTsets.DhtConnectionLimit) } diff --git a/src/server/torr/state.go b/src/server/torr/state.go deleted file mode 100644 index ae6e259..0000000 --- a/src/server/torr/state.go +++ /dev/null @@ -1,130 +0,0 @@ -package torr - -import ( - "fmt" - "sort" - - "github.com/anacrolix/torrent" -) - -type BTState struct { - LocalPort int - PeerID string - BannedIPs int - DHTs []torrent.DhtServer - - Torrents []*Torrent -} - -func (bt *BTServer) BTState() *BTState { - bt.mu.Lock() - defer bt.mu.Unlock() - - btState := new(BTState) - btState.LocalPort = bt.client.LocalPort() - btState.PeerID = fmt.Sprintf("%x", bt.client.PeerID()) - btState.BannedIPs = len(bt.client.BadPeerIPs()) - btState.DHTs = bt.client.DhtServers() - - for _, t := range bt.torrents { - btState.Torrents = append(btState.Torrents, t) - } - return btState -} - -type TorrentStats struct { - Name string `json:"name,omitempty"` - Hash string `json:"hash,omitempty"` - - TorrentStatus TorrentStatus `json:"torrent_status,omitempty"` - TorrentStatusString string `json:"torrent_status_string,omitempty"` - - LoadedSize int64 `json:"loaded_size,omitempty"` - TorrentSize int64 `json:"torrent_size,omitempty"` - - PreloadedBytes int64 `json:"preloaded_bytes,omitempty"` - PreloadSize int64 `json:"preload_size,omitempty"` - - DownloadSpeed float64 `json:"download_speed,omitempty"` - UploadSpeed float64 `json:"upload_speed,omitempty"` - - TotalPeers int `json:"total_peers,omitempty"` - PendingPeers int `json:"pending_peers,omitempty"` - ActivePeers int `json:"active_peers,omitempty"` - ConnectedSeeders int `json:"connected_seeders,omitempty"` - HalfOpenPeers int `json:"half_open_peers,omitempty"` - - BytesWritten int64 `json:"bytes_written,omitempty"` - BytesWrittenData int64 `json:"bytes_written_data,omitempty"` - BytesRead int64 `json:"bytes_read,omitempty"` - BytesReadData int64 `json:"bytes_read_data,omitempty"` - BytesReadUsefulData int64 `json:"bytes_read_useful_data,omitempty"` - ChunksWritten int64 `json:"chunks_written,omitempty"` - ChunksRead int64 `json:"chunks_read,omitempty"` - ChunksReadUseful int64 `json:"chunks_read_useful,omitempty"` - ChunksReadWasted int64 `json:"chunks_read_wasted,omitempty"` - PiecesDirtiedGood int64 `json:"pieces_dirtied_good,omitempty"` - PiecesDirtiedBad int64 `json:"pieces_dirtied_bad,omitempty"` - - FileStats []TorrentFileStat `json:"file_stats,omitempty"` -} - -type TorrentFileStat struct { - Id int `json:"id,omitempty"` - Path string `json:"path,omitempty"` - Length int64 `json:"length,omitempty"` -} - -func (t *Torrent) Stats() TorrentStats { - t.muTorrent.Lock() - defer t.muTorrent.Unlock() - - st := TorrentStats{} - - st.Name = t.Name() - st.Hash = t.hash.HexString() - st.TorrentStatus = t.Status - st.TorrentStatusString = t.Status.String() - - if t.Torrent != nil { - st.LoadedSize = t.Torrent.BytesCompleted() - st.TorrentSize = t.Length() - st.PreloadedBytes = t.PreloadedBytes - st.PreloadSize = t.PreloadSize - st.DownloadSpeed = t.DownloadSpeed - st.UploadSpeed = t.UploadSpeed - - tst := t.Torrent.Stats() - st.BytesWritten = tst.BytesWritten.Int64() - st.BytesWrittenData = tst.BytesWrittenData.Int64() - st.BytesRead = tst.BytesRead.Int64() - st.BytesReadData = tst.BytesReadData.Int64() - st.BytesReadUsefulData = tst.BytesReadUsefulData.Int64() - st.ChunksWritten = tst.ChunksWritten.Int64() - st.ChunksRead = tst.ChunksRead.Int64() - st.ChunksReadUseful = tst.ChunksReadUseful.Int64() - st.ChunksReadWasted = tst.ChunksReadWasted.Int64() - st.PiecesDirtiedGood = tst.PiecesDirtiedGood.Int64() - st.PiecesDirtiedBad = tst.PiecesDirtiedBad.Int64() - st.TotalPeers = tst.TotalPeers - st.PendingPeers = tst.PendingPeers - st.ActivePeers = tst.ActivePeers - st.ConnectedSeeders = tst.ConnectedSeeders - st.HalfOpenPeers = tst.HalfOpenPeers - - files := t.Files() - - sort.Slice(files, func(i, j int) bool { - return files[i].Path() < files[j].Path() - }) - - for i, f := range files { - st.FileStats = append(st.FileStats, TorrentFileStat{ - Id: i, - Path: f.Path(), - Length: f.Length(), - }) - } - } - return st -} diff --git a/src/server/torr/state/state.go b/src/server/torr/state/state.go new file mode 100644 index 0000000..7586e6a --- /dev/null +++ b/src/server/torr/state/state.go @@ -0,0 +1,74 @@ +package state + +type TorrentStatus int + +func (t TorrentStatus) String() string { + switch t { + case TorrentAdded: + return "Torrent added" + case TorrentGettingInfo: + return "Torrent getting info" + case TorrentPreload: + return "Torrent preload" + case TorrentWorking: + return "Torrent working" + case TorrentClosed: + return "Torrent closed" + case TorrentInDB: + return "Torrent in db" + default: + return "Torrent unknown status" + } +} + +const ( + TorrentAdded = TorrentStatus(iota) + TorrentGettingInfo + TorrentPreload + TorrentWorking + TorrentClosed + TorrentInDB +) + +type TorrentStats struct { + Name string `json:"name,omitempty"` + Hash string `json:"hash,omitempty"` + + TorrentStatus TorrentStatus `json:"torrent_status,omitempty"` + TorrentStatusString string `json:"torrent_status_string,omitempty"` + + LoadedSize int64 `json:"loaded_size,omitempty"` + TorrentSize int64 `json:"torrent_size,omitempty"` + + PreloadedBytes int64 `json:"preloaded_bytes,omitempty"` + PreloadSize int64 `json:"preload_size,omitempty"` + + DownloadSpeed float64 `json:"download_speed,omitempty"` + UploadSpeed float64 `json:"upload_speed,omitempty"` + + TotalPeers int `json:"total_peers,omitempty"` + PendingPeers int `json:"pending_peers,omitempty"` + ActivePeers int `json:"active_peers,omitempty"` + ConnectedSeeders int `json:"connected_seeders,omitempty"` + HalfOpenPeers int `json:"half_open_peers,omitempty"` + + BytesWritten int64 `json:"bytes_written,omitempty"` + BytesWrittenData int64 `json:"bytes_written_data,omitempty"` + BytesRead int64 `json:"bytes_read,omitempty"` + BytesReadData int64 `json:"bytes_read_data,omitempty"` + BytesReadUsefulData int64 `json:"bytes_read_useful_data,omitempty"` + ChunksWritten int64 `json:"chunks_written,omitempty"` + ChunksRead int64 `json:"chunks_read,omitempty"` + ChunksReadUseful int64 `json:"chunks_read_useful,omitempty"` + ChunksReadWasted int64 `json:"chunks_read_wasted,omitempty"` + PiecesDirtiedGood int64 `json:"pieces_dirtied_good,omitempty"` + PiecesDirtiedBad int64 `json:"pieces_dirtied_bad,omitempty"` + + FileStats []TorrentFileStat `json:"file_stats,omitempty"` +} + +type TorrentFileStat struct { + Id int `json:"id,omitempty"` + Path string `json:"path,omitempty"` + Length int64 `json:"length,omitempty"` +} diff --git a/src/server/torr/storage/torrstor/cache.go b/src/server/torr/storage/torrstor/cache.go index 4ac7cd9..ea034df 100644 --- a/src/server/torr/storage/torrstor/cache.go +++ b/src/server/torr/storage/torrstor/cache.go @@ -1,11 +1,11 @@ package torrstor import ( - "log" "sort" "sync" "github.com/anacrolix/torrent" + "server/log" "server/settings" "server/torr/utils" @@ -52,7 +52,7 @@ func NewCache(capacity int64, storage *Storage) *Cache { } func (c *Cache) Init(info *metainfo.Info, hash metainfo.Hash) { - log.Println("Create cache for:", info.Name) + log.TLogln("Create cache for:", info.Name) if c.capacity == 0 { c.capacity = info.PieceLength * 6 } @@ -89,7 +89,7 @@ func (c *Cache) Piece(m metainfo.Piece) storage.PieceImpl { func (c *Cache) Close() error { c.isRemove = false - log.Println("Close cache for:", c.hash) + log.TLogln("Close cache for:", c.hash) if _, ok := c.s.caches[c.hash]; ok { delete(c.s.caches, c.hash) } diff --git a/src/server/torr/torrent.go b/src/server/torr/torrent.go index 28c0d78..c348394 100644 --- a/src/server/torr/torrent.go +++ b/src/server/torr/torrent.go @@ -2,11 +2,13 @@ package torr import ( "io" - "log" + "sort" "sync" "time" + "server/log" "server/settings" + "server/torr/state" "server/torr/utils" utils2 "server/utils" @@ -16,43 +18,13 @@ import ( "github.com/anacrolix/torrent/metainfo" ) -type TorrentStatus int - -func (t TorrentStatus) String() string { - switch t { - case TorrentAdded: - return "Torrent added" - case TorrentGettingInfo: - return "Torrent getting info" - case TorrentPreload: - return "Torrent preload" - case TorrentWorking: - return "Torrent working" - case TorrentClosed: - return "Torrent closed" - case TorrentInDB: - return "Torrent in db" - default: - return "Torrent unknown status" - } -} - -const ( - TorrentAdded = TorrentStatus(iota) - TorrentGettingInfo - TorrentPreload - TorrentWorking - TorrentClosed - TorrentInDB -) - type Torrent struct { ///// info for db Title string Poster string *torrent.TorrentSpec - Status TorrentStatus + Status state.TorrentStatus ///// *torrent.Torrent @@ -103,7 +75,7 @@ func NewTorrent(spec *torrent.TorrentSpec, bt *BTServer) (*Torrent, error) { torr := new(Torrent) torr.Torrent = goTorrent - torr.Status = TorrentAdded + torr.Status = state.TorrentAdded torr.lastTimeSpeed = time.Now() torr.bt = bt torr.closed = goTorrent.Closed() @@ -135,12 +107,12 @@ func (t *Torrent) WaitInfo() bool { } func (t *Torrent) GotInfo() bool { - if t.Status == TorrentClosed { + if t.Status == state.TorrentClosed { return false } - t.Status = TorrentGettingInfo + t.Status = state.TorrentGettingInfo if t.WaitInfo() { - t.Status = TorrentWorking + t.Status = state.TorrentWorking t.expiredTime = time.Now().Add(time.Minute * 5) return true } else { @@ -166,6 +138,7 @@ func (t *Torrent) watch() { func (t *Torrent) progressEvent() { if t.expired() { + log.TLogln("Torrent close by timeout", t.Torrent.InfoHash().HexString()) t.drop() return } @@ -207,7 +180,7 @@ func (t *Torrent) updateRA() { } func (t *Torrent) expired() bool { - return t.cache.ReadersLen() == 0 && t.expiredTime.Before(time.Now()) && (t.Status == TorrentWorking || t.Status == TorrentClosed) + return t.cache.ReadersLen() == 0 && t.expiredTime.Before(time.Now()) && (t.Status == state.TorrentWorking || t.Status == state.TorrentClosed) } func (t *Torrent) Files() []*torrent.File { @@ -230,7 +203,7 @@ func (t *Torrent) Length() int64 { } func (t *Torrent) NewReader(file *torrent.File, readahead int64) *Reader { - if t.Status == TorrentClosed { + if t.Status == state.TorrentClosed { return nil } reader := NewReader(t, file, readahead) @@ -252,14 +225,14 @@ func (t *Torrent) Preload(index int, size int64) { return } - if t.Status == TorrentGettingInfo { + if t.Status == state.TorrentGettingInfo { t.WaitInfo() // wait change status time.Sleep(100 * time.Millisecond) } t.muTorrent.Lock() - if t.Status != TorrentWorking { + if t.Status != state.TorrentWorking { t.muTorrent.Unlock() return } @@ -271,12 +244,12 @@ func (t *Torrent) Preload(index int, size int64) { t.muTorrent.Unlock() return } - t.Status = TorrentPreload + t.Status = state.TorrentPreload t.muTorrent.Unlock() defer func() { - if t.Status == TorrentPreload { - t.Status = TorrentWorking + if t.Status == state.TorrentPreload { + t.Status = state.TorrentWorking } }() @@ -321,10 +294,10 @@ func (t *Torrent) Preload(index int, size int64) { t.PreloadSize = size var lastSize int64 = 0 errCount := 0 - for t.Status == TorrentPreload { + for t.Status == state.TorrentPreload { t.expiredTime = time.Now().Add(time.Minute * 5) t.PreloadedBytes = t.Torrent.BytesCompleted() - log.Println("Preload:", file.Torrent().InfoHash().HexString(), utils2.Format(float64(t.PreloadedBytes)), "/", utils2.Format(float64(t.PreloadSize)), "Speed:", utils2.Format(t.DownloadSpeed), "Peers:[", t.Torrent.Stats().ConnectedSeeders, "]", t.Torrent.Stats().ActivePeers, "/", t.Torrent.Stats().TotalPeers) + log.TLogln("Preload:", file.Torrent().InfoHash().HexString(), utils2.Format(float64(t.PreloadedBytes)), "/", utils2.Format(float64(t.PreloadSize)), "Speed:", utils2.Format(t.DownloadSpeed), "Peers:[", t.Torrent.Stats().ConnectedSeeders, "]", t.Torrent.Stats().ActivePeers, "/", t.Torrent.Stats().TotalPeers) if t.PreloadedBytes >= t.PreloadSize { return } @@ -352,7 +325,7 @@ func (t *Torrent) drop() { } func (t *Torrent) Close() { - t.Status = TorrentClosed + t.Status = state.TorrentClosed t.bt.mu.Lock() defer t.bt.mu.Unlock() @@ -362,3 +335,59 @@ func (t *Torrent) Close() { t.drop() } + +func (t *Torrent) Stats() *state.TorrentStats { + t.muTorrent.Lock() + defer t.muTorrent.Unlock() + + st := new(state.TorrentStats) + + st.TorrentStatus = t.Status + st.TorrentStatusString = t.Status.String() + if t.TorrentSpec != nil { + st.Hash = t.TorrentSpec.InfoHash.HexString() + } + if t.Torrent != nil { + st.Name = t.Torrent.Name() + st.Hash = t.Torrent.InfoHash().HexString() + st.LoadedSize = t.Torrent.BytesCompleted() + st.TorrentSize = t.Length() + st.PreloadedBytes = t.PreloadedBytes + st.PreloadSize = t.PreloadSize + st.DownloadSpeed = t.DownloadSpeed + st.UploadSpeed = t.UploadSpeed + + tst := t.Torrent.Stats() + st.BytesWritten = tst.BytesWritten.Int64() + st.BytesWrittenData = tst.BytesWrittenData.Int64() + st.BytesRead = tst.BytesRead.Int64() + st.BytesReadData = tst.BytesReadData.Int64() + st.BytesReadUsefulData = tst.BytesReadUsefulData.Int64() + st.ChunksWritten = tst.ChunksWritten.Int64() + st.ChunksRead = tst.ChunksRead.Int64() + st.ChunksReadUseful = tst.ChunksReadUseful.Int64() + st.ChunksReadWasted = tst.ChunksReadWasted.Int64() + st.PiecesDirtiedGood = tst.PiecesDirtiedGood.Int64() + st.PiecesDirtiedBad = tst.PiecesDirtiedBad.Int64() + st.TotalPeers = tst.TotalPeers + st.PendingPeers = tst.PendingPeers + st.ActivePeers = tst.ActivePeers + st.ConnectedSeeders = tst.ConnectedSeeders + st.HalfOpenPeers = tst.HalfOpenPeers + + files := t.Files() + + sort.Slice(files, func(i, j int) bool { + return files[i].Path() < files[j].Path() + }) + + for i, f := range files { + st.FileStats = append(st.FileStats, state.TorrentFileStat{ + Id: i, + Path: f.Path(), + Length: f.Length(), + }) + } + } + return st +} diff --git a/src/server/utils/filetypes.go b/src/server/utils/filetypes.go new file mode 100644 index 0000000..268ca45 --- /dev/null +++ b/src/server/utils/filetypes.go @@ -0,0 +1,89 @@ +package utils + +import ( + "path/filepath" + + "server/torr/state" +) + +var extVideo = map[string]interface{}{ + ".3g2": nil, + ".3gp": nil, + ".aaf": nil, + ".asf": nil, + ".avchd": nil, + ".avi": nil, + ".drc": nil, + ".flv": nil, + ".m2ts": nil, + ".ts": nil, + ".m2v": nil, + ".m4p": nil, + ".m4v": nil, + ".mkv": nil, + ".mng": nil, + ".mov": nil, + ".mp2": nil, + ".mp4": nil, + ".mpe": nil, + ".mpeg": nil, + ".mpg": nil, + ".mpv": nil, + ".mxf": nil, + ".nsv": nil, + ".ogg": nil, + ".ogv": nil, + ".qt": nil, + ".rm": nil, + ".rmvb": nil, + ".roq": nil, + ".svi": nil, + ".vob": nil, + ".webm": nil, + ".wmv": nil, + ".yuv": nil, +} + +var extAudio = map[string]interface{}{ + ".aac": nil, + ".aiff": nil, + ".ape": nil, + ".au": nil, + ".flac": nil, + ".gsm": nil, + ".it": nil, + ".m3u": nil, + ".m4a": nil, + ".mid": nil, + ".mod": nil, + ".mp3": nil, + ".mpa": nil, + ".pls": nil, + ".ra": nil, + ".s3m": nil, + ".sid": nil, + ".wav": nil, + ".wma": nil, + ".xm": nil, +} + +func GetMimeType(filename string) string { + ext := filepath.Ext(filename) + if _, ok := extVideo[ext]; ok { + return "video/*" + } + if _, ok := extAudio[ext]; ok { + return "audio/*" + } + return "*/*" +} + +func GetPlayableFiles(st state.TorrentStats) []state.TorrentFileStat { + files := make([]state.TorrentFileStat, 0) + for _, f := range st.FileStats { + if GetMimeType(f.Path) != "*/*" { + files = append(files, f) + } + } + return files +} diff --git a/src/server/web/api/m3u.go b/src/server/web/api/m3u.go new file mode 100644 index 0000000..10ee322 --- /dev/null +++ b/src/server/web/api/m3u.go @@ -0,0 +1,109 @@ +package api + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + sets "server/settings" + "server/torr/state" + "server/utils" +) + +func allPlayList(c *gin.Context) { + _, fromlast := c.GetQuery("fromlast") + stats := listTorrents() + + host := "http://" + c.Request.Host + list := "#EXTM3U\n" + + for _, stat := range stats { + list += getM3uList(stat, host, fromlast) + } + + c.Header("Content-Type", "audio/x-mpegurl") + c.Header("Connection", "close") + c.Header("Content-Disposition", `attachment; filename="all.m3u"`) + http.ServeContent(c.Writer, c.Request, "all.m3u", time.Now(), bytes.NewReader([]byte(list))) + + c.Status(200) +} + +func playList(c *gin.Context) { + hash := c.Query("torrhash") + _, fromlast := c.GetQuery("fromlast") + if hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + + stats := listTorrents() + var stat *state.TorrentStats + for _, st := range stats { + if st.Hash == hash { + stat = st + break + } + } + + if stat == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + + // TODO проверить + host := "http://" + c.Request.Host + list := getM3uList(stat, host, fromlast) + list = "#EXTM3U\n" + list + + c.Header("Content-Type", "audio/x-mpegurl") + c.Header("Connection", "close") + c.Header("Content-Disposition", `attachment; filename="playlist.m3u"`) + http.ServeContent(c.Writer, c.Request, "playlist.m3u", time.Now(), bytes.NewReader([]byte(list))) + + c.Status(200) +} + +func getM3uList(tor *state.TorrentStats, 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" + // http://127.0.0.1:8090/stream/fname?link=...&index=0&play + m3u += host + "/stream/" + url.QueryEscape(f.Path) + "?link=" + tor.Hash + "&file=" + fmt.Sprint(f.Id) + "\n" + } + } + } + return m3u +} + +func searchLastPlayed(tor *state.TorrentStats) int { + //TODO проверить + viewed := sets.ListViewed(tor.Hash) + for i := len(tor.FileStats); i > 0; i-- { + stat := tor.FileStats[i] + for _, v := range viewed { + if stat.Id == v.FileIndex { + return v.FileIndex + } + } + } + return -1 +} diff --git a/src/server/web/api/route.go b/src/server/web/api/route.go index 0253384..f4c16af 100644 --- a/src/server/web/api/route.go +++ b/src/server/web/api/route.go @@ -25,6 +25,11 @@ func SetupRouteApi(route *gin.Engine, serv *torr.BTServer) { route.GET("/stream", stream) route.GET("/stream/*fname", stream) + + route.POST("/viewed", viewed) + + route.GET("/playlist/all.m3u", allPlayList) + route.GET("/playlist", playList) } func echo(c *gin.Context) { diff --git a/src/server/web/api/settings.go b/src/server/web/api/settings.go index ab558c0..649a88c 100644 --- a/src/server/web/api/settings.go +++ b/src/server/web/api/settings.go @@ -13,10 +13,6 @@ type setsReqJS struct { requestI Sets *settings2.BTSets `json:"sets,omitempty"` } -type setsRespJS struct { - responseI - Sets *settings2.BTSets `json:"sets,omitempty"` -} func settings(c *gin.Context) { var req setsReqJS @@ -27,9 +23,7 @@ func settings(c *gin.Context) { } if req.Action == "get" { - resp := setsRespJS{} - resp.Sets = settings2.BTsets - c.JSON(200, resp) + c.JSON(200, settings2.BTsets) return } if req.Action == "set" { diff --git a/src/server/web/api/stream.go b/src/server/web/api/stream.go index 8aa559b..2f25c03 100644 --- a/src/server/web/api/stream.go +++ b/src/server/web/api/stream.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "net/url" "strconv" "github.com/gin-gonic/gin" @@ -10,6 +11,13 @@ import ( "server/web/api/utils" ) +// http://127.0.0.1:8090/stream/fname?link=...&index=0&stat +// http://127.0.0.1:8090/stream/fname?link=...&index=0&m3u +// http://127.0.0.1:8090/stream/fname?link=...&index=0&play +// http://127.0.0.1:8090/stream/fname?link=...&save&title=...&poster=... +// http://127.0.0.1:8090/stream/fname?link=...&index=0&play&save +// http://127.0.0.1:8090/stream/fname?link=...&index=0&play&save&title=...&poster=... + func stream(c *gin.Context) { link := c.Query("link") indexStr := c.Query("index") @@ -21,13 +29,19 @@ func stream(c *gin.Context) { title := c.Query("title") poster := c.Query("poster") - // TODO unescape args - if link == "" { c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty")) return } + if title == "" { + title = c.Param("fname") + } + + link, _ = url.QueryUnescape(link) + title, _ = url.QueryUnescape(title) + poster, _ = url.QueryUnescape(poster) + spec, err := utils.ParseLink(link) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) @@ -65,6 +79,7 @@ func stream(c *gin.Context) { // save to db if save { utils.AddTorrent(tor) + c.Status(200) } // wait torrent info if !tor.WaitInfo() { @@ -94,13 +109,13 @@ func stream(c *gin.Context) { if stat || (!m3u && !play) { c.JSON(200, tor.Stats()) return - } + } else // return m3u if query if m3u { //TODO m3u c.JSON(200, tor.Stats()) return - } + } else // return play if query if play { tor.Stream(index, c.Request, c.Writer) diff --git a/src/server/web/api/torrents.go b/src/server/web/api/torrents.go index 08f861e..554b779 100644 --- a/src/server/web/api/torrents.go +++ b/src/server/web/api/torrents.go @@ -5,11 +5,14 @@ import ( "github.com/anacrolix/torrent/metainfo" "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "server/log" "server/torr" + "server/torr/state" "server/web/api/utils" ) -//Action: add, get, rem, list +//Action: add, get, rem, list, drop type torrReqJS struct { requestI Link string `json:"link,omitempty"` @@ -26,43 +29,62 @@ func torrents(c *gin.Context) { c.AbortWithError(http.StatusBadRequest, err) return } - + c.Status(http.StatusBadRequest) switch req.Action { case "add": { - add(req, c) + addTorrent(req, c) } case "get": { - get(req, c) + getTorrent(req, c) } case "rem": { - rem(req, c) + remTorrent(req, c) } case "list": { - list(req, c) + listTorrent(req, c) + } + case "drop": + { + dropTorrent(req, c) } } } -func add(req torrReqJS, c *gin.Context) { +func addTorrent(req torrReqJS, c *gin.Context) { + 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 } torr, err := torr.NewTorrent(torrSpec, bts) if err != nil { + log.TLogln("error add torrent:", err) c.AbortWithError(http.StatusInternalServerError, err) return } + + if !torr.WaitInfo() { + log.TLogln("error add torrent:", "timeout connection torrent") + c.AbortWithError(http.StatusNotFound, errors.New("timeout connection torrent")) + return + } + torr.Title = req.Title torr.Poster = req.Poster + if torr.Title == "" { + torr.Title = torr.Name() + } + if req.SaveToDB { + log.TLogln("save to db:", torr.Torrent.InfoHash().HexString()) utils.AddTorrent(torr) } @@ -70,7 +92,7 @@ func add(req torrReqJS, c *gin.Context) { c.JSON(200, st) } -func get(req torrReqJS, c *gin.Context) { +func getTorrent(req torrReqJS, c *gin.Context) { hash := metainfo.NewHashFromHex(req.Hash) tor := bts.GetTorrent(hash) if tor == nil { @@ -85,17 +107,22 @@ func get(req torrReqJS, c *gin.Context) { } } -func rem(req torrReqJS, c *gin.Context) { +func remTorrent(req torrReqJS, c *gin.Context) { hash := metainfo.NewHashFromHex(req.Hash) bts.RemoveTorrent(hash) utils.RemTorrent(hash) c.Status(200) } -func list(req torrReqJS, c *gin.Context) { +func listTorrent(req torrReqJS, c *gin.Context) { + stats := listTorrents() + c.JSON(200, stats) +} + +func listTorrents() []*state.TorrentStats { btlist := bts.ListTorrents() dblist := utils.ListTorrents() - var stats []torr.TorrentStats + var stats []*state.TorrentStats for _, tr := range btlist { stats = append(stats, tr.Stats()) } @@ -109,6 +136,16 @@ mainloop: } stats = append(stats, db.Stats()) } - - c.JSON(200, stats) + return stats +} + +func dropTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + hash := metainfo.NewHashFromHex(req.Hash) + + bts.RemoveTorrent(hash) + c.Status(200) } diff --git a/src/server/web/api/viewed.go b/src/server/web/api/viewed.go new file mode 100644 index 0000000..c882eb0 --- /dev/null +++ b/src/server/web/api/viewed.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + sets "server/settings" +) + +// 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) +}