Compare commits
6 Commits
2f0497ebc6
...
8144cabc76
Author | SHA1 | Date | |
---|---|---|---|
8144cabc76 | |||
57b70eb585 | |||
8e8a1dc3e9 | |||
61bfd9eee6 | |||
ab3cf82fa9 | |||
6a946d137d |
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.quimbo.fr/odwrtw/canape/backend/auth"
|
"git.quimbo.fr/odwrtw/canape/backend/auth"
|
||||||
@ -24,18 +25,16 @@ const (
|
|||||||
|
|
||||||
// Eventers is a global map of all the available Eventers
|
// Eventers is a global map of all the available Eventers
|
||||||
var Eventers map[string]*PolochonEventers
|
var Eventers map[string]*PolochonEventers
|
||||||
var eventersSetup bool
|
var once sync.Once
|
||||||
|
|
||||||
func initEventers(env *web.Env) {
|
func initEventers(env *web.Env) {
|
||||||
if eventersSetup {
|
once.Do(func() {
|
||||||
return
|
env.Log.Infof("Initialising eventers")
|
||||||
}
|
Eventers = map[string]*PolochonEventers{
|
||||||
|
torrentEventName: NewTorrentEventers(env),
|
||||||
Eventers = map[string]*PolochonEventers{
|
videoEventName: NewVideoEventers(env),
|
||||||
torrentEventName: NewTorrentEventers(env),
|
}
|
||||||
videoEventName: NewVideoEventers(env),
|
})
|
||||||
}
|
|
||||||
eventersSetup = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WsHandler handles the websockets messages
|
// WsHandler handles the websockets messages
|
||||||
|
@ -13,9 +13,12 @@ import (
|
|||||||
// TorrentEventer represents the Eventer for torrents
|
// TorrentEventer represents the Eventer for torrents
|
||||||
type TorrentEventer struct {
|
type TorrentEventer struct {
|
||||||
*BaseEventer
|
*BaseEventer
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
pClient *papi.Client
|
pClient *papi.Client
|
||||||
torrents []papi.Torrent
|
// previous keep the previous data
|
||||||
|
previous []*papi.Torrent
|
||||||
|
// data holds the computed data
|
||||||
|
data []*models.TorrentVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
var torrentEventName = "torrents"
|
var torrentEventName = "torrents"
|
||||||
@ -43,9 +46,8 @@ func NewTorrentEventer(env *web.Env, polo *models.Polochon) (Eventer, error) {
|
|||||||
users: []*Channel{},
|
users: []*Channel{},
|
||||||
name: torrentEventName,
|
name: torrentEventName,
|
||||||
},
|
},
|
||||||
pClient: client,
|
pClient: client,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
torrents: []papi.Torrent{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tn, nil
|
return tn, nil
|
||||||
@ -60,7 +62,7 @@ func (t *TorrentEventer) Append(chanl *Channel) {
|
|||||||
Type: torrentEventName,
|
Type: torrentEventName,
|
||||||
Status: OK,
|
Status: OK,
|
||||||
},
|
},
|
||||||
Data: t.torrents,
|
Data: t.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
chanl.sendEvent(event)
|
chanl.sendEvent(event)
|
||||||
@ -96,27 +98,23 @@ func (t *TorrentEventer) Launch() error {
|
|||||||
|
|
||||||
// torrentsUpdate sends to the eventStream if torrents change
|
// torrentsUpdate sends to the eventStream if torrents change
|
||||||
func (t *TorrentEventer) torrentsUpdate() error {
|
func (t *TorrentEventer) torrentsUpdate() error {
|
||||||
// Get torrents
|
|
||||||
torrents, err := t.pClient.GetTorrents()
|
torrents, err := t.pClient.GetTorrents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if reflect.DeepEqual(t.torrents, torrents) {
|
if reflect.DeepEqual(t.previous, torrents) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
t.env.Log.Debugf("torrents have changed!")
|
t.env.Log.Debugf("torrents have changed!")
|
||||||
|
|
||||||
notification := make([]*models.TorrentVideo, len(torrents))
|
data := models.NewTorrentVideos(t.env.Backend.Detailer, t.env.Database, t.env.Log, torrents)
|
||||||
for i := range torrents {
|
|
||||||
notification[i] = models.NewTorrentVideo(&torrents[i])
|
|
||||||
notification[i].Update(t.env.Backend.Detailer, t.env.Log)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.NotifyAll(notification)
|
t.NotifyAll(data)
|
||||||
|
|
||||||
t.torrents = torrents
|
t.previous = torrents
|
||||||
|
t.data = data
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -50,10 +50,10 @@ func run() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
backend := &models.Backend{
|
backend := &models.Backend{
|
||||||
Database: db,
|
Database: db,
|
||||||
PublicDir: cf.PublicDir,
|
|
||||||
ImgURLPrefix: cf.ImgURLPrefix,
|
|
||||||
}
|
}
|
||||||
|
models.SetPublicDir(cf.PublicDir)
|
||||||
|
models.SetImgURLPrefix(cf.ImgURLPrefix)
|
||||||
|
|
||||||
// Generate auth params
|
// Generate auth params
|
||||||
authParams := auth.Params{
|
authParams := auth.Params{
|
||||||
|
@ -7,10 +7,8 @@ import (
|
|||||||
|
|
||||||
// Backend represents the data backend
|
// Backend represents the data backend
|
||||||
type Backend struct {
|
type Backend struct {
|
||||||
Database *sqlx.DB
|
Database *sqlx.DB
|
||||||
PublicDir string
|
configured bool
|
||||||
ImgURLPrefix string
|
|
||||||
configured bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name implements the Module interface
|
// Name implements the Module interface
|
||||||
|
@ -2,9 +2,6 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
polochon "github.com/odwrtw/polochon/lib"
|
polochon "github.com/odwrtw/polochon/lib"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -31,15 +28,6 @@ func (b *Backend) GetMovieDetails(pMovie *polochon.Movie, log *logrus.Entry) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the movie images
|
|
||||||
imgURL := fmt.Sprintf("movies/%s.jpg", pMovie.ImdbID)
|
|
||||||
imgFile := filepath.Join(b.PublicDir, "img", imgURL)
|
|
||||||
posterURL := ""
|
|
||||||
if _, err := os.Stat(imgFile); !os.IsNotExist(err) {
|
|
||||||
posterURL = b.ImgURLPrefix + imgURL
|
|
||||||
}
|
|
||||||
pMovie.Thumb = posterURL
|
|
||||||
|
|
||||||
log.Debugf("got movie %s from backend", pMovie.ImdbID)
|
log.Debugf("got movie %s from backend", pMovie.ImdbID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -59,6 +47,7 @@ func (b *Backend) GetShowDetails(pShow *polochon.Show, log *logrus.Entry) error
|
|||||||
log.Warnf("error while getting episodes: %s", err)
|
log.Warnf("error while getting episodes: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
@ -86,6 +87,12 @@ func FillEpisodeFromDB(eDB *episodeDB, pEpisode *polochon.ShowEpisode) {
|
|||||||
pEpisode.Thumb = eDB.Thumb
|
pEpisode.Thumb = eDB.Thumb
|
||||||
pEpisode.Runtime = eDB.Runtime
|
pEpisode.Runtime = eDB.Runtime
|
||||||
pEpisode.Aired = eDB.Aired
|
pEpisode.Aired = eDB.Aired
|
||||||
|
pEpisode.Thumb = imageURL(fmt.Sprintf(
|
||||||
|
"shows/%s/%d-%d.jpg",
|
||||||
|
eDB.ShowImdbID,
|
||||||
|
eDB.Season,
|
||||||
|
eDB.Episode,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEpisode gets an episode and fills the polochon episode
|
// GetEpisode gets an episode and fills the polochon episode
|
||||||
|
30
backend/models/global.go
Normal file
30
backend/models/global.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Public gobal variables
|
||||||
|
var (
|
||||||
|
PublicDir string
|
||||||
|
ImgURLPrefix string
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetPublicDir sets the public dir
|
||||||
|
func SetPublicDir(s string) {
|
||||||
|
PublicDir = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetImgURLPrefix sets the img url prefix
|
||||||
|
func SetImgURLPrefix(s string) {
|
||||||
|
ImgURLPrefix = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageURL(suffix string) string {
|
||||||
|
imgFile := filepath.Join(PublicDir, "img", suffix)
|
||||||
|
if _, err := os.Stat(imgFile); !os.IsNotExist(err) {
|
||||||
|
return ImgURLPrefix + suffix
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
@ -89,6 +89,7 @@ func FillMovieFromDB(mDB *movieDB, pMovie *polochon.Movie) {
|
|||||||
pMovie.Genres = mDB.Genres
|
pMovie.Genres = mDB.Genres
|
||||||
pMovie.SortTitle = mDB.SortTitle
|
pMovie.SortTitle = mDB.SortTitle
|
||||||
pMovie.Tagline = mDB.Tagline
|
pMovie.Tagline = mDB.Tagline
|
||||||
|
pMovie.Thumb = imageURL("movies/" + mDB.ImdbID + ".jpg")
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateFromMovie will update the movieDB from a Movie
|
// updateFromMovie will update the movieDB from a Movie
|
||||||
|
@ -42,7 +42,7 @@ type showDB struct {
|
|||||||
|
|
||||||
// UpsertShow a show in the database
|
// UpsertShow a show in the database
|
||||||
func UpsertShow(db *sqlx.DB, s *polochon.Show) error {
|
func UpsertShow(db *sqlx.DB, s *polochon.Show) error {
|
||||||
sDB := NewShowFromPolochon(s)
|
sDB := newShowFromPolochon(s)
|
||||||
// Upsert the show
|
// Upsert the show
|
||||||
r, err := db.NamedQuery(upsertShowQuery, sDB)
|
r, err := db.NamedQuery(upsertShowQuery, sDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,8 +60,8 @@ func UpsertShow(db *sqlx.DB, s *polochon.Show) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShowFromPolochon returns an showDB from a polochon Show
|
// newShowFromPolochon returns an showDB from a polochon Show
|
||||||
func NewShowFromPolochon(s *polochon.Show) *showDB {
|
func newShowFromPolochon(s *polochon.Show) *showDB {
|
||||||
sDB := showDB{
|
sDB := showDB{
|
||||||
ImdbID: s.ImdbID,
|
ImdbID: s.ImdbID,
|
||||||
TvdbID: s.TvdbID,
|
TvdbID: s.TvdbID,
|
||||||
@ -103,4 +103,7 @@ func FillShowFromDB(sDB *showDB, pShow *polochon.Show) {
|
|||||||
pShow.TvdbID = sDB.TvdbID
|
pShow.TvdbID = sDB.TvdbID
|
||||||
pShow.Year = sDB.Year
|
pShow.Year = sDB.Year
|
||||||
pShow.FirstAired = &sDB.FirstAired
|
pShow.FirstAired = &sDB.FirstAired
|
||||||
|
pShow.Banner = imageURL("shows/" + sDB.ImdbID + "/banner.jpg")
|
||||||
|
pShow.Fanart = imageURL("shows/" + sDB.ImdbID + "/fanart.jpg")
|
||||||
|
pShow.Poster = imageURL("shows/" + sDB.ImdbID + "/poster.jpg")
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/odwrtw/papi"
|
"github.com/odwrtw/papi"
|
||||||
polochon "github.com/odwrtw/polochon/lib"
|
polochon "github.com/odwrtw/polochon/lib"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -9,6 +10,7 @@ import (
|
|||||||
// TorrentVideo reprensents a torrent embeding the video inforamtions
|
// TorrentVideo reprensents a torrent embeding the video inforamtions
|
||||||
type TorrentVideo struct {
|
type TorrentVideo struct {
|
||||||
*papi.Torrent
|
*papi.Torrent
|
||||||
|
Img string `json:"img"`
|
||||||
Video polochon.Video `json:"video,omitempty"`
|
Video polochon.Video `json:"video,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ func NewTorrentVideo(t *papi.Torrent) *TorrentVideo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the Torrent video with the database details
|
// Update updates the Torrent video with the database details
|
||||||
func (t *TorrentVideo) Update(detailer polochon.Detailer, log *logrus.Entry) {
|
func (t *TorrentVideo) Update(detailer polochon.Detailer, db *sqlx.DB, log *logrus.Entry) {
|
||||||
if t.Video == nil {
|
if t.Video == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -39,4 +41,28 @@ func (t *TorrentVideo) Update(detailer polochon.Detailer, log *logrus.Entry) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("function", "TorrentVideo.Update").Errorf(err.Error())
|
log.WithField("function", "TorrentVideo.Update").Errorf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch v := t.Video.(type) {
|
||||||
|
case *polochon.ShowEpisode:
|
||||||
|
if v.Show != nil {
|
||||||
|
if err := GetShow(db, v.Show); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Img = v.Show.Poster
|
||||||
|
v.Show = nil
|
||||||
|
}
|
||||||
|
case *polochon.Movie:
|
||||||
|
t.Img = v.Thumb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTorrentVideos returns a new slice of TorrentVideo from papi torrents
|
||||||
|
func NewTorrentVideos(detailer polochon.Detailer, db *sqlx.DB, log *logrus.Entry, torrents []*papi.Torrent) []*TorrentVideo {
|
||||||
|
tv := make([]*TorrentVideo, len(torrents))
|
||||||
|
for i := range torrents {
|
||||||
|
tv[i] = NewTorrentVideo(torrents[i])
|
||||||
|
tv[i].Update(detailer, db, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tv
|
||||||
}
|
}
|
||||||
|
@ -82,16 +82,12 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
|
|||||||
AudioCodec: audioCodec,
|
AudioCodec: audioCodec,
|
||||||
VideoCodec: videoCodec,
|
VideoCodec: videoCodec,
|
||||||
Container: container,
|
Container: container,
|
||||||
Thumb: e.getThumbURL(),
|
Thumb: e.Thumb,
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(episodeToMarshal)
|
return json.Marshal(episodeToMarshal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Episode) getThumbURL() string {
|
|
||||||
return e.show.GetImageURL(fmt.Sprintf("%d-%d", e.Season, e.Episode))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEpisode returns an Episode
|
// NewEpisode returns an Episode
|
||||||
func NewEpisode(show *Show, season, episode int) *Episode {
|
func NewEpisode(show *Show, season, episode int) *Episode {
|
||||||
return &Episode{
|
return &Episode{
|
||||||
|
@ -38,9 +38,9 @@ func (s *Show) MarshalJSON() ([]byte, error) {
|
|||||||
PosterURL string `json:"poster_url"`
|
PosterURL string `json:"poster_url"`
|
||||||
}{
|
}{
|
||||||
alias: (*alias)(s),
|
alias: (*alias)(s),
|
||||||
BannerURL: s.GetImageURL("banner"),
|
BannerURL: s.Banner,
|
||||||
FanartURL: s.GetImageURL("fanart"),
|
FanartURL: s.Fanart,
|
||||||
PosterURL: s.GetImageURL("poster"),
|
PosterURL: s.Poster,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Episode obj from polochon.Episodes and add them to the object to
|
// Create Episode obj from polochon.Episodes and add them to the object to
|
||||||
|
@ -52,13 +52,7 @@ func ListHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
|
|||||||
return env.RenderError(w, err)
|
return env.RenderError(w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
torrents := make([]*models.TorrentVideo, len(list))
|
torrents := models.NewTorrentVideos(env.Backend.Detailer, env.Database, env.Log, list)
|
||||||
for i, t := range list {
|
|
||||||
tv := models.NewTorrentVideo(&t)
|
|
||||||
tv.Update(env.Backend.Detailer, env.Log)
|
|
||||||
torrents[i] = tv
|
|
||||||
}
|
|
||||||
|
|
||||||
return env.RenderJSON(w, torrents)
|
return env.RenderJSON(w, torrents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ const TorrentsDropdown = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TorrentsDropdownTitle = () => {
|
const TorrentsDropdownTitle = () => {
|
||||||
const count = useSelector((state) => state.torrents.torrents.length);
|
const count = useSelector((state) => state.torrents.count);
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
return <span>Torrents</span>;
|
return <span>Torrents</span>;
|
||||||
}
|
}
|
||||||
|
17
frontend/js/components/torrents/list/poster.js
Normal file
17
frontend/js/components/torrents/list/poster.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export const Poster = ({ url }) => {
|
||||||
|
if (!url || url === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-md-2 d-none d-md-block">
|
||||||
|
<img className="card-img" src={url} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Poster.propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
};
|
@ -20,25 +20,27 @@ export const Progress = ({ torrent }) => {
|
|||||||
const totalSize = prettySize(torrent.status.total_size);
|
const totalSize = prettySize(torrent.status.total_size);
|
||||||
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
|
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
|
||||||
return (
|
return (
|
||||||
<div className="card-body pb-0">
|
<div>
|
||||||
|
<div className="progress bg-light">
|
||||||
|
<div
|
||||||
|
className={progressBarClass}
|
||||||
|
style={{ width: percentDone }}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={percentDone}
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
{started && (
|
{started && (
|
||||||
<>
|
<p>
|
||||||
<div className="progress bg-light">
|
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
|
||||||
<div
|
</p>
|
||||||
className={progressBarClass}
|
)}
|
||||||
style={{ width: percentDone }}
|
{!started && (
|
||||||
role="progressbar"
|
<p>
|
||||||
aria-valuenow={percentDone}
|
<small>Not yet started</small>
|
||||||
aria-valuemin="0"
|
</p>
|
||||||
aria-valuemax="100"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{!started && <p>Download not yet started</p>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
import { prettyEpisodeName } from "../../../utils";
|
import { prettyEpisodeNameWithoutShow } from "../../../utils";
|
||||||
import { removeTorrent } from "../../../actions/torrents";
|
import { removeTorrent } from "../../../actions/torrents";
|
||||||
|
|
||||||
import { Progress } from "./progress";
|
import { Progress } from "./progress";
|
||||||
@ -10,32 +10,31 @@ import { Progress } from "./progress";
|
|||||||
export const Torrent = ({ torrent }) => {
|
export const Torrent = ({ torrent }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const torrentTitle = (torrent) => {
|
const title = (torrent) => {
|
||||||
switch (torrent.type) {
|
if (torrent.type !== "episode" || !torrent.video) {
|
||||||
case "movie":
|
return "";
|
||||||
return torrent.video ? torrent.video.title : torrent.status.name;
|
|
||||||
case "episode":
|
|
||||||
return torrent.video
|
|
||||||
? prettyEpisodeName(
|
|
||||||
torrent.video.show_title,
|
|
||||||
torrent.video.season,
|
|
||||||
torrent.video.episode
|
|
||||||
)
|
|
||||||
: torrent.status.name;
|
|
||||||
default:
|
|
||||||
return torrent.status.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
prettyEpisodeNameWithoutShow(
|
||||||
|
torrent.video.season,
|
||||||
|
torrent.video.episode
|
||||||
|
) + " - "
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card w-100 mb-3">
|
<div className="border-top">
|
||||||
<h5 className="card-header">
|
<div className="card-text d-flex flex-row">
|
||||||
<span className="text text-break">{torrentTitle(torrent)}</span>
|
<div className="mt-1 flex-fill">
|
||||||
<span
|
<span className="text text-break">{title(torrent)}</span>
|
||||||
className="fa fa-trash clickable pull-right"
|
<small className="text-muted text-break">{torrent.status.name}</small>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="fa fa-trash btn text-right"
|
||||||
onClick={() => dispatch(removeTorrent(torrent.status.id))}
|
onClick={() => dispatch(removeTorrent(torrent.status.id))}
|
||||||
></span>
|
/>
|
||||||
</h5>
|
</div>
|
||||||
<Progress torrent={torrent} />
|
<Progress torrent={torrent} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
49
frontend/js/components/torrents/list/torrentGroup.js
Normal file
49
frontend/js/components/torrents/list/torrentGroup.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { Torrent } from "./torrent";
|
||||||
|
import { Poster } from "./poster";
|
||||||
|
|
||||||
|
export const TorrentGroup = ({ torrentKey }) => {
|
||||||
|
const torrents = useSelector((state) =>
|
||||||
|
state.torrents.torrents.get(torrentKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (torrents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = (torrent) => {
|
||||||
|
switch (torrent.type) {
|
||||||
|
case "movie":
|
||||||
|
return torrent.video.title;
|
||||||
|
case "episode":
|
||||||
|
return torrent.video.show_title;
|
||||||
|
default:
|
||||||
|
return "Files";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-100 mb-3 card">
|
||||||
|
<div className="row no-gutters">
|
||||||
|
<Poster url={torrents[0].img} />
|
||||||
|
<div className="col-sm">
|
||||||
|
<div className="card-body">
|
||||||
|
<h4 className="card-title">{title(torrents[0])}</h4>
|
||||||
|
{torrents.map((torrent, i) => (
|
||||||
|
<Torrent
|
||||||
|
key={torrent.video ? torrent.video.imdb_id : i}
|
||||||
|
torrent={torrent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
TorrentGroup.propTypes = {
|
||||||
|
torrentKey: PropTypes.string.isRequired,
|
||||||
|
};
|
@ -1,12 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import { Torrent } from "./torrent";
|
import { TorrentGroup } from "./torrentGroup";
|
||||||
|
|
||||||
export const Torrents = () => {
|
export const Torrents = () => {
|
||||||
const torrents = useSelector((state) => state.torrents.torrents);
|
const torrentsKeys = useSelector((state) =>
|
||||||
|
Array.from(state.torrents.torrents.keys())
|
||||||
|
);
|
||||||
|
|
||||||
if (torrents.length === 0) {
|
if (torrentsKeys.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="jumbotron">
|
<div className="jumbotron">
|
||||||
<h2>No torrents</h2>
|
<h2>No torrents</h2>
|
||||||
@ -16,8 +18,8 @@ export const Torrents = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex flex-wrap">
|
<div className="d-flex flex-wrap">
|
||||||
{torrents.map((torrent, index) => (
|
{torrentsKeys.map((key) => (
|
||||||
<Torrent key={index} torrent={torrent} />
|
<TorrentGroup key={key} torrentKey={key} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,10 +3,42 @@ import { produce } from "immer";
|
|||||||
const defaultState = {
|
const defaultState = {
|
||||||
fetching: false,
|
fetching: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
torrents: [],
|
torrents: new Map(),
|
||||||
|
count: 0,
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Group the torrents by imdb id
|
||||||
|
const formatTorrents = (input) => {
|
||||||
|
let torrents = new Map();
|
||||||
|
if (!input) {
|
||||||
|
return torrents;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.forEach((t) => {
|
||||||
|
let key;
|
||||||
|
switch (t.type) {
|
||||||
|
case "movie":
|
||||||
|
key = t.video ? t.video.imdb_id : "unknown";
|
||||||
|
break;
|
||||||
|
case "episode":
|
||||||
|
key = t.video ? t.video.show_imdb_id : "unknown";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key = "unknown";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrents.has(key)) {
|
||||||
|
torrents.set(key, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
torrents.get(key).push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
return torrents;
|
||||||
|
};
|
||||||
|
|
||||||
export default (state = defaultState, action) =>
|
export default (state = defaultState, action) =>
|
||||||
produce(state, (draft) => {
|
produce(state, (draft) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -16,7 +48,8 @@ export default (state = defaultState, action) =>
|
|||||||
|
|
||||||
case "TORRENTS_FETCH_FULFILLED":
|
case "TORRENTS_FETCH_FULFILLED":
|
||||||
draft.fetching = false;
|
draft.fetching = false;
|
||||||
draft.torrents = action.payload.response.data;
|
draft.torrents = formatTorrents(action.payload.response.data);
|
||||||
|
draft.count = action.payload.response.data.length;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "TORRENTS_SEARCH_PENDING":
|
case "TORRENTS_SEARCH_PENDING":
|
||||||
|
@ -18,6 +18,9 @@ export const prettyDurationFromMinutes = (runtime) => {
|
|||||||
|
|
||||||
const pad = (d) => (d < 10 ? "0" + d.toString() : d.toString());
|
const pad = (d) => (d < 10 ? "0" + d.toString() : d.toString());
|
||||||
|
|
||||||
|
export const prettyEpisodeNameWithoutShow = (season, episode) =>
|
||||||
|
`S${pad(season)}E${pad(episode)}`;
|
||||||
|
|
||||||
export const prettyEpisodeName = (showName, season, episode) =>
|
export const prettyEpisodeName = (showName, season, episode) =>
|
||||||
`${showName} S${pad(season)}E${pad(episode)}`;
|
`${showName} S${pad(season)}E${pad(episode)}`;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user