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" "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"
@ -25,16 +24,18 @@ 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 once sync.Once var eventersSetup bool
func initEventers(env *web.Env) { func initEventers(env *web.Env) {
once.Do(func() { if eventersSetup {
env.Log.Infof("Initialising eventers") return
}
Eventers = map[string]*PolochonEventers{ Eventers = map[string]*PolochonEventers{
torrentEventName: NewTorrentEventers(env), torrentEventName: NewTorrentEventers(env),
videoEventName: NewVideoEventers(env), videoEventName: NewVideoEventers(env),
} }
}) eventersSetup = true
} }
// WsHandler handles the websockets messages // WsHandler handles the websockets messages

View File

@ -15,10 +15,7 @@ type TorrentEventer struct {
*BaseEventer *BaseEventer
done chan struct{} done chan struct{}
pClient *papi.Client pClient *papi.Client
// previous keep the previous data torrents []papi.Torrent
previous []*papi.Torrent
// data holds the computed data
data []*models.TorrentVideo
} }
var torrentEventName = "torrents" var torrentEventName = "torrents"
@ -48,6 +45,7 @@ func NewTorrentEventer(env *web.Env, polo *models.Polochon) (Eventer, error) {
}, },
pClient: client, pClient: client,
done: make(chan struct{}), done: make(chan struct{}),
torrents: []papi.Torrent{},
} }
return tn, nil return tn, nil
@ -62,7 +60,7 @@ func (t *TorrentEventer) Append(chanl *Channel) {
Type: torrentEventName, Type: torrentEventName,
Status: OK, Status: OK,
}, },
Data: t.data, Data: t.torrents,
} }
chanl.sendEvent(event) chanl.sendEvent(event)
@ -98,23 +96,27 @@ 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.previous, torrents) { if reflect.DeepEqual(t.torrents, torrents) {
return nil return nil
} }
t.env.Log.Debugf("torrents have changed!") 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.torrents = torrents
t.data = data
return nil return nil
} }

View File

@ -51,9 +51,9 @@ func run() error {
} }
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{

View File

@ -8,6 +8,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
ImgURLPrefix string
configured bool configured bool
} }

View File

@ -2,6 +2,9 @@ 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"
@ -28,6 +31,15 @@ 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
@ -47,7 +59,6 @@ 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
} }

View File

@ -2,7 +2,6 @@ package models
import ( import (
"database/sql" "database/sql"
"fmt"
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -87,12 +86,6 @@ 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

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.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

View File

@ -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,7 +103,4 @@ 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")
} }

View File

@ -1,7 +1,6 @@
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"
@ -10,7 +9,6 @@ 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"`
} }
@ -31,7 +29,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, db *sqlx.DB, log *logrus.Entry) { func (t *TorrentVideo) Update(detailer polochon.Detailer, log *logrus.Entry) {
if t.Video == nil { if t.Video == nil {
return return
} }
@ -41,28 +39,4 @@ func (t *TorrentVideo) Update(detailer polochon.Detailer, db *sqlx.DB, log *logr
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
} }

View File

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

View File

@ -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.Banner, BannerURL: s.GetImageURL("banner"),
FanartURL: s.Fanart, FanartURL: s.GetImageURL("fanart"),
PosterURL: s.Poster, PosterURL: s.GetImageURL("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

View File

@ -52,7 +52,13 @@ func ListHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, err) 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) return env.RenderJSON(w, torrents)
} }

View File

@ -170,7 +170,7 @@ const TorrentsDropdown = () => {
}; };
const TorrentsDropdownTitle = () => { const TorrentsDropdownTitle = () => {
const count = useSelector((state) => state.torrents.count); const count = useSelector((state) => state.torrents.torrents.length);
if (count === 0) { if (count === 0) {
return <span>Torrents</span>; 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 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> <div className="card-body pb-0">
{started && (
<>
<div className="progress bg-light"> <div className="progress bg-light">
<div <div
className={progressBarClass} className={progressBarClass}
@ -31,16 +33,12 @@ export const Progress = ({ torrent }) => {
aria-valuemax="100" aria-valuemax="100"
></div> ></div>
</div> </div>
{started && (
<p> <p>
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate} {downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
</p> </p>
</>
)} )}
{!started && ( {!started && <p>Download not yet started</p>}
<p>
<small>Not yet started</small>
</p>
)}
</div> </div>
); );
}; };

View File

@ -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 { prettyEpisodeNameWithoutShow } from "../../../utils"; import { prettyEpisodeName } from "../../../utils";
import { removeTorrent } from "../../../actions/torrents"; import { removeTorrent } from "../../../actions/torrents";
import { Progress } from "./progress"; import { Progress } from "./progress";
@ -10,31 +10,32 @@ import { Progress } from "./progress";
export const Torrent = ({ torrent }) => { export const Torrent = ({ torrent }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const title = (torrent) => { const torrentTitle = (torrent) => {
if (torrent.type !== "episode" || !torrent.video) { switch (torrent.type) {
return ""; case "movie":
} return torrent.video ? torrent.video.title : torrent.status.name;
case "episode":
return ( return torrent.video
prettyEpisodeNameWithoutShow( ? prettyEpisodeName(
torrent.video.show_title,
torrent.video.season, torrent.video.season,
torrent.video.episode torrent.video.episode
) + " - " )
); : torrent.status.name;
default:
return torrent.status.name;
}
}; };
return ( return (
<div className="border-top"> <div className="card w-100 mb-3">
<div className="card-text d-flex flex-row"> <h5 className="card-header">
<div className="mt-1 flex-fill"> <span className="text text-break">{torrentTitle(torrent)}</span>
<span className="text text-break">{title(torrent)}</span> <span
<small className="text-muted text-break">{torrent.status.name}</small> className="fa fa-trash clickable pull-right"
</div>
<div
className="fa fa-trash btn text-right"
onClick={() => dispatch(removeTorrent(torrent.status.id))} onClick={() => dispatch(removeTorrent(torrent.status.id))}
/> ></span>
</div> </h5>
<Progress torrent={torrent} /> <Progress torrent={torrent} />
</div> </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 React from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { TorrentGroup } from "./torrentGroup"; import { Torrent } from "./torrent";
export const Torrents = () => { export const Torrents = () => {
const torrentsKeys = useSelector((state) => const torrents = useSelector((state) => state.torrents.torrents);
Array.from(state.torrents.torrents.keys())
);
if (torrentsKeys.length === 0) { if (torrents.length === 0) {
return ( return (
<div className="jumbotron"> <div className="jumbotron">
<h2>No torrents</h2> <h2>No torrents</h2>
@ -18,8 +16,8 @@ export const Torrents = () => {
return ( return (
<div className="d-flex flex-wrap"> <div className="d-flex flex-wrap">
{torrentsKeys.map((key) => ( {torrents.map((torrent, index) => (
<TorrentGroup key={key} torrentKey={key} /> <Torrent key={index} torrent={torrent} />
))} ))}
</div> </div>
); );

View File

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

View File

@ -18,9 +18,6 @@ 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)}`;