Compare commits

..

6 Commits

Author SHA1 Message Date
2f0497ebc6 Get the movie poster with a get details
All checks were successful
continuous-integration/drone/push Build is passing
2020-04-11 19:07:11 +02:00
5b68ddb098 Movie the torrent progress bar in a new component 2020-04-11 18:28:44 +02:00
0aa3b6fc59 Move torrent list components in separate files 2020-04-11 18:17:33 +02:00
3d7b663f97 Add a pretty name to the listed torrents 2020-04-11 18:10:56 +02:00
f8db5e1211 Fetch the show title while fetching an episode
All checks were successful
continuous-integration/drone/push Build is passing
2020-04-11 17:45:33 +02:00
e41c9bfdfa Return the video details embedded in the torrents
This requires the eventers to have the app env
2020-04-11 17:45:33 +02:00
21 changed files with 111 additions and 257 deletions

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"git.quimbo.fr/odwrtw/canape/backend/auth"
@ -25,16 +24,18 @@ const (
// Eventers is a global map of all the available Eventers
var Eventers map[string]*PolochonEventers
var once sync.Once
var eventersSetup bool
func initEventers(env *web.Env) {
once.Do(func() {
env.Log.Infof("Initialising eventers")
if eventersSetup {
return
}
Eventers = map[string]*PolochonEventers{
torrentEventName: NewTorrentEventers(env),
videoEventName: NewVideoEventers(env),
}
})
eventersSetup = true
}
// WsHandler handles the websockets messages

View File

@ -15,10 +15,7 @@ type TorrentEventer struct {
*BaseEventer
done chan struct{}
pClient *papi.Client
// previous keep the previous data
previous []*papi.Torrent
// data holds the computed data
data []*models.TorrentVideo
torrents []papi.Torrent
}
var torrentEventName = "torrents"
@ -48,6 +45,7 @@ func NewTorrentEventer(env *web.Env, polo *models.Polochon) (Eventer, error) {
},
pClient: client,
done: make(chan struct{}),
torrents: []papi.Torrent{},
}
return tn, nil
@ -62,7 +60,7 @@ func (t *TorrentEventer) Append(chanl *Channel) {
Type: torrentEventName,
Status: OK,
},
Data: t.data,
Data: t.torrents,
}
chanl.sendEvent(event)
@ -98,23 +96,27 @@ func (t *TorrentEventer) Launch() error {
// torrentsUpdate sends to the eventStream if torrents change
func (t *TorrentEventer) torrentsUpdate() error {
// Get torrents
torrents, err := t.pClient.GetTorrents()
if err != nil {
return err
}
if reflect.DeepEqual(t.previous, torrents) {
if reflect.DeepEqual(t.torrents, torrents) {
return nil
}
t.env.Log.Debugf("torrents have changed!")
data := models.NewTorrentVideos(t.env.Backend.Detailer, t.env.Database, t.env.Log, torrents)
notification := make([]*models.TorrentVideo, len(torrents))
for i := range torrents {
notification[i] = models.NewTorrentVideo(&torrents[i])
notification[i].Update(t.env.Backend.Detailer, t.env.Log)
}
t.NotifyAll(data)
t.NotifyAll(notification)
t.previous = torrents
t.data = data
t.torrents = torrents
return nil
}

View File

@ -51,9 +51,9 @@ func run() error {
}
backend := &models.Backend{
Database: db,
PublicDir: cf.PublicDir,
ImgURLPrefix: cf.ImgURLPrefix,
}
models.SetPublicDir(cf.PublicDir)
models.SetImgURLPrefix(cf.ImgURLPrefix)
// Generate auth params
authParams := auth.Params{

View File

@ -8,6 +8,8 @@ import (
// Backend represents the data backend
type Backend struct {
Database *sqlx.DB
PublicDir string
ImgURLPrefix string
configured bool
}

View File

@ -2,6 +2,9 @@ package models
import (
"errors"
"fmt"
"os"
"path/filepath"
polochon "github.com/odwrtw/polochon/lib"
"github.com/sirupsen/logrus"
@ -28,6 +31,15 @@ func (b *Backend) GetMovieDetails(pMovie *polochon.Movie, log *logrus.Entry) 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)
return nil
@ -47,7 +59,6 @@ func (b *Backend) GetShowDetails(pShow *polochon.Show, log *logrus.Entry) error
log.Warnf("error while getting episodes: %s", err)
return err
}
return nil
}

View File

@ -2,7 +2,6 @@ package models
import (
"database/sql"
"fmt"
"time"
"github.com/jmoiron/sqlx"
@ -87,12 +86,6 @@ func FillEpisodeFromDB(eDB *episodeDB, pEpisode *polochon.ShowEpisode) {
pEpisode.Thumb = eDB.Thumb
pEpisode.Runtime = eDB.Runtime
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

View File

@ -1,30 +0,0 @@
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 ""
}

View File

@ -89,7 +89,6 @@ func FillMovieFromDB(mDB *movieDB, pMovie *polochon.Movie) {
pMovie.Genres = mDB.Genres
pMovie.SortTitle = mDB.SortTitle
pMovie.Tagline = mDB.Tagline
pMovie.Thumb = imageURL("movies/" + mDB.ImdbID + ".jpg")
}
// updateFromMovie will update the movieDB from a Movie

View File

@ -42,7 +42,7 @@ type showDB struct {
// UpsertShow a show in the database
func UpsertShow(db *sqlx.DB, s *polochon.Show) error {
sDB := newShowFromPolochon(s)
sDB := NewShowFromPolochon(s)
// Upsert the show
r, err := db.NamedQuery(upsertShowQuery, sDB)
if err != nil {
@ -60,8 +60,8 @@ func UpsertShow(db *sqlx.DB, s *polochon.Show) error {
return nil
}
// newShowFromPolochon returns an showDB from a polochon Show
func newShowFromPolochon(s *polochon.Show) *showDB {
// NewShowFromPolochon returns an showDB from a polochon Show
func NewShowFromPolochon(s *polochon.Show) *showDB {
sDB := showDB{
ImdbID: s.ImdbID,
TvdbID: s.TvdbID,
@ -103,7 +103,4 @@ func FillShowFromDB(sDB *showDB, pShow *polochon.Show) {
pShow.TvdbID = sDB.TvdbID
pShow.Year = sDB.Year
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")
}

View File

@ -1,7 +1,6 @@
package models
import (
"github.com/jmoiron/sqlx"
"github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib"
"github.com/sirupsen/logrus"
@ -10,7 +9,6 @@ import (
// TorrentVideo reprensents a torrent embeding the video inforamtions
type TorrentVideo struct {
*papi.Torrent
Img string `json:"img"`
Video polochon.Video `json:"video,omitempty"`
}
@ -31,7 +29,7 @@ func NewTorrentVideo(t *papi.Torrent) *TorrentVideo {
}
// Update updates the Torrent video with the database details
func (t *TorrentVideo) Update(detailer polochon.Detailer, db *sqlx.DB, log *logrus.Entry) {
func (t *TorrentVideo) Update(detailer polochon.Detailer, log *logrus.Entry) {
if t.Video == nil {
return
}
@ -41,28 +39,4 @@ func (t *TorrentVideo) Update(detailer polochon.Detailer, db *sqlx.DB, log *logr
if err != nil {
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
}

View File

@ -82,12 +82,16 @@ func (e *Episode) MarshalJSON() ([]byte, error) {
AudioCodec: audioCodec,
VideoCodec: videoCodec,
Container: container,
Thumb: e.Thumb,
Thumb: e.getThumbURL(),
}
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
func NewEpisode(show *Show, season, episode int) *Episode {
return &Episode{

View File

@ -38,9 +38,9 @@ func (s *Show) MarshalJSON() ([]byte, error) {
PosterURL string `json:"poster_url"`
}{
alias: (*alias)(s),
BannerURL: s.Banner,
FanartURL: s.Fanart,
PosterURL: s.Poster,
BannerURL: s.GetImageURL("banner"),
FanartURL: s.GetImageURL("fanart"),
PosterURL: s.GetImageURL("poster"),
}
// Create Episode obj from polochon.Episodes and add them to the object to

View File

@ -52,7 +52,13 @@ func ListHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, err)
}
torrents := models.NewTorrentVideos(env.Backend.Detailer, env.Database, env.Log, list)
torrents := make([]*models.TorrentVideo, len(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)
}

View File

@ -170,7 +170,7 @@ const TorrentsDropdown = () => {
};
const TorrentsDropdownTitle = () => {
const count = useSelector((state) => state.torrents.count);
const count = useSelector((state) => state.torrents.torrents.length);
if (count === 0) {
return <span>Torrents</span>;
}

View File

@ -1,17 +0,0 @@
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,
};

View File

@ -20,7 +20,9 @@ export const Progress = ({ torrent }) => {
const totalSize = prettySize(torrent.status.total_size);
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
return (
<div>
<div className="card-body pb-0">
{started && (
<>
<div className="progress bg-light">
<div
className={progressBarClass}
@ -31,16 +33,12 @@ export const Progress = ({ torrent }) => {
aria-valuemax="100"
></div>
</div>
{started && (
<p>
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
</p>
</>
)}
{!started && (
<p>
<small>Not yet started</small>
</p>
)}
{!started && <p>Download not yet started</p>}
</div>
);
};

View File

@ -2,7 +2,7 @@ import React from "react";
import PropTypes from "prop-types";
import { useDispatch } from "react-redux";
import { prettyEpisodeNameWithoutShow } from "../../../utils";
import { prettyEpisodeName } from "../../../utils";
import { removeTorrent } from "../../../actions/torrents";
import { Progress } from "./progress";
@ -10,31 +10,32 @@ import { Progress } from "./progress";
export const Torrent = ({ torrent }) => {
const dispatch = useDispatch();
const title = (torrent) => {
if (torrent.type !== "episode" || !torrent.video) {
return "";
}
return (
prettyEpisodeNameWithoutShow(
const torrentTitle = (torrent) => {
switch (torrent.type) {
case "movie":
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 (
<div className="border-top">
<div className="card-text d-flex flex-row">
<div className="mt-1 flex-fill">
<span className="text text-break">{title(torrent)}</span>
<small className="text-muted text-break">{torrent.status.name}</small>
</div>
<div
className="fa fa-trash btn text-right"
<div className="card w-100 mb-3">
<h5 className="card-header">
<span className="text text-break">{torrentTitle(torrent)}</span>
<span
className="fa fa-trash clickable pull-right"
onClick={() => dispatch(removeTorrent(torrent.status.id))}
/>
</div>
></span>
</h5>
<Progress torrent={torrent} />
</div>
);

View File

@ -1,49 +0,0 @@
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,
};

View File

@ -1,14 +1,12 @@
import React from "react";
import { useSelector } from "react-redux";
import { TorrentGroup } from "./torrentGroup";
import { Torrent } from "./torrent";
export const Torrents = () => {
const torrentsKeys = useSelector((state) =>
Array.from(state.torrents.torrents.keys())
);
const torrents = useSelector((state) => state.torrents.torrents);
if (torrentsKeys.length === 0) {
if (torrents.length === 0) {
return (
<div className="jumbotron">
<h2>No torrents</h2>
@ -18,8 +16,8 @@ export const Torrents = () => {
return (
<div className="d-flex flex-wrap">
{torrentsKeys.map((key) => (
<TorrentGroup key={key} torrentKey={key} />
{torrents.map((torrent, index) => (
<Torrent key={index} torrent={torrent} />
))}
</div>
);

View File

@ -3,42 +3,10 @@ import { produce } from "immer";
const defaultState = {
fetching: false,
searching: false,
torrents: new Map(),
count: 0,
torrents: [],
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) =>
produce(state, (draft) => {
switch (action.type) {
@ -48,8 +16,7 @@ export default (state = defaultState, action) =>
case "TORRENTS_FETCH_FULFILLED":
draft.fetching = false;
draft.torrents = formatTorrents(action.payload.response.data);
draft.count = action.payload.response.data.length;
draft.torrents = action.payload.response.data;
break;
case "TORRENTS_SEARCH_PENDING":

View File

@ -18,9 +18,6 @@ export const prettyDurationFromMinutes = (runtime) => {
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) =>
`${showName} S${pad(season)}E${pad(episode)}`;