Compare commits

..

No commits in common. "d94843be9f7820b51e6c66a2786dcde71cad6da4" and "ed508bbf3b25ca1b5e7b9f44e4a232eb0988af74" have entirely different histories.

31 changed files with 236 additions and 503 deletions

View File

@ -5,7 +5,6 @@ import (
"time"
"git.quimbo.fr/odwrtw/canape/backend/models"
"git.quimbo.fr/odwrtw/canape/backend/web"
"github.com/gorilla/websocket"
"github.com/jmoiron/sqlx"
"github.com/sirupsen/logrus"
@ -13,6 +12,7 @@ import (
// Channel represents the channel of the user and the server
type Channel struct {
log *logrus.Entry
// Channel where the eventer will write messages to
serverEventStream chan ServerEvent
// Channel where the eventer will write errors to
@ -31,7 +31,7 @@ type Channel struct {
}
// go routine writing events to the websocket connection
func (c *Channel) writer(env *web.Env) {
func (c *Channel) writer() {
// Create the ping timer that will ping the client every pingWait seconds
// to check that he's still listening
pingTicker := time.NewTicker(pingWait)
@ -46,30 +46,30 @@ func (c *Channel) writer(env *web.Env) {
case <-pingTicker.C:
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
env.Log.Warnf("error writing message: %s", err)
c.log.Warnf("error writing message: %s", err)
return
}
case e := <-c.serverEventStream:
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteJSON(e); err != nil {
env.Log.Warnf("error writing JSON message: %s", err)
c.log.Warnf("error writing JSON message: %s", err)
return
}
case err := <-c.serverErrorStream:
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteJSON(err); err != nil {
env.Log.Warnf("error writing JSON error: %s", err)
c.log.Warnf("error writing JSON error: %s", err)
return
}
case <-c.done:
env.Log.Debug("all done finished")
c.log.Debug("all done finished")
return
}
}
}
// go routine reading messages from the websocket connection
func (c *Channel) reader(env *web.Env) {
func (c *Channel) reader() {
// Read loop
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
for {
@ -79,19 +79,19 @@ func (c *Channel) reader(env *web.Env) {
if err := c.conn.ReadJSON(&msg); err != nil {
switch e := err.(type) {
case *websocket.CloseError:
env.Log.Info("close error")
c.log.Info("close error")
case net.Error:
if e.Timeout() {
env.Log.WithField(
c.log.WithField(
"error", err,
).Warn("timeout")
} else {
env.Log.WithField(
c.log.WithField(
"error", err,
).Warn("unknown net error")
}
default:
env.Log.WithField(
c.log.WithField(
"error", err,
).Warn("unknown error reading message")
}
@ -104,31 +104,31 @@ func (c *Channel) reader(env *web.Env) {
e, ok := Eventers[msg.Message]
if !ok {
env.Log.Warnf("no such event to subscribe %q", msg.Message)
c.log.Warnf("no such event to subscribe %q", msg.Message)
continue
}
switch msg.Type {
case "subscribe":
env.Log.Debugf("subscribe to %s", msg.Message)
c.log.Debugf("subscribe to %s", msg.Message)
if _, ok := c.Events[e.Name]; ok {
env.Log.Infof("user %s is already subscribed to %s", c.User.Name, e.Name)
c.log.Infof("user %s is already subscribed to %s", c.User.Name, e.Name)
continue
}
if err := e.Subscribe(c); err != nil {
c.Error(e.Name, err, env.Log)
c.Error(e.Name, err)
continue
}
c.Events[e.Name] = struct{}{}
case "unsubscribe":
env.Log.Debugf("unsubscribe from %s", msg.Message)
c.log.Debugf("unsubscribe from %s", msg.Message)
if _, ok := c.Events[e.Name]; !ok {
env.Log.Infof("user %s is not subscribed to %s", c.User.Name, e.Name)
c.log.Infof("user %s is not subscribed to %s", c.User.Name, e.Name)
continue
}
e.Unsubscribe(c)
default:
env.Log.Warnf("invalid type: %s", msg.Type)
c.log.Warnf("invalid type: %s", msg.Type)
}
}
}
@ -147,11 +147,11 @@ func (c *Channel) close() {
}
// Error sends an error into the errorStream channel
func (c *Channel) Error(name string, err error, log *logrus.Entry) {
func (c *Channel) Error(name string, err error) {
if c.closed {
return
}
log.WithField("name", name).Warn(err)
c.log.WithField("name", name).Warn(err)
c.serverErrorStream <- ServerError{
Event: Event{
Type: name,
@ -165,11 +165,11 @@ func (c *Channel) Error(name string, err error, log *logrus.Entry) {
}
// FatalError sends an error into the errorStream channel
func (c *Channel) FatalError(name string, err error, log *logrus.Entry) {
func (c *Channel) FatalError(name string, err error) {
if c.closed {
return
}
log.WithField("name", name).Warn(err)
c.log.WithField("name", name).Warn(err)
c.serverErrorStream <- ServerError{
Event: Event{
Type: name,

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"git.quimbo.fr/odwrtw/canape/backend/auth"
@ -23,24 +22,14 @@ const (
writeWait = 30 * time.Second
)
// Eventers is a global map of all the available Eventers
var Eventers map[string]*PolochonEventers
var once sync.Once
func initEventers(env *web.Env) {
once.Do(func() {
env.Log.Infof("Initialising eventers")
Eventers = map[string]*PolochonEventers{
torrentEventName: NewTorrentEventers(env),
videoEventName: NewVideoEventers(env),
}
})
// Eventers is a map of all the available Eventers
var Eventers = map[string]*PolochonEventers{
torrentEventName: NewTorrentEventers(),
videoEventName: NewVideoEventers(),
}
// WsHandler handles the websockets messages
func WsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
initEventers(env)
// Get the user
user := auth.GetCurrentUser(r, env.Log)
@ -78,6 +67,7 @@ func WsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
serverErrorStream: serverErrorStream,
done: make(chan struct{}, 1),
conn: ws,
log: env.Log,
User: user,
db: env.Database,
Events: map[string]struct{}{},
@ -86,9 +76,9 @@ func WsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
}
// Launch the go routine responsible for writing events in the websocket
go c.writer(env)
go c.writer()
// Launch the reader responsible for reading events from the websocket
c.reader(env)
c.reader()
return nil
}
@ -132,7 +122,6 @@ func PolochonHookHandler(env *web.Env, w http.ResponseWriter, r *http.Request) e
// HookDebugHandler handles the websockets messages
func HookDebugHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
initEventers(env)
debug := map[string]map[string][]*Channel{}
for e, event := range Eventers {

View File

@ -5,13 +5,13 @@ import (
"sync"
"git.quimbo.fr/odwrtw/canape/backend/models"
"git.quimbo.fr/odwrtw/canape/backend/web"
"github.com/sirupsen/logrus"
)
// BaseEventer represents the basis of a Eventer
type BaseEventer struct {
env *web.Env
users []*Channel
log *logrus.Entry
name string
}
@ -20,17 +20,22 @@ type BaseEventer struct {
// Eventer per polochon
type PolochonEventers struct {
sync.RWMutex
env *web.Env
Name string
log *logrus.Entry
polochons map[string]Eventer
NewEventer func(*web.Env, *models.Polochon) (Eventer, error)
NewEventer func(polo *models.Polochon, log *logrus.Entry) (Eventer, error)
}
// NewEventers returns a new PolochonEventers
func NewEventers(env *web.Env) *PolochonEventers {
func NewEventers() *PolochonEventers {
// Setup the logger
logger := logrus.New()
logger.Formatter = &logrus.TextFormatter{FullTimestamp: true}
logger.Level = logrus.DebugLevel
return &PolochonEventers{
env: env,
polochons: map[string]Eventer{},
log: logrus.NewEntry(logger),
}
}
@ -41,7 +46,7 @@ func (p *PolochonEventers) Subscribe(chanl *Channel) error {
p.Lock()
defer p.Unlock()
p.env.Log.Debugf("subscribing with the user %s", chanl.User.Name)
p.log.Debugf("subscribing with the user %s", chanl.User.Name)
if !chanl.User.PolochonActivated {
return fmt.Errorf("polochon not activated")
}
@ -50,7 +55,7 @@ func (p *PolochonEventers) Subscribe(chanl *Channel) error {
// listening
tn, ok := p.polochons[chanl.User.PolochonID.String]
if !ok {
p.env.Log.Debugf("create new eventer for polochon %s", chanl.User.PolochonID.String)
p.log.Debugf("create new eventer for polochon %s", chanl.User.PolochonID.String)
// Get the user's polochon
polo, err := chanl.User.GetPolochon(chanl.db)
@ -59,7 +64,7 @@ func (p *PolochonEventers) Subscribe(chanl *Channel) error {
}
// Create a new Eventer for this polochon
tn, err = p.NewEventer(p.env, polo)
tn, err = p.NewEventer(polo, p.log)
if err != nil {
return err
}
@ -77,7 +82,7 @@ func (p *PolochonEventers) Subscribe(chanl *Channel) error {
// Add the Channel to the Eventer
tn.Append(chanl)
p.env.Log.Debugf("eventer created for polochon %s", chanl.User.PolochonID.String)
p.log.Debugf("eventer created for polochon %s", chanl.User.PolochonID.String)
return nil
}
@ -88,21 +93,21 @@ func (p *PolochonEventers) Unsubscribe(chanl *Channel) {
p.Lock()
defer p.Unlock()
p.env.Log.Debugf("unsubscribing from %s with %s", p.Name, chanl.User.Name)
p.log.Debugf("unsubscribing from %s with %s", p.Name, chanl.User.Name)
tn, ok := p.polochons[chanl.User.PolochonID.String]
if !ok {
p.env.Log.Warnf("no eventer for polochon %s, not unsubscribing", chanl.User.PolochonID.String)
p.log.Warnf("no eventer for polochon %s, not unsubscribing", chanl.User.PolochonID.String)
return
}
if err := tn.Unsubscribe(chanl); err != nil {
p.env.Log.Errorf("failed to unsubscribe eventer: %s", err.Error())
p.log.Errorf("failed to unsubscribe eventer: %s", err.Error())
// TODO: check if we need to return here
}
if len(tn.Subscribers()) == 0 {
p.env.Log.Debugf("empty subscribers for this polochon, delete it")
p.log.Debugf("empty subscribers for this polochon, delete it")
tn.Finish()
// Delete the polochon from the Eventer when it's finished
delete(p.polochons, chanl.User.PolochonID.String)
@ -128,7 +133,7 @@ func (e *BaseEventer) Unsubscribe(chanl *Channel) error {
i++
continue
}
e.env.Log.Debugf("found the user channel %s for user %s, deleting it...", chanl.ID, chanl.User.Name)
e.log.Debugf("found the user channel %s for user %s, deleting it...", chanl.ID, chanl.User.Name)
// Delete this event from the list of events the channel is subscribed
delete(e.users[i].Events, e.name)
@ -157,7 +162,7 @@ func (e *BaseEventer) Unsubscribe(chanl *Channel) error {
func (e *BaseEventer) FatalError(err error) {
for _, chanl := range e.users {
// Send the error
chanl.FatalError(e.name, err, e.env.Log)
chanl.FatalError(e.name, err)
// Delete the event from the channel events
delete(chanl.Events, e.name)
}
@ -186,7 +191,7 @@ func (e *BaseEventer) NotifyAll(data interface{}) {
// Send the events to all the subscribed users
for _, chanl := range e.users {
e.env.Log.Debugf("sending event to %s", chanl.User.Name)
e.log.Debugf("sending event to %s", chanl.User.Name)
chanl.sendEvent(event)
}
}

View File

@ -6,33 +6,30 @@ import (
"time"
"git.quimbo.fr/odwrtw/canape/backend/models"
"git.quimbo.fr/odwrtw/canape/backend/web"
"github.com/odwrtw/papi"
"github.com/sirupsen/logrus"
)
// TorrentEventer represents the Eventer for torrents
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
done chan struct{}
pClient *papi.Client
torrents []papi.Torrent
}
var torrentEventName = "torrents"
// NewTorrentEventers returns a new PolochonEventers for torrents
func NewTorrentEventers(env *web.Env) *PolochonEventers {
eventer := NewEventers(env)
func NewTorrentEventers() *PolochonEventers {
eventer := NewEventers()
eventer.NewEventer = NewTorrentEventer
eventer.Name = torrentEventName
return eventer
}
// NewTorrentEventer returns a new Eventer for a specific Polochon
func NewTorrentEventer(env *web.Env, polo *models.Polochon) (Eventer, error) {
func NewTorrentEventer(polo *models.Polochon, log *logrus.Entry) (Eventer, error) {
// Create a new papi client
client, err := polo.NewPapiClient()
if err != nil {
@ -42,12 +39,13 @@ func NewTorrentEventer(env *web.Env, polo *models.Polochon) (Eventer, error) {
// This is the first time this polochon is requested, create the TorrentEventer
tn := &TorrentEventer{
BaseEventer: &BaseEventer{
env: env,
users: []*Channel{},
log: log,
name: torrentEventName,
},
pClient: client,
done: make(chan struct{}),
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)
@ -79,7 +77,7 @@ func (t *TorrentEventer) Launch() error {
err := t.torrentsUpdate()
if err != nil {
t.env.Log.Warnf("got error getting torrents: %s", err)
t.log.Warnf("got error getting torrents: %s", err)
}
for {
@ -87,10 +85,10 @@ func (t *TorrentEventer) Launch() error {
case <-timeTicker.C:
err := t.torrentsUpdate()
if err != nil {
t.env.Log.Warnf("got error getting torrents: %s", err)
t.log.Warnf("got error getting torrents: %s", err)
}
case <-t.done:
t.env.Log.Debug("quit torrent notifier")
t.log.Debug("quit torrent notifier")
return nil
}
}
@ -98,23 +96,21 @@ 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!")
t.log.Debugf("torrents have changed!")
data := models.NewTorrentVideos(t.env.Backend.Detailer, t.env.Database, t.env.Log, torrents)
t.NotifyAll(torrents)
t.NotifyAll(data)
t.previous = torrents
t.data = data
t.torrents = torrents
return nil
}
@ -122,6 +118,6 @@ func (t *TorrentEventer) torrentsUpdate() error {
// Finish implements the Eventer interface
// It is called when there is no more users subscribed
func (t *TorrentEventer) Finish() {
t.env.Log.Debugf("sending the done channel")
t.log.Debugf("sending the done channel")
t.done <- struct{}{}
}

View File

@ -2,7 +2,7 @@ package events
import (
"git.quimbo.fr/odwrtw/canape/backend/models"
"git.quimbo.fr/odwrtw/canape/backend/web"
"github.com/sirupsen/logrus"
)
// VideoEventer represents the Eventer for tests
@ -13,19 +13,19 @@ type VideoEventer struct {
var videoEventName = "newVideo"
// NewVideoEventers implements the Eventer interface
func NewVideoEventers(env *web.Env) *PolochonEventers {
eventer := NewEventers(env)
func NewVideoEventers() *PolochonEventers {
eventer := NewEventers()
eventer.NewEventer = NewVideoEventer
eventer.Name = videoEventName
return eventer
}
// NewVideoEventer returns a new Eventer
func NewVideoEventer(env *web.Env, polo *models.Polochon) (Eventer, error) {
func NewVideoEventer(polo *models.Polochon, log *logrus.Entry) (Eventer, error) {
return &VideoEventer{
BaseEventer: &BaseEventer{
env: env,
users: []*Channel{},
log: log,
name: videoEventName,
},
}, nil

View File

@ -196,7 +196,7 @@ func RefreshMovies(env *web.Env) {
// Iterate over the map of movies to refresh them
for id := range movieMap {
movie := movies.New(env, id, nil, nil, false)
movie := movies.New(id, nil, nil, false, env.Config.PublicDir, env.Config.ImgURLPrefix)
// Refresh the movie
err := movie.Refresh(env, env.Config.Movie.Detailers)
if err != nil {

View File

@ -80,7 +80,7 @@ func GetMovies(env *web.Env, user *models.User, source string, category string)
// Fill all the movies infos from the list of IDs
for _, id := range media.IDs {
pMovie, _ := pMovies.Has(id)
movie := movies.New(env, id, client, pMovie, moviesWishlist.IsMovieInWishlist(id))
movie := movies.New(id, client, pMovie, moviesWishlist.IsMovieInWishlist(id), env.Config.PublicDir, env.Config.ImgURLPrefix)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}
// Then with the default detailers

View File

@ -49,11 +49,7 @@ func run() error {
if err != nil {
return err
}
backend := &models.Backend{
Database: db,
}
models.SetPublicDir(cf.PublicDir)
models.SetImgURLPrefix(cf.ImgURLPrefix)
backend := &models.Backend{Database: db}
// Generate auth params
authParams := auth.Params{

View File

@ -47,7 +47,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"
@ -26,12 +25,8 @@ const (
FROM episodes WHERE show_imdb_id=$1;`
getEpisodeQuery = `
SELECT s.title show_title, e.*
FROM shows s , episodes e
WHERE s.imdb_id = e.show_imdb_id
AND e.show_imdb_id=$1
AND e.season=$2
AND e.episode=$3;`
SELECT *
FROM episodes WHERE show_imdb_id=$1 AND season=$2 AND episode=$3;`
)
// episodeDB represents the Episode in the DB
@ -41,7 +36,6 @@ type episodeDB struct {
ImdbID string `db:"imdb_id"`
ShowImdbID string `db:"show_imdb_id"`
ShowTvdbID int `db:"show_tvdb_id"`
ShowTitle string `db:"show_title"`
Season int `db:"season"`
Episode int `db:"episode"`
Title string `db:"title"`
@ -76,7 +70,6 @@ func FillEpisodeFromDB(eDB *episodeDB, pEpisode *polochon.ShowEpisode) {
// Keep the data that never changes but only if we have it
updateIfNonEmpty(&pEpisode.EpisodeImdbID, eDB.ImdbID)
updateIfNonEmpty(&pEpisode.ShowImdbID, eDB.ShowImdbID)
updateIfNonEmpty(&pEpisode.ShowTitle, eDB.ShowTitle)
updateIfNonZeroInt(&pEpisode.TvdbID, eDB.TvdbID)
updateIfNonZeroInt(&pEpisode.ShowTvdbID, eDB.ShowTvdbID)
pEpisode.Season = eDB.Season
@ -87,12 +80,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,68 +0,0 @@
package models
import (
"github.com/jmoiron/sqlx"
"github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib"
"github.com/sirupsen/logrus"
)
// TorrentVideo reprensents a torrent embeding the video inforamtions
type TorrentVideo struct {
*papi.Torrent
Img string `json:"img"`
Video polochon.Video `json:"video,omitempty"`
}
// NewTorrentVideo returns a new TorrentVideo
func NewTorrentVideo(t *papi.Torrent) *TorrentVideo {
torrent := &polochon.Torrent{
ImdbID: t.ImdbID,
Type: polochon.VideoType(t.Type),
Quality: polochon.Quality(t.Quality),
Season: t.Season,
Episode: t.Episode,
}
return &TorrentVideo{
Torrent: t,
Video: torrent.Video(),
}
}
// Update updates the Torrent video with the database details
func (t *TorrentVideo) Update(detailer polochon.Detailer, db *sqlx.DB, log *logrus.Entry) {
if t.Video == nil {
return
}
// TODO: refresh the video in db if not found
err := detailer.GetDetails(t.Video, log)
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

@ -76,7 +76,7 @@ func RefreshMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) e
}
// Create a new movie
m := New(env, id, client, pMovie, isWishlisted)
m := New(id, client, pMovie, isWishlisted, env.Config.PublicDir, env.Config.ImgURLPrefix)
// Refresh the movie's infos
if err := m.Refresh(env, env.Config.Movie.Detailers); err != nil {
@ -141,11 +141,12 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error {
for _, m := range movies {
pMovie, _ := pMovies.Has(m.ImdbID)
movie := New(
env,
m.ImdbID,
client,
pMovie,
moviesWishlist.IsMovieInWishlist(m.ImdbID),
env.Config.PublicDir,
env.Config.ImgURLPrefix,
)
// First check in the DB
@ -251,11 +252,12 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
for _, imdbID := range moviesWishlist.List() {
pMovie, _ := pMovies.Has(imdbID)
movie := New(
env,
imdbID,
client,
pMovie,
moviesWishlist.IsMovieInWishlist(imdbID),
env.Config.PublicDir,
env.Config.ImgURLPrefix,
)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}

View File

@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
@ -42,9 +43,8 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
VideoCodec string `json:"video_codec"`
Container string `json:"container"`
}{
Alias: (*Alias)(m),
// TODO: remove this field to use m.Thumb
PosterURL: m.Thumb,
Alias: (*Alias)(m),
PosterURL: m.PosterURL(),
Subtitles: []subtitles.Subtitle{},
}
@ -74,12 +74,12 @@ func (m *Movie) MarshalJSON() ([]byte, error) {
}
// New returns a new Movie with all the needed infos
func New(env *web.Env, imdbID string, client *papi.Client, pMovie *papi.Movie, isWishlisted bool) *Movie {
func New(imdbID string, client *papi.Client, pMovie *papi.Movie, isWishlisted bool, publicDir, imgURLPrefix string) *Movie {
return &Movie{
client: client,
pMovie: pMovie,
publicDir: env.Config.PublicDir,
imgURLPrefix: env.Config.ImgURLPrefix,
publicDir: publicDir,
imgURLPrefix: imgURLPrefix,
Wishlisted: isWishlisted,
Movie: &polochon.Movie{
ImdbID: imdbID,
@ -115,7 +115,7 @@ func (m *Movie) GetDetails(env *web.Env, detailers []polochon.Detailer) error {
// If found, return
// If not, retrives details with the detailers 'after' and update them in
// database
func (m *Movie) GetAndFetch(env *web.Env, before, after []polochon.Detailer) error {
func (m *Movie) GetAndFetch(env *web.Env, before []polochon.Detailer, after []polochon.Detailer) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.GetAndFetch",
@ -182,7 +182,7 @@ func (m *Movie) GetTorrents(env *web.Env, torrenters []polochon.Torrenter) error
// If found, return
// If not, retrives torrents with the torrenters 'after' and update them in
// database
func (m *Movie) GetAndFetchTorrents(env *web.Env, before, after []polochon.Torrenter) error {
func (m *Movie) GetAndFetchTorrents(env *web.Env, before []polochon.Torrenter, after []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.GetAndFetchTorrents",
@ -244,6 +244,15 @@ func (m *Movie) imgFile() string {
return filepath.Join(m.publicDir, "img", m.imgURL())
}
// PosterURL returns the image URL or the default image if the poster is not yet downloaded
func (m *Movie) PosterURL() string {
// Check if the movie image exists
if _, err := os.Stat(m.imgFile()); os.IsNotExist(err) {
return ""
}
return m.imgURLPrefix + m.imgURL()
}
// getPolochonMovies returns an array of the user's polochon movies
func getPolochonMovies(user *models.User, env *web.Env) ([]*Movie, error) {
movies := []*Movie{}
@ -269,11 +278,12 @@ func getPolochonMovies(user *models.User, env *web.Env) ([]*Movie, error) {
// Create Movies objects from the movies retrieved
for _, pmovie := range pmovies.List() {
movie := New(
env,
pmovie.ImdbID,
client,
pmovie,
moviesWishlist.IsMovieInWishlist(pmovie.ImdbID),
env.Config.PublicDir,
env.Config.ImgURLPrefix,
)
movies = append(movies, movie)
}

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

@ -7,7 +7,6 @@ import (
"sort"
"git.quimbo.fr/odwrtw/canape/backend/auth"
"git.quimbo.fr/odwrtw/canape/backend/models"
"git.quimbo.fr/odwrtw/canape/backend/web"
"github.com/gorilla/mux"
"github.com/odwrtw/papi"
@ -47,12 +46,11 @@ func ListHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, err)
}
list, err := client.GetTorrents()
torrents, err := client.GetTorrents()
if err != nil {
return env.RenderError(w, err)
}
torrents := models.NewTorrentVideos(env.Backend.Detailer, env.Database, env.Log, list)
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,7 +1,9 @@
import React from "react";
import React, { useState } from "react";
import PropTypes from "prop-types";
import { useDispatch, useSelector } from "react-redux";
import { AddTorrent } from "./list/addTorrent";
import { Torrents } from "./list/torrents";
import { prettySize } from "../../utils";
import { addTorrent, removeTorrent } from "../../actions/torrents";
export const TorrentList = () => {
return (
@ -13,3 +15,108 @@ export const TorrentList = () => {
</div>
);
};
const AddTorrent = () => {
const dispatch = useDispatch();
const [url, setUrl] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (url === "") {
return;
}
dispatch(
addTorrent({
result: { url: url },
})
);
setUrl("");
};
return (
<form onSubmit={(e) => handleSubmit(e)}>
<input
type="text"
className="form-control mb-3 w-100"
placeholder="Add torrent URL"
onSubmit={handleSubmit}
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</form>
);
};
const Torrents = () => {
const torrents = useSelector((state) => state.torrents.torrents);
if (torrents.length === 0) {
return (
<div className="jumbotron">
<h2>No torrents</h2>
</div>
);
}
return (
<div className="d-flex flex-wrap">
{torrents.map((torrent, index) => (
<Torrent key={index} torrent={torrent} />
))}
</div>
);
};
const Torrent = ({ torrent }) => {
const dispatch = useDispatch();
var progressStyle = torrent.status.is_finished
? "success"
: "info progress-bar-striped progress-bar-animated";
const progressBarClass = "progress-bar bg-" + progressStyle;
var percentDone = torrent.status.percent_done;
const started = percentDone !== 0;
if (started) {
percentDone = Number(percentDone).toFixed(1) + "%";
}
// Pretty sizes
const downloadedSize = prettySize(torrent.status.downloaded_size);
const totalSize = prettySize(torrent.status.total_size);
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
return (
<div className="card w-100 mb-3">
<h5 className="card-header">
<span className="text text-break">{torrent.status.name}</span>
<span
className="fa fa-trash clickable pull-right"
onClick={() => dispatch(removeTorrent(torrent.status.id))}
></span>
</h5>
<div className="card-body pb-0">
{started && (
<React.Fragment>
<div className="progress bg-light">
<div
className={progressBarClass}
style={{ width: percentDone }}
role="progressbar"
aria-valuenow={percentDone}
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<p>
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
</p>
</React.Fragment>
)}
{!started && <p>Download not yet started</p>}
</div>
</div>
);
};
Torrent.propTypes = {
torrent: PropTypes.object.isRequired,
};

View File

@ -1,35 +0,0 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTorrent } from "../../../actions/torrents";
export const AddTorrent = () => {
const dispatch = useDispatch();
const [url, setUrl] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (url === "") {
return;
}
dispatch(
addTorrent({
result: { url: url },
})
);
setUrl("");
};
return (
<form onSubmit={(e) => handleSubmit(e)}>
<input
type="text"
className="form-control mb-3 w-100"
placeholder="Add torrent URL"
onSubmit={handleSubmit}
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</form>
);
};

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

@ -1,49 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { prettySize } from "../../../utils";
export const Progress = ({ torrent }) => {
var progressStyle = torrent.status.is_finished
? "success"
: "info progress-bar-striped progress-bar-animated";
const progressBarClass = "progress-bar bg-" + progressStyle;
var percentDone = torrent.status.percent_done;
const started = percentDone !== 0;
if (started) {
percentDone = Number(percentDone).toFixed(1) + "%";
}
// Pretty sizes
const downloadedSize = prettySize(torrent.status.downloaded_size);
const totalSize = prettySize(torrent.status.total_size);
const downloadRate = prettySize(torrent.status.download_rate) + "/s";
return (
<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 && (
<p>
{downloadedSize} / {totalSize} - {percentDone} - {downloadRate}
</p>
)}
{!started && (
<p>
<small>Not yet started</small>
</p>
)}
</div>
);
};
Progress.propTypes = {
torrent: PropTypes.object.isRequired,
};

View File

@ -1,44 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { useDispatch } from "react-redux";
import { prettyEpisodeNameWithoutShow } from "../../../utils";
import { removeTorrent } from "../../../actions/torrents";
import { Progress } from "./progress";
export const Torrent = ({ torrent }) => {
const dispatch = useDispatch();
const title = (torrent) => {
if (torrent.type !== "episode" || !torrent.video) {
return "";
}
return (
prettyEpisodeNameWithoutShow(
torrent.video.season,
torrent.video.episode
) + " - "
);
};
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"
onClick={() => dispatch(removeTorrent(torrent.status.id))}
/>
</div>
<Progress torrent={torrent} />
</div>
);
};
Torrent.propTypes = {
torrent: PropTypes.object.isRequired,
};

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,26 +0,0 @@
import React from "react";
import { useSelector } from "react-redux";
import { TorrentGroup } from "./torrentGroup";
export const Torrents = () => {
const torrentsKeys = useSelector((state) =>
Array.from(state.torrents.torrents.keys())
);
if (torrentsKeys.length === 0) {
return (
<div className="jumbotron">
<h2>No torrents</h2>
</div>
);
}
return (
<div className="d-flex flex-wrap">
{torrentsKeys.map((key) => (
<TorrentGroup key={key} torrentKey={key} />
))}
</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)}`;

6
go.mod
View File

@ -1,6 +1,6 @@
module git.quimbo.fr/odwrtw/canape
go 1.14
go 1.12
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
@ -15,8 +15,8 @@ require (
github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/odwrtw/errors v0.0.0-20170604160533-c747b9d17833
github.com/odwrtw/papi v0.0.0-20200413153625-62744e1c1b73
github.com/odwrtw/polochon v0.0.0-20200413153516-6d6ff1d17684
github.com/odwrtw/papi v0.0.0-20200410143325-49e6f827259d
github.com/odwrtw/polochon v0.0.0-20200410143337-006e3fb9fb55
github.com/phyber/negroni-gzip v0.0.0-20180113114010-ef6356a5d029
github.com/pioz/tvdb v0.0.0-20190503215423-f45c687faba9 // indirect
github.com/robfig/cron v1.1.0

6
go.sum
View File

@ -134,12 +134,10 @@ github.com/odwrtw/guessit v0.0.0-20200131084001-f88613483547/go.mod h1:W22g7wtc0
github.com/odwrtw/imdb-watchlist v0.0.0-20190417175016-b7a9f7503d69 h1:ow6b/4Jj7J5iYwU678/rbijvaNUJrYkg13j9Nivkung=
github.com/odwrtw/imdb-watchlist v0.0.0-20190417175016-b7a9f7503d69/go.mod h1:o2tLH95CtNdqhDb0aS2NbU+1I4PmaNsODpr33Ry0JC0=
github.com/odwrtw/papi v0.0.0-20190413103029-bd5bfea85ae6/go.mod h1:CXotdtODLpW0/yuFV5XH8Rmrj0eAfPLvdMKykPM2WCk=
github.com/odwrtw/papi v0.0.0-20200410143325-49e6f827259d h1:it4hnCveS8eFymg0ll9KRzO/iQm/olSW0sb8Ctm3gXI=
github.com/odwrtw/papi v0.0.0-20200410143325-49e6f827259d/go.mod h1:eY0skvVHJBwbSJ18uq2c1T4SvhdEV8R0XFSb0zKh5Yo=
github.com/odwrtw/papi v0.0.0-20200413153625-62744e1c1b73 h1:19mh4fw/WGFtYg/7oHg1Y5rU9ZRxD3LqrtwY2NI6l6w=
github.com/odwrtw/papi v0.0.0-20200413153625-62744e1c1b73/go.mod h1:eY0skvVHJBwbSJ18uq2c1T4SvhdEV8R0XFSb0zKh5Yo=
github.com/odwrtw/polochon v0.0.0-20200410143337-006e3fb9fb55 h1:hcHBTi+HfYz5p6wgtvQCbrdog0uOB/7eVxPZA5Qff80=
github.com/odwrtw/polochon v0.0.0-20200410143337-006e3fb9fb55/go.mod h1:sAYf/A5tDmins2GHZn2mEFarmYltAZv+bcmSKSxDUaI=
github.com/odwrtw/polochon v0.0.0-20200413153516-6d6ff1d17684 h1:b9JDu8423NGXOYN0gtoiVrLKbZ/CfAyc2ouCRuE+mI8=
github.com/odwrtw/polochon v0.0.0-20200413153516-6d6ff1d17684/go.mod h1:rBjekia21ToZoTxJqR/5Ued8EYwKTtamq+bo/XINOjA=
github.com/odwrtw/tpb v0.0.0-20200130133144-c846aa382c6f h1:fwEIGT+o3e8+XkBqrwsE3/+9ketTQXflPhCkv3/w990=
github.com/odwrtw/tpb v0.0.0-20200130133144-c846aa382c6f/go.mod h1:updLvMbQo2xHoz94MX9+GqmSoKhf6E8fs/J+wLvvu6A=
github.com/odwrtw/trakttv v0.0.0-20200404161731-0d594827e4f9 h1:PuQLHO75MXUsJpf9BcTVxvR/FCkdn1MZnZt6h3o6cJI=