From 9701c9c00f1eb62f4daa900805f3951f1d93d419 Mon Sep 17 00:00:00 2001 From: Lucas BEE Date: Sun, 19 Feb 2017 11:45:34 +0000 Subject: [PATCH 1/4] Update readme to connect to the dev DB --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 4447e2d..3851b05 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,7 @@ yarn start ## Connect to the database ``` -docker run -it --rm --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test -``` - -You'll need to connect to the dev database with this command: - -``` -\c dev +docker run -it --rm -e PGPASSWORD=test --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test -d dev ``` ## Default users From 63aa470bf80fe487117d042a8ecc0d55b15c37cc Mon Sep 17 00:00:00 2001 From: Lucas BEE Date: Sun, 19 Feb 2017 11:50:30 +0000 Subject: [PATCH 2/4] Create backend implementing Detailer and Torrenter This Detailer and Torrenter will fetch information from the database --- src/internal/auth/auth.go | 6 +- src/internal/backend/backend.go | 146 ++++++++++++++++++++++++++++++++ src/internal/movies/movies.go | 40 +++++++-- src/internal/shows/episodes.go | 7 +- src/internal/shows/shows.go | 21 +++++ src/main.go | 18 ++-- 6 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 src/internal/backend/backend.go diff --git a/src/internal/auth/auth.go b/src/internal/auth/auth.go index e9457c6..0597981 100644 --- a/src/internal/auth/auth.go +++ b/src/internal/auth/auth.go @@ -21,7 +21,7 @@ var ( // UserBackend interface for user backend type UserBackend interface { - Get(username string) (User, error) + GetUser(username string) (User, error) } // User interface for user @@ -64,7 +64,7 @@ func (a *Authorizer) GenHash(password string) (string, error) { // Login cheks password and creates a jwt token func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) (User, error) { - u, err := a.Backend.Get(username) + u, err := a.Backend.GetUser(username) if err != nil { return nil, err } @@ -109,7 +109,7 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use } // Get the user - u, err := a.Backend.Get(tokenClaims.Username) + u, err := a.Backend.GetUser(tokenClaims.Username) if err != nil { return nil, err } diff --git a/src/internal/backend/backend.go b/src/internal/backend/backend.go new file mode 100644 index 0000000..3bb0094 --- /dev/null +++ b/src/internal/backend/backend.go @@ -0,0 +1,146 @@ +package backend + +import ( + "database/sql" + "errors" + + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" + + "github.com/Sirupsen/logrus" + "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" +) + +// Backend represents the data backend +type Backend struct { + Database *sqlx.DB +} + +// GetUser gets the username from the UserBackend +// Implements the UserBackend interface +func (b *Backend) GetUser(username string) (auth.User, error) { + return users.Get(b.Database, username) +} + +// Name implements the Module interface +func (b *Backend) Name() string { + return "canape-backend" +} + +// GetDetails implements the polochon Detailer interface +func (b *Backend) GetDetails(media interface{}, log *logrus.Entry) error { + switch t := media.(type) { + case *polochon.Show: + return b.GetShowDetails(t, log) + case *polochon.ShowEpisode: + return b.GetShowEpisodeDetails(t, log) + case *polochon.Movie: + return b.GetMovieDetails(t, log) + default: + return errors.New("invalid type") + } +} + +// GetMovieDetails gets details for movies +func (b *Backend) GetMovieDetails(pmovie *polochon.Movie, log *logrus.Entry) error { + m := movies.New(pmovie.ImdbID) + if err := m.GetMovie(b.Database); err != nil { + return err + } + log.Debugf("got movie %s from backend", pmovie.ImdbID) + *pmovie = m.Movie + return nil +} + +// GetShowDetails gets details for shows +func (b *Backend) GetShowDetails(pshow *polochon.Show, log *logrus.Entry) error { + s := shows.New(pshow.ImdbID) + if err := s.GetShow(b.Database); err != nil { + return err + } + log.Debugf("got show %s from backend", pshow.ImdbID) + *pshow = s.Show + return nil +} + +// GetShowEpisodeDetails gets details for episodes +func (b *Backend) GetShowEpisodeDetails(pepisode *polochon.ShowEpisode, log *logrus.Entry) error { + e := shows.NewEpisode() + e.ShowImdbID = pepisode.ShowImdbID + e.Season = pepisode.Season + e.Episode = pepisode.Episode + if err := e.GetEpisode(b.Database); err != nil { + return err + } + log.Debugf("got episode S%02dE%02d %s from backend", pepisode.Season, pepisode.Episode, pepisode.ShowImdbID) + *pepisode = e.ShowEpisode + return nil +} + +// GetTorrents implements the polochon Torrenter interface +func (b *Backend) GetTorrents(media interface{}, log *logrus.Entry) error { + switch t := media.(type) { + case *polochon.ShowEpisode: + return b.GetEpisodeTorrents(t, log) + case *polochon.Movie: + return b.GetMovieTorrents(t, log) + default: + return errors.New("invalid type") + } +} + +// GetMovieTorrents fetch Torrents for movies +func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error { + // m := movies.New(pmovie.ImdbID) + movieTorrents, err := torrents.GetMovieTorrents(b.Database, pmovie.ImdbID) + switch err { + case nil: + log.Debug("torrents found in backend") + case sql.ErrNoRows: + log.Debug("torrent not found in backend") + default: + // Unexpected error + return err + } + log.Debugf("got torrents for movie %s from backend", pmovie.ImdbID) + + for _, t := range movieTorrents { + pmovie.Torrents = append(pmovie.Torrents, t.Torrent) + } + + return nil +} + +// GetEpisodeTorrents fetch Torrents for episodes +func (b *Backend) GetEpisodeTorrents(pepisode *polochon.ShowEpisode, log *logrus.Entry) error { + e := shows.NewEpisode() + e.ShowImdbID = pepisode.ShowImdbID + e.Season = pepisode.Season + e.Episode = pepisode.Episode + episodeTorrents, err := torrents.GetEpisodeTorrents( + b.Database, + pepisode.ShowImdbID, + pepisode.Season, + pepisode.Episode, + ) + switch err { + case nil: + log.Debug("torrents found in backend") + case sql.ErrNoRows: + log.Debug("torrent not found in backend") + default: + // Unexpected error + return err + } + log.Debugf("got torrents for episode S%02dE%02d %s from backend", pepisode.Season, pepisode.Episode, pepisode.ShowImdbID) + // Add the torrents to the episode + for _, t := range episodeTorrents { + e.Torrents = append(e.Torrents, t.Torrent) + } + + return nil +} diff --git a/src/internal/movies/movies.go b/src/internal/movies/movies.go index 8d69cb8..34ac7fb 100644 --- a/src/internal/movies/movies.go +++ b/src/internal/movies/movies.go @@ -32,7 +32,7 @@ const ( tagline=:tagline RETURNING id;` - getMovieQueryByImdbID = ` + getUserMovieQueryByImdbID = ` SELECT movies.id, movies.title, @@ -54,7 +54,7 @@ const ( ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$2 WHERE movies.imdb_id=$1;` - getMovieQueryByID = ` + getUserMovieQueryByID = ` SELECT movies.id, movies.title, @@ -76,6 +76,16 @@ const ( ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$2 WHERE movies.id=$1;` + getMovieQueryByImdbID = ` + SELECT * + FROM movies + WHERE imdb_id=$1;` + + getMovieQueryByID = ` + SELECT * + FROM movies + WHERE movies.id=$1;` + deleteMovieQuery = `DELETE FROM movies WHERE id=$1;` ) @@ -170,9 +180,9 @@ func (m *Movie) Get(env *web.Env, user *users.User) error { var mDB MovieDB var err error if m.ID != "" { - err = env.Database.QueryRowx(getMovieQueryByID, m.ID, user.ID).StructScan(&mDB) + err = env.Database.QueryRowx(getUserMovieQueryByID, m.ID, user.ID).StructScan(&mDB) } else if m.ImdbID != "" { - err = env.Database.QueryRowx(getMovieQueryByImdbID, m.ImdbID, user.ID).StructScan(&mDB) + err = env.Database.QueryRowx(getUserMovieQueryByImdbID, m.ImdbID, user.ID).StructScan(&mDB) } else { err = fmt.Errorf("Can't get movie details, you have to specify an ID or ImdbID") } @@ -187,7 +197,27 @@ func (m *Movie) Get(env *web.Env, user *users.User) error { return nil } -// GetDetails retrieves details for the movie, first try to get info from db, +// GetMovie returns show details in database from id or imdbid or an error +func (m *Movie) GetMovie(db *sqlx.DB) error { + var mDB MovieDB + var err error + if m.ID != "" { + err = db.QueryRowx(getMovieQueryByID, m.ID).StructScan(&mDB) + } else if m.ImdbID != "" { + err = db.QueryRowx(getMovieQueryByImdbID, m.ImdbID).StructScan(&mDB) + } else { + err = fmt.Errorf("Can't get movie details, you have to specify an ID or ImdbID") + } + if err != nil { + return err + } + + // Set the poster url + // m.PosterURL = m.GetPosterURL(env) + + m.FillFromDB(&mDB) + return nil +} // if not exists, use polochon.Detailer and save informations in the database // for future use // diff --git a/src/internal/shows/episodes.go b/src/internal/shows/episodes.go index 29f79f6..7b751a7 100644 --- a/src/internal/shows/episodes.go +++ b/src/internal/shows/episodes.go @@ -187,8 +187,13 @@ func (e *Episode) GetTorrents(env *web.Env, force bool) error { // Get returns an episode func (e *Episode) Get(env *web.Env) error { + return e.GetEpisode(env.Database) +} + +// GetEpisode returns an episode +func (e *Episode) GetEpisode(db *sqlx.DB) error { var episodeDB EpisodeDB - err := env.Database.QueryRowx(getEpisodeQuery, e.ShowImdbID, e.Season, e.Episode).StructScan(&episodeDB) + err := db.QueryRowx(getEpisodeQuery, e.ShowImdbID, e.Season, e.Episode).StructScan(&episodeDB) if err != nil { return err } diff --git a/src/internal/shows/shows.go b/src/internal/shows/shows.go index 681d77b..e0e4187 100644 --- a/src/internal/shows/shows.go +++ b/src/internal/shows/shows.go @@ -171,6 +171,27 @@ func (s *Show) Get(env *web.Env, user *users.User) error { return nil } +// GetShow returns a show with user info like tracked +func (s *Show) GetShow(db *sqlx.DB) error { + var err error + var sDB ShowDB + if s.ID != "" { + err = db.QueryRowx(getShowQueryByID, s.ID).StructScan(&sDB) + } else if s.ImdbID != "" { + err = db.QueryRowx(getShowQueryByImdbID, s.ImdbID).StructScan(&sDB) + } else { + err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID") + } + if err != nil { + return err + } + // Set the poster url + // s.PosterURL = s.GetPosterURL(env) + + s.FillFromDB(&sDB) + return nil +} + // GetDetails retrieves details for the show, first try to // get info from db, if not exists, use polochon.Detailer // and save informations in the database for future use diff --git a/src/main.go b/src/main.go index 313a0f2..78fc2c2 100644 --- a/src/main.go +++ b/src/main.go @@ -5,6 +5,7 @@ import ( "os" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" @@ -20,16 +21,6 @@ import ( "github.com/urfave/negroni" ) -// UserBackend represents the data backend to get the user -type UserBackend struct { - Database *sqlx.DB -} - -// Get gets the username from the UserBackend -func (b *UserBackend) Get(username string) (auth.User, error) { - return users.Get(b.Database, username) -} - func main() { var cfgPath string cfgPath = os.Getenv("CONFIG_FILE") @@ -48,20 +39,23 @@ func main() { log.Panic(err) } + // Connect to the database db, err := sqlx.Connect("postgres", cf.PGDSN) if err != nil { log.Panic(err) } - uBackend := &UserBackend{Database: db} + backend := &backend.Backend{Database: db} + // Generate auth params authParams := auth.Params{ - Backend: uBackend, + Backend: backend, Pepper: cf.Authorizer.Pepper, Cost: cf.Authorizer.Cost, Secret: cf.Authorizer.Secret, } authorizer := auth.New(authParams) + // Create web environment needed by the app env := web.NewEnv(web.EnvParams{ Database: db, Auth: authorizer, From 8f9ec25808e17f94d39f3408b0cf386d98b4ebac Mon Sep 17 00:00:00 2001 From: Lucas BEE Date: Mon, 20 Mar 2017 14:35:40 +0000 Subject: [PATCH 3/4] Massive rewrite https://w000t.me/a479573ac6 --- INTERNAL.md | 64 +++ .../0004_change_media_category.down.sql | 3 + .../0004_change_media_category.up.sql | 3 + src/internal/auth/middleware.go | 8 +- src/internal/backend/backend.go | 130 +---- src/internal/backend/detailer.go | 68 +++ src/internal/backend/episode_torrents.go | 105 ++++ src/internal/backend/episodes.go | 141 +++++ src/internal/backend/explorer.go | 47 ++ src/internal/backend/movie_torrents.go | 101 ++++ src/internal/backend/movie_wishlist.go | 125 +++++ src/internal/backend/movies.go | 119 +++++ src/internal/backend/show_wishlist.go | 140 +++++ src/internal/backend/shows.go | 104 ++++ src/internal/backend/torrenter.go | 76 +++ src/internal/config/canape.go | 51 +- .../external_medias/external_medias.go | 221 ++++++-- .../external_medias/external_medias_test.go | 75 --- src/internal/external_medias/handlers.go | 294 ++++------- src/internal/movies/handlers.go | 248 +++++---- src/internal/movies/movies.go | 491 +++++++----------- src/internal/movies/movies_test.go | 92 ---- src/internal/movies/wishlist.go | 83 --- src/internal/random/random.go | 1 + src/internal/shows/episodes.go | 290 +++++------ src/internal/shows/handlers.go | 330 ++++++------ src/internal/shows/shows.go | 458 +++++----------- src/internal/shows/shows_test.go | 187 ------- src/internal/shows/wishlist.go | 87 ---- src/internal/torrents/episode_torrents.go | 160 ------ src/internal/torrents/handlers.go | 17 +- src/internal/torrents/movie_torrents.go | 171 ------ src/internal/users/handlers.go | 1 + src/internal/users/users.go | 29 +- src/internal/users/users_test.go | 337 ------------ src/internal/web/env.go | 15 + src/main.go | 52 +- src/routes.go | 51 ++ 38 files changed, 2315 insertions(+), 2660 deletions(-) create mode 100644 INTERNAL.md create mode 100644 sql/migration/0004_change_media_category.down.sql create mode 100644 sql/migration/0004_change_media_category.up.sql create mode 100644 src/internal/backend/detailer.go create mode 100644 src/internal/backend/episode_torrents.go create mode 100644 src/internal/backend/episodes.go create mode 100644 src/internal/backend/explorer.go create mode 100644 src/internal/backend/movie_torrents.go create mode 100644 src/internal/backend/movie_wishlist.go create mode 100644 src/internal/backend/movies.go create mode 100644 src/internal/backend/show_wishlist.go create mode 100644 src/internal/backend/shows.go create mode 100644 src/internal/backend/torrenter.go delete mode 100644 src/internal/external_medias/external_medias_test.go delete mode 100644 src/internal/movies/movies_test.go delete mode 100644 src/internal/movies/wishlist.go delete mode 100644 src/internal/shows/shows_test.go delete mode 100644 src/internal/shows/wishlist.go delete mode 100644 src/internal/torrents/episode_torrents.go delete mode 100644 src/internal/torrents/movie_torrents.go delete mode 100644 src/internal/users/users_test.go create mode 100644 src/routes.go diff --git a/INTERNAL.md b/INTERNAL.md new file mode 100644 index 0000000..5fae012 --- /dev/null +++ b/INTERNAL.md @@ -0,0 +1,64 @@ +# Methods + +* (1) Get from DB + - GetDetails (Detailer : canape-backend) +* (2) Get from DB and fetch if not found + - GetDetails (Detailer : canape-backend) + Stop if found + - GetDetails (Detailer : all the others) + Upsert if found +* (3) Refresh + - GetDetails (Detailer : all the others) + Upsert if found + +## Movies + +### /movies/polochon + +-Fetch polochon movies + Movies (2)- + +### /movies/{id}/refresh + +-Movies (3) + Torrents (3)- + +### /movies/explore + +-Explorer (1) + Movies (2) + Torrents (1)- + +### /movies/search + +-Search + Movies (2) + Torrents (1)- + +### /movies/refresh + +Explorer (3) + Movies (3) + Torrents (3) + +## Shows + +### /shows/polochon + +-Fetch polochon shows + Shows (2)- + +### /shows/explore + +-Explorer (1) + Shows (2)- + +### /shows/search + +-Search + Shows (2)- + +### /shows/{id} + +-Shows (2) + Episodes (1) + Torrents (1)- + +### /shows/{id}/refresh + +-Shows (3) + Episodes (1) + Torrents (3)- + +### /shows/{id}/seasons/{season}/episodes/{episode} + +-Episode (3) + Torrents (3)- + +### /shows/refresh + +Explorer (3) + Shows (3) + Torrents (3) diff --git a/sql/migration/0004_change_media_category.down.sql b/sql/migration/0004_change_media_category.down.sql new file mode 100644 index 0000000..7055da1 --- /dev/null +++ b/sql/migration/0004_change_media_category.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE external_medias ALTER COLUMN category TYPE media_category USING category::media_category; +ALTER TABLE external_medias ALTER COLUMN source TYPE media_category USING source::media_source; +ALTER TABLE external_medias ALTER COLUMN type TYPE media_category USING type::media_type; diff --git a/sql/migration/0004_change_media_category.up.sql b/sql/migration/0004_change_media_category.up.sql new file mode 100644 index 0000000..508f3a6 --- /dev/null +++ b/sql/migration/0004_change_media_category.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE external_medias ALTER COLUMN category TYPE text; +ALTER TABLE external_medias ALTER COLUMN source TYPE text; +ALTER TABLE external_medias ALTER COLUMN type TYPE text; diff --git a/src/internal/auth/middleware.go b/src/internal/auth/middleware.go index a810288..37888fd 100644 --- a/src/internal/auth/middleware.go +++ b/src/internal/auth/middleware.go @@ -13,6 +13,8 @@ type Middleware struct { log *logrus.Entry } +type authContextKey string + // NewMiddleware returns a new authentication middleware func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware { return &Middleware{ @@ -34,7 +36,8 @@ func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http m.log.Debugf("got a nil user in the context") } - ctx := context.WithValue(r.Context(), "auth.user", user) + ctxKey := authContextKey("auth.user") + ctx := context.WithValue(r.Context(), ctxKey, user) r = r.WithContext(ctx) next(w, r) @@ -80,7 +83,8 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next func GetCurrentUser(r *http.Request, log *logrus.Entry) User { log.Debug("getting user from context") - u := r.Context().Value("auth.user") + ctxKey := authContextKey("auth.user") + u := r.Context().Value(ctxKey) if u == nil { return nil } diff --git a/src/internal/backend/backend.go b/src/internal/backend/backend.go index 3bb0094..4fac62d 100644 --- a/src/internal/backend/backend.go +++ b/src/internal/backend/backend.go @@ -1,18 +1,10 @@ package backend import ( - "database/sql" - "errors" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" - "github.com/Sirupsen/logrus" "github.com/jmoiron/sqlx" - polochon "github.com/odwrtw/polochon/lib" ) // Backend represents the data backend @@ -20,127 +12,13 @@ type Backend struct { Database *sqlx.DB } -// GetUser gets the username from the UserBackend -// Implements the UserBackend interface -func (b *Backend) GetUser(username string) (auth.User, error) { - return users.Get(b.Database, username) -} - // Name implements the Module interface func (b *Backend) Name() string { return "canape-backend" } -// GetDetails implements the polochon Detailer interface -func (b *Backend) GetDetails(media interface{}, log *logrus.Entry) error { - switch t := media.(type) { - case *polochon.Show: - return b.GetShowDetails(t, log) - case *polochon.ShowEpisode: - return b.GetShowEpisodeDetails(t, log) - case *polochon.Movie: - return b.GetMovieDetails(t, log) - default: - return errors.New("invalid type") - } -} - -// GetMovieDetails gets details for movies -func (b *Backend) GetMovieDetails(pmovie *polochon.Movie, log *logrus.Entry) error { - m := movies.New(pmovie.ImdbID) - if err := m.GetMovie(b.Database); err != nil { - return err - } - log.Debugf("got movie %s from backend", pmovie.ImdbID) - *pmovie = m.Movie - return nil -} - -// GetShowDetails gets details for shows -func (b *Backend) GetShowDetails(pshow *polochon.Show, log *logrus.Entry) error { - s := shows.New(pshow.ImdbID) - if err := s.GetShow(b.Database); err != nil { - return err - } - log.Debugf("got show %s from backend", pshow.ImdbID) - *pshow = s.Show - return nil -} - -// GetShowEpisodeDetails gets details for episodes -func (b *Backend) GetShowEpisodeDetails(pepisode *polochon.ShowEpisode, log *logrus.Entry) error { - e := shows.NewEpisode() - e.ShowImdbID = pepisode.ShowImdbID - e.Season = pepisode.Season - e.Episode = pepisode.Episode - if err := e.GetEpisode(b.Database); err != nil { - return err - } - log.Debugf("got episode S%02dE%02d %s from backend", pepisode.Season, pepisode.Episode, pepisode.ShowImdbID) - *pepisode = e.ShowEpisode - return nil -} - -// GetTorrents implements the polochon Torrenter interface -func (b *Backend) GetTorrents(media interface{}, log *logrus.Entry) error { - switch t := media.(type) { - case *polochon.ShowEpisode: - return b.GetEpisodeTorrents(t, log) - case *polochon.Movie: - return b.GetMovieTorrents(t, log) - default: - return errors.New("invalid type") - } -} - -// GetMovieTorrents fetch Torrents for movies -func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error { - // m := movies.New(pmovie.ImdbID) - movieTorrents, err := torrents.GetMovieTorrents(b.Database, pmovie.ImdbID) - switch err { - case nil: - log.Debug("torrents found in backend") - case sql.ErrNoRows: - log.Debug("torrent not found in backend") - default: - // Unexpected error - return err - } - log.Debugf("got torrents for movie %s from backend", pmovie.ImdbID) - - for _, t := range movieTorrents { - pmovie.Torrents = append(pmovie.Torrents, t.Torrent) - } - - return nil -} - -// GetEpisodeTorrents fetch Torrents for episodes -func (b *Backend) GetEpisodeTorrents(pepisode *polochon.ShowEpisode, log *logrus.Entry) error { - e := shows.NewEpisode() - e.ShowImdbID = pepisode.ShowImdbID - e.Season = pepisode.Season - e.Episode = pepisode.Episode - episodeTorrents, err := torrents.GetEpisodeTorrents( - b.Database, - pepisode.ShowImdbID, - pepisode.Season, - pepisode.Episode, - ) - switch err { - case nil: - log.Debug("torrents found in backend") - case sql.ErrNoRows: - log.Debug("torrent not found in backend") - default: - // Unexpected error - return err - } - log.Debugf("got torrents for episode S%02dE%02d %s from backend", pepisode.Season, pepisode.Episode, pepisode.ShowImdbID) - // Add the torrents to the episode - for _, t := range episodeTorrents { - e.Torrents = append(e.Torrents, t.Torrent) - } - - return nil +// GetUser gets the username from the UserBackend +// Implements the UserBackend interface +func (b *Backend) GetUser(username string) (auth.User, error) { + return users.Get(b.Database, username) } diff --git a/src/internal/backend/detailer.go b/src/internal/backend/detailer.go new file mode 100644 index 0000000..15fb06b --- /dev/null +++ b/src/internal/backend/detailer.go @@ -0,0 +1,68 @@ +package backend + +import ( + "errors" + + "github.com/Sirupsen/logrus" + polochon "github.com/odwrtw/polochon/lib" +) + +// GetDetails implements the polochon Detailer interface +func (b *Backend) GetDetails(media interface{}, log *logrus.Entry) error { + switch t := media.(type) { + case *polochon.Show: + return b.GetShowDetails(t, log) + case *polochon.ShowEpisode: + return b.GetShowEpisodeDetails(t, log) + case *polochon.Movie: + return b.GetMovieDetails(t, log) + default: + return errors.New("invalid type") + } +} + +// GetMovieDetails gets details for movies +func (b *Backend) GetMovieDetails(pMovie *polochon.Movie, log *logrus.Entry) error { + // Get the movie + if err := GetMovie(b.Database, pMovie); err != nil { + return err + } + + log.Debugf("got movie %s from backend", pMovie.ImdbID) + + return nil +} + +// GetShowDetails gets details for shows +func (b *Backend) GetShowDetails(pShow *polochon.Show, log *logrus.Entry) error { + // Get the show + if err := GetShow(b.Database, pShow); err != nil { + return err + } + + log.Debugf("got show %s from backend", pShow.ImdbID) + // Get the show's episodes + err := GetEpisodes(b.Database, pShow, log) + if err != nil { + log.Warnf("error while getting episodes: %s", err) + return err + } + return nil +} + +// GetShowEpisodeDetails gets details for episodes +func (b *Backend) GetShowEpisodeDetails(pEpisode *polochon.ShowEpisode, log *logrus.Entry) error { + // Get the episode + if err := GetEpisode(b.Database, pEpisode); err != nil { + return err + } + + log.Debugf( + "got episode S%02dE%02d %s from backend", + pEpisode.Season, + pEpisode.Episode, + pEpisode.ShowImdbID, + ) + + return nil +} diff --git a/src/internal/backend/episode_torrents.go b/src/internal/backend/episode_torrents.go new file mode 100644 index 0000000..acd3320 --- /dev/null +++ b/src/internal/backend/episode_torrents.go @@ -0,0 +1,105 @@ +package backend + +import ( + "database/sql" + "time" + + "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" +) + +const ( + upsertEpisodeTorrentQuery = ` + INSERT INTO episode_torrents (imdb_id, url, source, quality, upload_user, + season, episode, seeders, leechers) + VALUES (:imdb_id, :url, :source, :quality, :upload_user, :season, :episode, + :seeders, :leechers) + ON CONFLICT (imdb_id, season, episode, quality, source) + DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality, + upload_user=:upload_user, season=:season, episode=:episode, + seeders=:seeders, leechers=:leechers + RETURNING id;` + + getEpisodeTorrentQuery = ` + SELECT * + FROM episode_torrents WHERE imdb_id=$1 AND season=$2 AND episode=$3;` +) + +// EpisodeTorrentDB represents the EpisodeTorrent in the DB +type EpisodeTorrentDB struct { + ID string `db:"id"` + ImdbID string `db:"imdb_id"` + URL string `db:"url"` + Source string `db:"source"` + Quality string `db:"quality"` + UploadUser string `db:"upload_user"` + Season int `db:"season"` + Episode int `db:"episode"` + Seeders int `db:"seeders"` + Leechers int `db:"leechers"` + Created time.Time `db:"created_at"` + Updated time.Time `db:"updated_at"` +} + +// NewTorrentFromEpisodeTorrentDB returns a polochon.Torrent from an +// episodeTorrentDB +func NewTorrentFromEpisodeTorrentDB(eDB *EpisodeTorrentDB) *polochon.Torrent { + q, _ := polochon.StringToQuality(eDB.Quality) + return &polochon.Torrent{ + Quality: *q, + URL: eDB.URL, + Seeders: eDB.Seeders, + Leechers: eDB.Leechers, + Source: eDB.Source, + UploadUser: eDB.UploadUser, + } +} + +// NewEpisodeTorrentDB returns an EpisodeTorrentDB ready to be put in DB from a +// polochon.Torrent +func NewEpisodeTorrentDB(t *polochon.Torrent, imdbID string, season, episode int) *EpisodeTorrentDB { + return &EpisodeTorrentDB{ + ImdbID: imdbID, + Season: season, + Episode: episode, + URL: t.URL, + Source: t.Source, + Quality: string(t.Quality), + UploadUser: t.UploadUser, + Seeders: t.Seeders, + Leechers: t.Leechers, + } +} + +// GetEpisodeTorrents returns show episodes torrents from database +func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]polochon.Torrent, error) { + var torrentsDB = []*EpisodeTorrentDB{} + err := db.Select(&torrentsDB, getEpisodeTorrentQuery, imdbID, season, episode) + if err != nil { + return nil, err + } + + if len(torrentsDB) == 0 { + return nil, sql.ErrNoRows + } + + var torrents []polochon.Torrent + for _, torrentDB := range torrentsDB { + torrent := NewTorrentFromEpisodeTorrentDB(torrentDB) + torrents = append(torrents, *torrent) + } + + return torrents, nil +} + +// UpsertEpisodeTorrent upserts an episode torrent from a polochon torrent +func UpsertEpisodeTorrent(db *sqlx.DB, torrent *polochon.Torrent, imdbID string, season, episode int) error { + // Create the EpisodeTorrent ready to be put in db + eDB := NewEpisodeTorrentDB(torrent, imdbID, season, episode) + _, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB) + if err != nil { + return err + } + + return nil +} diff --git a/src/internal/backend/episodes.go b/src/internal/backend/episodes.go new file mode 100644 index 0000000..e7a521a --- /dev/null +++ b/src/internal/backend/episodes.go @@ -0,0 +1,141 @@ +package backend + +import ( + "time" + + "github.com/Sirupsen/logrus" + "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" +) + +const ( + upsertEpisodeQuery = ` + INSERT INTO episodes (show_imdb_id, show_tvdb_id, title, season, episode, tvdb_id, aired, plot, runtime, rating, imdb_id) + VALUES (:show_imdb_id, :show_tvdb_id, :title, :season, :episode, :tvdb_id, :aired, :plot, :runtime, :rating, :imdb_id) + ON CONFLICT (show_imdb_id, season, episode) + DO UPDATE + SET show_imdb_id=:show_imdb_id, show_tvdb_id=:show_tvdb_id, title=:title, + season=:season, episode=:episode, tvdb_id=:tvdb_id, aired=:aired, + plot=:plot, runtime=:runtime, rating=:rating, imdb_id=:imdb_id + RETURNING id;` + + getEpisodesQuery = ` + SELECT * + FROM episodes WHERE show_imdb_id=$1;` + + getEpisodeQuery = ` + SELECT * + FROM episodes WHERE show_imdb_id=$1 AND season=$2 AND episode=$3;` +) + +// EpisodeDB represents the Episode in the DB +type EpisodeDB struct { + ID string `db:"id"` + TvdbID int `db:"tvdb_id"` + ImdbID string `db:"imdb_id"` + ShowImdbID string `db:"show_imdb_id"` + ShowTvdbID int `db:"show_tvdb_id"` + Season int `db:"season"` + Episode int `db:"episode"` + Title string `db:"title"` + Rating float32 `db:"rating"` + Plot string `db:"plot"` + Thumb string `db:"thumb"` + Runtime int `db:"runtime"` + Aired string `db:"aired"` + ReleaseGroup string `db:"release_group"` + Created time.Time `db:"created_at"` + Updated time.Time `db:"updated_at"` +} + +// NewEpisodeFromPolochon returns an EpisodeDB from a polochon ShowEpisode +func NewEpisodeFromPolochon(e *polochon.ShowEpisode) *EpisodeDB { + return &EpisodeDB{ + TvdbID: e.TvdbID, + ImdbID: e.EpisodeImdbID, + ShowImdbID: e.ShowImdbID, + ShowTvdbID: e.ShowTvdbID, + Season: e.Season, + Episode: e.Episode, + Title: e.Title, + Rating: e.Rating, + Plot: e.Plot, + Thumb: e.Thumb, + Runtime: e.Runtime, + Aired: e.Aired, + ReleaseGroup: e.ReleaseGroup, + } +} + +// NewEpisodeFromDB returns a new polochon ShowEpisode from an EpisodeDB +func NewEpisodeFromDB(eDB *EpisodeDB) *polochon.ShowEpisode { + pEpisode := polochon.ShowEpisode{} + FillEpisodeFromDB(eDB, &pEpisode) + return &pEpisode +} + +// FillEpisodeFromDB fills a ShowEpisode from an EpisodeDB +func FillEpisodeFromDB(eDB *EpisodeDB, pEpisode *polochon.ShowEpisode) { + pEpisode.TvdbID = eDB.TvdbID + pEpisode.EpisodeImdbID = eDB.ImdbID + pEpisode.ShowImdbID = eDB.ShowImdbID + pEpisode.ShowTvdbID = eDB.ShowTvdbID + pEpisode.Season = eDB.Season + pEpisode.Episode = eDB.Episode + pEpisode.Title = eDB.Title + pEpisode.Rating = eDB.Rating + pEpisode.Plot = eDB.Plot + pEpisode.Thumb = eDB.Thumb + pEpisode.Runtime = eDB.Runtime + pEpisode.Aired = eDB.Aired +} + +// GetEpisode gets an episode and fills the polochon episode +func GetEpisode(db *sqlx.DB, pEpisode *polochon.ShowEpisode) error { + var episodeDB EpisodeDB + err := db.QueryRowx(getEpisodeQuery, pEpisode.ShowImdbID, pEpisode.Season, pEpisode.Episode).StructScan(&episodeDB) + if err != nil { + return err + } + + FillEpisodeFromDB(&episodeDB, pEpisode) + + return nil +} + +// GetEpisodes gets show's episodes and fills the polochon show +func GetEpisodes(db *sqlx.DB, pShow *polochon.Show, log *logrus.Entry) error { + // Get the episodes + var episodesDB = []*EpisodeDB{} + err := db.Select(&episodesDB, getEpisodesQuery, pShow.ImdbID) + if err != nil { + return err + } + if len(episodesDB) == 0 { + log.Debug("got no episodes") + return nil + } + + log.Debugf("got %d episodes", len(episodesDB)) + for _, episodeDB := range episodesDB { + episode := NewEpisodeFromDB(episodeDB) + pShow.Episodes = append(pShow.Episodes, episode) + } + + return nil +} + +// UpsertEpisode upserts the episode +func UpsertEpisode(db *sqlx.DB, showEpisode *polochon.ShowEpisode) error { + e := NewEpisodeFromPolochon(showEpisode) + var id string + r, err := db.NamedQuery(upsertEpisodeQuery, e) + if err != nil { + return err + } + for r.Next() { + r.Scan(&id) + } + e.ID = id + return nil +} diff --git a/src/internal/backend/explorer.go b/src/internal/backend/explorer.go new file mode 100644 index 0000000..e8a1160 --- /dev/null +++ b/src/internal/backend/explorer.go @@ -0,0 +1,47 @@ +package backend + +import ( + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" + + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +const ( + upsertExternalMediaQuery = ` + INSERT INTO external_medias (type, source, category, ids) + VALUES (:type, :source, :category, :ids) + ON CONFLICT (type, source, category) + DO UPDATE SET type=:type, source=:source, category=:category, ids=:ids + RETURNING id;` + + getExternalMediaQuery = `SELECT * FROM external_medias WHERE type=$1 AND source=$2 AND category=$3 LIMIT 1;` +) + +// Media represents an external media +type Media struct { + sqly.BaseModel + Type string `db:"type"` + Source string `db:"source"` + Category string `db:"category"` + IDs pq.StringArray `db:"ids"` +} + +// Explore will return an array of Medias from the DB +func Explore(db *sqlx.DB, mtype, msrc, mcat string) (*Media, error) { + m := &Media{} + if err := db.QueryRowx(getExternalMediaQuery, mtype, msrc, mcat).StructScan(m); err != nil { + return nil, err + } + + return m, nil +} + +// Upsert adds or updates the Media in the database +func (m *Media) Upsert(db *sqlx.DB) error { + _, err := db.NamedQuery(upsertExternalMediaQuery, m) + if err != nil { + return err + } + return nil +} diff --git a/src/internal/backend/movie_torrents.go b/src/internal/backend/movie_torrents.go new file mode 100644 index 0000000..a204f7c --- /dev/null +++ b/src/internal/backend/movie_torrents.go @@ -0,0 +1,101 @@ +package backend + +import ( + "database/sql" + "time" + + "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" +) + +const ( + upsertMovieTorrentQuery = ` + INSERT INTO movie_torrents (imdb_id, url, source, quality, upload_user, + seeders, leechers) + VALUES (:imdb_id, :url, :source, :quality, :upload_user, :seeders, + :leechers) + ON CONFLICT (imdb_id, quality, source) + DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality, + upload_user=:upload_user, seeders=:seeders, leechers=:leechers + RETURNING id;` + + getMovieTorrentQueryByImdbID = ` + SELECT * + FROM movie_torrents WHERE imdb_id=$1;` +) + +// MovieTorrentDB represents the MovieTorrent in the DB +type MovieTorrentDB struct { + ID string `db:"id"` + ImdbID string `db:"imdb_id"` + URL string `db:"url"` + Source string `db:"source"` + Quality string `db:"quality"` + UploadUser string `db:"upload_user"` + Seeders int `db:"seeders"` + Leechers int `db:"leechers"` + Created time.Time `db:"created_at"` + Updated time.Time `db:"updated_at"` +} + +// NewTorrentFromMovieTorrentDB creates a new polochon.Torrent from a +// MovieTorrentDB +func NewTorrentFromMovieTorrentDB(mDB *MovieTorrentDB) *polochon.Torrent { + q, _ := polochon.StringToQuality(mDB.Quality) + return &polochon.Torrent{ + Quality: *q, + URL: mDB.URL, + Seeders: mDB.Seeders, + Leechers: mDB.Leechers, + Source: mDB.Source, + UploadUser: mDB.UploadUser, + } +} + +// NewMovieTorrentDB returns a MovieTorrent ready to be put in DB from a +// Torrent +func NewMovieTorrentDB(t *polochon.Torrent, imdbID string) MovieTorrentDB { + return MovieTorrentDB{ + ImdbID: imdbID, + URL: t.URL, + Source: t.Source, + Quality: string(t.Quality), + UploadUser: t.UploadUser, + Seeders: t.Seeders, + Leechers: t.Leechers, + } +} + +// GetMovieTorrents returns polochon.Torrents from the database +func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]polochon.Torrent, error) { + var torrentsDB = []*MovieTorrentDB{} + // Get the torrents from the DB + err := db.Select(&torrentsDB, getMovieTorrentQueryByImdbID, imdbID) + if err != nil { + return nil, err + } + + if len(torrentsDB) == 0 { + return nil, sql.ErrNoRows + } + + // Create polochon Torrents from the MovieTorrentDB + var torrents []polochon.Torrent + for _, torrentDB := range torrentsDB { + torrent := NewTorrentFromMovieTorrentDB(torrentDB) + torrents = append(torrents, *torrent) + } + + return torrents, nil +} + +// UpsertMovieTorrent adds or updates MovieTorrent in db +func UpsertMovieTorrent(db *sqlx.DB, t *polochon.Torrent, imdbID string) error { + mDB := NewMovieTorrentDB(t, imdbID) + _, err := db.NamedQuery(upsertMovieTorrentQuery, mDB) + if err != nil { + return err + } + + return nil +} diff --git a/src/internal/backend/movie_wishlist.go b/src/internal/backend/movie_wishlist.go new file mode 100644 index 0000000..8b2615d --- /dev/null +++ b/src/internal/backend/movie_wishlist.go @@ -0,0 +1,125 @@ +package backend + +import ( + "fmt" + + "github.com/jmoiron/sqlx" +) + +const ( + upsertMovieWishlistQuery = ` + INSERT INTO movies_tracked (imdb_id, user_id) + VALUES ($1, $2) + ON CONFLICT (imdb_id, user_id) + DO UPDATE + SET imdb_id=$1, user_id=$2;` + + isMovieWishlistedQueryByUserID = ` + SELECT + movies.imdb_id + FROM movies INNER JOIN movies_tracked + ON + movies.imdb_id=movies_tracked.imdb_id + AND + movies_tracked.user_id=$2 + WHERE movies.imdb_id=$1;` + + getMovieWishlistQueryByUserID = ` + SELECT + movies.imdb_id + FROM movies INNER JOIN movies_tracked + ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$1;` + + deleteMovieWishlistedQueryByID = `DELETE FROM movies_tracked WHERE imdb_id=$1 AND user_id=$2;` +) + +// MovieWishlist represents the list of movies wishlisted by a user +type MovieWishlist struct { + movies map[string]struct{} +} + +// NewMovieWishlist returns a new MovieWishlist +func NewMovieWishlist() *MovieWishlist { + return &MovieWishlist{ + movies: map[string]struct{}{}, + } +} + +// add adds movie to the MovieWishlist +func (w *MovieWishlist) add(imdbID string) { + w.movies[imdbID] = struct{}{} +} + +// List return a slice of imdbID wishlisted +func (w *MovieWishlist) List() []string { + movies := make([]string, len(w.movies)) + + i := 0 + for imdbID := range w.movies { + movies[i] = imdbID + i++ + } + + return movies +} + +// GetMovieWishlist returns a MovieWishlist obejct for a user +func GetMovieWishlist(db *sqlx.DB, userID string) (*MovieWishlist, error) { + var movies = []string{} + // Get the list of movies + err := db.Select(&movies, getMovieWishlistQueryByUserID, userID) + if err != nil { + return nil, err + } + // Create a new MovieWishlist + wishlist := NewMovieWishlist() + for _, imdbID := range movies { + // add the movie to the wishlist object + wishlist.add(imdbID) + } + + return wishlist, nil +} + +// IsMovieInWishlist returns true if the movie is in the wishlist +func (w *MovieWishlist) IsMovieInWishlist(imdbID string) bool { + _, ok := w.movies[imdbID] + return ok +} + +// IsMovieWishlisted returns true if the movie is wishlisted +func IsMovieWishlisted(db *sqlx.DB, userID, imdbID string) (bool, error) { + var movies = []string{} + // Check if the movie is wishlisted by the user + err := db.Select(&movies, isMovieWishlistedQueryByUserID, imdbID, userID) + if err != nil { + return false, err + } + if len(movies) > 0 { + return true, nil + } + + return false, nil +} + +// AddMovieToWishlist Adds a movie to a user's wishlist in DB +func AddMovieToWishlist(db *sqlx.DB, userID, imdbID string) error { + _, err := db.Exec(upsertMovieWishlistQuery, imdbID, userID) + if err != nil { + return err + } + return nil +} + +// DeleteMovieFromWishlist deletes a movie from a user's wishlist in DB +func DeleteMovieFromWishlist(db *sqlx.DB, userID, imdbID string) error { + r, err := db.Exec(deleteMovieWishlistedQueryByID, imdbID, userID) + if err != nil { + return err + } + count, _ := r.RowsAffected() + if count != 1 { + return fmt.Errorf("Unexpected number of row deleted: %d", count) + } + return nil +} diff --git a/src/internal/backend/movies.go b/src/internal/backend/movies.go new file mode 100644 index 0000000..15880a8 --- /dev/null +++ b/src/internal/backend/movies.go @@ -0,0 +1,119 @@ +package backend + +import ( + "fmt" + "time" + + "github.com/jmoiron/sqlx" + "github.com/lib/pq" + polochon "github.com/odwrtw/polochon/lib" +) + +const ( + upsertMovieQuery = ` + INSERT INTO movies (imdb_id, title, rating, votes, plot, tmdb_id, year, + genres, original_title, runtime, sort_title, tagline) + VALUES (:imdb_id, :title, :rating, :votes, :plot, :tmdb_id, :year, :genres, + :original_title, :runtime, :sort_title, :tagline) + ON CONFLICT (imdb_id) + DO UPDATE + SET imdb_id=:imdb_id, title=:title, rating=:rating, votes=:votes, + plot=:plot, tmdb_id=:tmdb_id, year=:year, genres=:genres, + original_title=:original_title, runtime=:runtime, sort_title=:sort_title, + tagline=:tagline + RETURNING id;` + + getMovieQueryByImdbID = ` + SELECT * + FROM movies + WHERE imdb_id=$1;` +) + +// MovieDB represents the Movie in the DB +type MovieDB struct { + ID string `db:"id"` + ImdbID string `db:"imdb_id"` + TmdbID int `db:"tmdb_id"` + UserID *string `db:"user_id"` + Title string `db:"title"` + OriginalTitle string `db:"original_title"` + SortTitle string `db:"sort_title"` + Rating float32 `db:"rating"` + Votes int `db:"votes"` + Plot string `db:"plot"` + Year int `db:"year"` + Runtime int `db:"runtime"` + Tagline string `db:"tagline"` + Genres pq.StringArray `db:"genres"` + Created time.Time `db:"created_at"` + Updated time.Time `db:"updated_at"` +} + +// GetMovie fills show details of a polochon.Movie +func GetMovie(db *sqlx.DB, pMovie *polochon.Movie) error { + var mDB MovieDB + var err error + if pMovie.ImdbID != "" { + // Get the data from the DB + err = db.QueryRowx(getMovieQueryByImdbID, pMovie.ImdbID).StructScan(&mDB) + } else { + err = fmt.Errorf("Can't get movie details, you have to specify an ImdbID") + } + if err != nil { + return err + } + + // Fills the polochon.Movie from the MovieDB + FillFromDB(&mDB, pMovie) + return nil +} + +// FillFromDB fills a Movie from a MovieDB extracted from the DB +func FillFromDB(mDB *MovieDB, pMovie *polochon.Movie) { + pMovie.ImdbID = mDB.ImdbID + pMovie.Title = mDB.Title + pMovie.Rating = mDB.Rating + pMovie.Votes = mDB.Votes + pMovie.Plot = mDB.Plot + pMovie.TmdbID = mDB.TmdbID + pMovie.Year = mDB.Year + pMovie.OriginalTitle = mDB.OriginalTitle + pMovie.Runtime = mDB.Runtime + pMovie.Genres = mDB.Genres + pMovie.SortTitle = mDB.SortTitle + pMovie.Tagline = mDB.Tagline +} + +// UpsertMovie upsert a polochon Movie in the database +func UpsertMovie(db *sqlx.DB, pMovie *polochon.Movie) error { + mDB := NewMovieDB(pMovie) + _, err := db.NamedQuery(upsertMovieQuery, mDB) + if err != nil { + return err + } + + return nil +} + +// NewMovieDB returns a Movie ready to be put in DB from a +// polochon Movie +func NewMovieDB(m *polochon.Movie) MovieDB { + genres := []string{} + if m.Genres != nil { + genres = m.Genres + } + return MovieDB{ + ImdbID: m.ImdbID, + Title: m.Title, + Rating: m.Rating, + Votes: m.Votes, + Plot: m.Plot, + TmdbID: m.TmdbID, + Year: m.Year, + OriginalTitle: m.OriginalTitle, + Runtime: m.Runtime, + SortTitle: m.SortTitle, + Tagline: m.Tagline, + Genres: genres, + } +} diff --git a/src/internal/backend/show_wishlist.go b/src/internal/backend/show_wishlist.go new file mode 100644 index 0000000..ab0e6fc --- /dev/null +++ b/src/internal/backend/show_wishlist.go @@ -0,0 +1,140 @@ +package backend + +import ( + "fmt" + + "github.com/jmoiron/sqlx" +) + +const ( + upsertShowWishlistQuery = ` + INSERT INTO shows_tracked (imdb_id, user_id, season, episode) + VALUES ($1, $2, $3, $4) + ON CONFLICT (imdb_id, user_id) + DO UPDATE + SET imdb_id=$1, user_id=$2, season=$3, episode=$4;` + + isShowWishlistedQueryByUserID = ` + SELECT + shows.imdb_id + FROM shows INNER JOIN shows_tracked + ON + shows.imdb_id=shows_tracked.imdb_id + AND + shows_tracked.user_id=$2 + WHERE shows.imdb_id=$1;` + + getShowWishlistQueryByUserID = ` + SELECT + shows.imdb_id + FROM shows INNER JOIN shows_tracked + ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$1;` + + deleteShowWishlistedQueryByID = `DELETE FROM shows_tracked WHERE imdb_id=$1 AND user_id=$2;` +) + +// ShowWishlist represents the show wishlist of a user +type ShowWishlist struct { + shows map[string]*WishedShow +} + +// WishedShow represents a wished show of a user +type WishedShow struct { + ImdbID string `db:"imdb_id"` + Season int + Episode int +} + +// NewShowWishlist returns a new ShowWishlist +func NewShowWishlist() *ShowWishlist { + return &ShowWishlist{ + shows: map[string]*WishedShow{}, + } +} + +// add adds a show to the ShowWishlist object +func (w *ShowWishlist) add(imdbID string, season, episode int) { + w.shows[imdbID] = &WishedShow{ + ImdbID: imdbID, + Season: season, + Episode: episode, + } +} + +// List return a slice of WishedShow +func (w *ShowWishlist) List() []*WishedShow { + shows := make([]*WishedShow, len(w.shows)) + + i := 0 + for _, s := range w.shows { + shows[i] = &WishedShow{ + ImdbID: s.ImdbID, + Season: s.Season, + Episode: s.Episode, + } + i++ + } + + return shows +} + +// GetShowWishlist returns a ShowWishlist for a user +func GetShowWishlist(db *sqlx.DB, userID string) (*ShowWishlist, error) { + var wishedShows = []*WishedShow{} + // Get the wishlisted shows + err := db.Select(&wishedShows, getShowWishlistQueryByUserID, userID) + if err != nil { + return nil, err + } + // Create the new object + wishlist := NewShowWishlist() + for _, wishedShow := range wishedShows { + // Add the show to the wishlist + wishlist.add(wishedShow.ImdbID, wishedShow.Season, wishedShow.Episode) + } + + return wishlist, nil +} + +// IsShowInWishlist returns true and the WishedShow if the show is in the +// wishlist +func (w *ShowWishlist) IsShowInWishlist(imdbID string) (*WishedShow, bool) { + show, ok := w.shows[imdbID] + return show, ok +} + +// IsShowWishlisted returns true and the WishedShow if the show is wishlisted +func IsShowWishlisted(db *sqlx.DB, userID, imdbID string) (*WishedShow, error) { + var wishedShows = []*WishedShow{} + err := db.Select(&wishedShows, isShowWishlistedQueryByUserID, imdbID, userID) + if err != nil { + return nil, err + } + if len(wishedShows) != 1 { + return nil, nil + } + + return wishedShows[0], nil +} + +// AddShowToWishlist Adds a show to a user's wishlist +func AddShowToWishlist(db *sqlx.DB, userID, imdbID string, season, episode int) error { + _, err := db.Exec(upsertShowWishlistQuery, imdbID, userID, season, episode) + if err != nil { + return err + } + return nil +} + +// DeleteShowFromWishlist deletes a show from a user's wishlist +func DeleteShowFromWishlist(db *sqlx.DB, userID, imdbID string) error { + r, err := db.Exec(deleteShowWishlistedQueryByID, imdbID, userID) + if err != nil { + return err + } + count, _ := r.RowsAffected() + if count != 1 { + return fmt.Errorf("Unexpected number of row deleted: %d", count) + } + return nil +} diff --git a/src/internal/backend/shows.go b/src/internal/backend/shows.go new file mode 100644 index 0000000..fd4611a --- /dev/null +++ b/src/internal/backend/shows.go @@ -0,0 +1,104 @@ +package backend + +import ( + "fmt" + "time" + + "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" +) + +const ( + upsertShowQuery = ` + INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired) + VALUES (:imdb_id, :title, :rating, :plot, :tvdb_id, :year, :first_aired) + ON CONFLICT (imdb_id) + DO UPDATE + SET imdb_id=:imdb_id, title=:title, rating=:rating, plot=:plot, + tvdb_id=:tvdb_id, year=:year, first_aired=:first_aired + RETURNING id;` + + getShowQueryByImdbID = ` + SELECT * + FROM shows WHERE imdb_id=$1;` +) + +// ShowDB represents the Show in the DB +type ShowDB struct { + ID string `db:"id"` + ImdbID string `db:"imdb_id"` + TvdbID int `db:"tvdb_id"` + TrackedSeason *int `db:"season"` + TrackedEpisode *int `db:"episode"` + Title string `db:"title"` + Rating float32 `db:"rating"` + Plot string `db:"plot"` + Year int `db:"year"` + FirstAired time.Time `db:"first_aired"` + Created time.Time `db:"created_at"` + Updated time.Time `db:"updated_at"` +} + +// UpsertShow a show in the database +func UpsertShow(db *sqlx.DB, s *polochon.Show) error { + sDB := NewShowFromPolochon(s) + // Upsert the show + _, err := db.NamedQuery(upsertShowQuery, sDB) + if err != nil { + return err + } + + for _, e := range s.Episodes { + // Upsert its episodes + err = UpsertEpisode(db, e) + if err != nil { + return err + } + } + return nil +} + +// NewShowFromPolochon returns an ShowDB from a polochon Show +func NewShowFromPolochon(s *polochon.Show) *ShowDB { + sDB := ShowDB{ + ImdbID: s.ImdbID, + TvdbID: s.TvdbID, + Title: s.Title, + Rating: s.Rating, + Plot: s.Plot, + Year: s.Year, + } + if s.FirstAired != nil { + sDB.FirstAired = *s.FirstAired + } + return &sDB +} + +// GetShow fills a show from the DB +func GetShow(db *sqlx.DB, pShow *polochon.Show) error { + var err error + var sDB ShowDB + if pShow.ImdbID != "" { + err = db.QueryRowx(getShowQueryByImdbID, pShow.ImdbID).StructScan(&sDB) + } else { + err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID") + } + if err != nil { + return err + } + + // Fill the show from the ShowDB + FillShowFromDB(&sDB, pShow) + return nil +} + +// FillShowFromDB returns a Show from a ShowDB extracted from the DB +func FillShowFromDB(sDB *ShowDB, pShow *polochon.Show) { + pShow.ImdbID = sDB.ImdbID + pShow.Title = sDB.Title + pShow.Rating = sDB.Rating + pShow.Plot = sDB.Plot + pShow.TvdbID = sDB.TvdbID + pShow.Year = sDB.Year + pShow.FirstAired = &sDB.FirstAired +} diff --git a/src/internal/backend/torrenter.go b/src/internal/backend/torrenter.go new file mode 100644 index 0000000..28037d9 --- /dev/null +++ b/src/internal/backend/torrenter.go @@ -0,0 +1,76 @@ +package backend + +import ( + "database/sql" + "errors" + + "github.com/Sirupsen/logrus" + polochon "github.com/odwrtw/polochon/lib" +) + +// GetTorrents implements the polochon Torrenter interface +func (b *Backend) GetTorrents(media interface{}, log *logrus.Entry) error { + switch t := media.(type) { + case *polochon.ShowEpisode: + return b.GetEpisodeTorrents(t, log) + case *polochon.Movie: + return b.GetMovieTorrents(t, log) + default: + return errors.New("invalid type") + } +} + +// GetMovieTorrents fetch Torrents for movies +func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error { + movieTorrents, err := GetMovieTorrents(b.Database, pmovie.ImdbID) + switch err { + case nil: + log.Debug("torrents found in backend") + case sql.ErrNoRows: + log.Debug("torrent not found in backend") + default: + // Unexpected error + return err + } + log.Debugf( + "got %d torrents for movie %s from backend", + len(movieTorrents), + pmovie.ImdbID, + ) + + // Add the torrents to the movie + pmovie.Torrents = movieTorrents + + return nil +} + +// GetEpisodeTorrents fetch Torrents for episodes +func (b *Backend) GetEpisodeTorrents(pepisode *polochon.ShowEpisode, log *logrus.Entry) error { + episodeTorrents, err := GetEpisodeTorrents( + b.Database, + pepisode.ShowImdbID, + pepisode.Season, + pepisode.Episode, + ) + switch err { + case nil: + log.Debug("torrents found in backend") + case sql.ErrNoRows: + log.Debug("torrent not found in backend") + default: + // Unexpected error + return err + } + log.Debugf( + "got %d torrents for episode S%02dE%02d %s from backend", + len(episodeTorrents), + pepisode.Season, + pepisode.Episode, + pepisode.ShowImdbID, + ) + + // Add the torrents to the episode + pepisode.Torrents = episodeTorrents + + return nil +} diff --git a/src/internal/config/canape.go b/src/internal/config/canape.go index 8234217..cc141c7 100644 --- a/src/internal/config/canape.go +++ b/src/internal/config/canape.go @@ -7,12 +7,14 @@ import ( polochon "github.com/odwrtw/polochon/lib" "github.com/odwrtw/polochon/modules/eztv" "github.com/odwrtw/polochon/modules/tmdb" + "github.com/odwrtw/polochon/modules/trakttv" "github.com/odwrtw/polochon/modules/tvdb" "github.com/odwrtw/polochon/modules/yts" "gopkg.in/yaml.v2" ) +// Config represents the Config of the canape app type Config struct { Authorizer AuthorizerConfig `yaml:"authorizer"` PGDSN string `yaml:"pgdsn"` @@ -22,21 +24,26 @@ type Config struct { // TODO improve the detailers and torrenters configurations TmdbAPIKey string `yaml:"tmdb_api_key"` + MovieExplorers []polochon.Explorer MovieDetailers []polochon.Detailer MovieTorrenters []polochon.Torrenter MovieSearchers []polochon.Searcher + ShowExplorers []polochon.Explorer ShowDetailers []polochon.Detailer ShowTorrenters []polochon.Torrenter ShowSearchers []polochon.Searcher } +// AuthorizerConfig is the config for the authentication type AuthorizerConfig struct { Pepper string `yaml:"pepper"` Cost int `yaml:"cost"` Secret string `yaml:"secret"` } +// Load loads a config file from a path and return a Config object func Load(path string) (*Config, error) { + // Open the file file, err := os.Open(path) if err != nil { return nil, err @@ -45,26 +52,51 @@ func Load(path string) (*Config, error) { cf := &Config{} + // Read it b, err := ioutil.ReadAll(file) if err != nil { return nil, err } + // Unmarshal its content err = yaml.Unmarshal(b, cf) if err != nil { return nil, err } + // Initialize the default values + // Default movie detailers cf.MovieDetailers = []polochon.Detailer{} if cf.TmdbAPIKey != "" { - d, err := tmdb.New(&tmdb.Params{cf.TmdbAPIKey}) + d, err := tmdb.New( + &tmdb.Params{ + ApiKey: cf.TmdbAPIKey, + }, + ) if err != nil { return nil, err } cf.MovieDetailers = append(cf.MovieDetailers, d) } + // Default movie explorers + cf.MovieExplorers = []polochon.Explorer{} + ytsExp, err := yts.NewExplorer() + if err != nil { + return nil, err + } + cf.MovieExplorers = append(cf.MovieExplorers, ytsExp) + if cf.TraktTVClientID != "" { + traktExp, err := trakttv.NewExplorer(&trakttv.Params{ + ClientID: cf.TraktTVClientID, + }) + if err != nil { + return nil, err + } + cf.MovieExplorers = append(cf.MovieExplorers, traktExp) + } + // Default movie torrenters cf.MovieTorrenters = []polochon.Torrenter{} d, err := yts.New() @@ -89,6 +121,23 @@ func Load(path string) (*Config, error) { } cf.ShowSearchers = append(cf.ShowSearchers, showSearcher) + // Default show explorers + cf.ShowExplorers = []polochon.Explorer{} + eztvExp, err := eztv.NewExplorer() + if err != nil { + return nil, err + } + cf.ShowExplorers = append(cf.ShowExplorers, eztvExp) + if cf.TraktTVClientID != "" { + traktExp, err := trakttv.NewExplorer(&trakttv.Params{ + ClientID: cf.TraktTVClientID, + }) + if err != nil { + return nil, err + } + cf.ShowExplorers = append(cf.ShowExplorers, traktExp) + } + // Default show detailers cf.ShowDetailers = []polochon.Detailer{} showDetailer, err := tvdb.NewDetailer() diff --git a/src/internal/external_medias/external_medias.go b/src/internal/external_medias/external_medias.go index 338b078..d2de14c 100644 --- a/src/internal/external_medias/external_medias.go +++ b/src/internal/external_medias/external_medias.go @@ -1,58 +1,189 @@ package extmedias import ( - "github.com/jmoiron/sqlx" - "github.com/lib/pq" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" + "fmt" + + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" + + "github.com/Sirupsen/logrus" + polochon "github.com/odwrtw/polochon/lib" ) -const ( - upsertExternalMediaQuery = ` - INSERT INTO external_medias (type, source, category, ids) - VALUES (:type, :source, :category, :ids) - ON CONFLICT (type, source, category) - DO UPDATE SET type=:type, source=:source, category=:category, ids=:ids - RETURNING id;` +// NewExplorer returns a polochon.Explorer from the list of Explorers in the config +func NewExplorer(env *web.Env, mediaType, name string) (polochon.Explorer, error) { + var explorers []polochon.Explorer + if mediaType == "movie" { + explorers = env.Config.MovieExplorers + } else { + explorers = env.Config.ShowExplorers + } - deleteExternalMediaQuery = `DELETE FROM external_medias WHERE id=:id;` - getExternalMediaQuery = `SELECT * FROM external_medias WHERE type=$1 AND source=$2 AND category=$3 LIMIT 1;` -) - -// Media represents an external media -type Media struct { - sqly.BaseModel - Type string `db:"type"` - Source string `db:"source"` - Category string `db:"category"` - IDs pq.StringArray `db:"ids"` + for _, e := range explorers { + // Check the name of the explorer + if e.Name() == name { + return e, nil + } + } + return nil, fmt.Errorf("no such explorer %s", name) } -// Upsert adds or updates or adds the Media in the database -func (m *Media) Upsert(db *sqlx.DB) error { - var id string - r, err := db.NamedQuery(upsertExternalMediaQuery, m) +// GetMediaIDs will get some media IDs +func GetMediaIDs(env *web.Env, mediaType string, source string, category string) ([]string, error) { + log := env.Log.WithFields(logrus.Fields{ + "mediaType": mediaType, + "source": source, + "category": category, + "function": "extmedias.GetMediaIds", + }) + + // Get the explorer that we need + explorer, err := NewExplorer(env, mediaType, source) if err != nil { - return err - } - for r.Next() { - r.Scan(&id) - } - m.ID = id - return nil -} - -// Delete the media from database or raise an error -func (m *Media) Delete(db *sqlx.DB) error { - _, err := db.NamedExec(deleteExternalMediaQuery, m) - return err -} - -// Get gets a media -func Get(db *sqlx.DB, mtype, msrc, mcat string) (*Media, error) { - m := &Media{} - if err := db.QueryRowx(getExternalMediaQuery, mtype, msrc, mcat).StructScan(m); err != nil { return nil, err } - return m, nil + var ids []string + if mediaType == "movie" { + // Explore new movies + medias, err := explorer.GetMovieList(category, log) + if err != nil { + return nil, err + } + // Add them in the list of ids + for _, media := range medias { + ids = append(ids, media.ImdbID) + } + } else { + // Explore new shows + medias, err := explorer.GetShowList(category, log) + if err != nil { + return nil, err + } + // Add them in the list of ids + for _, media := range medias { + ids = append(ids, media.ImdbID) + } + } + + log.Debugf("got %d medias from %s", len(ids), source) + + // Upserts the new Media entry + media := &backend.Media{ + Type: mediaType, + Source: source, + Category: category, + IDs: ids, + } + + err = media.Upsert(env.Database) + if err != nil { + return nil, err + } + + log.Debug("medias updated in database") + + return ids, nil +} + +// RefreshShows refresh explored shows +// Call all explorers and Refresh +// Retrieve a list of all the ids to be refreshed +// Refresh all the shows + Torrents for all episodes +func RefreshShows(env *web.Env) { + showMap := make(map[string]struct{}) + // Iterate over all show explorers + for _, showExplorer := range env.Config.ShowExplorers { + availableOptions := showExplorer.AvailableShowOptions() + // Iterate over all show explorer options + for _, option := range availableOptions { + env.Log.Infof("refreshing shows %s %s", showExplorer.Name(), option) + + // Get the new explored shows + showIds, err := GetMediaIDs(env, "show", showExplorer.Name(), option) + if err != nil { + env.Log.Warnf("error while refreshing %s/%s : %s", showExplorer.Name(), option, err) + continue + } + + // Add them in a map ( to get every shows only once ) + for _, id := range showIds { + showMap[id] = struct{}{} + } + } + } + env.Log.Info("========================================================") + // Iterate over the map of shows to refresh them + for id := range showMap { + show := shows.New(id) + // Refresh the shows + err := show.Refresh(env, env.Config.ShowDetailers) + if err != nil { + env.Log.Warnf("error while refreshing show %s : %s", id, err) + } + // Iterate over all episodes to refresh their torrents + for _, e := range show.Episodes { + // Refresh the torrents + err := shows.RefreshTorrents(env, e, env.Config.ShowTorrenters) + if err != nil { + env.Log.Error(err) + } + } + } +} + +// RefreshMovies refresh explored Movies +// Call all explorers and Refresh +// Retrieve a list of all the ids to be refreshed +// Refresh all the movies + Torrents +func RefreshMovies(env *web.Env) { + movieMap := make(map[string]struct{}) + // Iterate over all movie explorers + for _, movieExplorer := range env.Config.MovieExplorers { + availableOptions := movieExplorer.AvailableMovieOptions() + // Iterate over all movie explorer options + for _, option := range availableOptions { + env.Log.Infof("refreshing movies %s %s", movieExplorer.Name(), option) + + // Get the new explored movies + movieIds, err := GetMediaIDs(env, "movie", movieExplorer.Name(), option) + if err != nil { + env.Log.Warnf("error while refreshing %s/%s : %s", movieExplorer.Name(), option, err) + continue + } + + // Add them in a map ( to get every movies only once ) + for _, id := range movieIds { + movieMap[id] = struct{}{} + } + } + } + env.Log.Info("========================================================") + // Iterate over the map of movies to refresh them + for id := range movieMap { + movie := movies.New(id, nil, nil, false, env.Config.PublicDir) + // Refresh the movie + err := movie.Refresh(env, env.Config.MovieDetailers) + if err != nil { + env.Log.Warnf("error while refreshing movie %s : %s", id, err) + } + // Refresh its torrents + err = movie.RefreshTorrents(env, env.Config.MovieTorrenters) + if err != nil { + env.Log.Warnf("error while refreshing movie torrents %s : %s", id, err) + } + } +} + +// Refresh refreshes explore new shows and refresh them +func Refresh(env *web.Env) { + env.Log.Debugf("refreshing shows") + RefreshShows(env) + env.Log.Debugf("done refreshing shows") + + env.Log.Debugf("refreshing movies") + RefreshMovies(env) + env.Log.Debugf("done refreshing movies") } diff --git a/src/internal/external_medias/external_medias_test.go b/src/internal/external_medias/external_medias_test.go deleted file mode 100644 index 519823a..0000000 --- a/src/internal/external_medias/external_medias_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package extmedias - -import ( - "database/sql" - "fmt" - "os" - "testing" - - _ "github.com/mattes/migrate/driver/postgres" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - - "github.com/jmoiron/sqlx" -) - -var db *sqlx.DB -var pgdsn string - -func init() { - var err error - - pgdsn = os.Getenv("POSTGRES_DSN") - db, err = sqlx.Connect("postgres", pgdsn) - - if err != nil { - fmt.Printf("Unavailable PG tests:\n %v\n", err) - os.Exit(1) - } -} - -func TestAddExternalMedias(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - media := &Media{ - Type: "movie", - Source: "trakttv", - Category: "trending", - IDs: []string{"1", "2", "3"}, - } - - // Add it - if err := media.Upsert(db); err != nil { - t.Fatalf("failed to add external media: %q", err) - } - - // Update the IDs - media.IDs = []string{"1", "2", "3", "4"} - if err := media.Upsert(db); err != nil { - t.Fatalf("failed to update the external media: %q", err) - } - - // Find it - m, err := Get(db, media.Type, media.Source, media.Category) - if err != nil { - t.Fatalf("failed get the external media: %q", err) - } - - // Small, almost useless check - if len(m.IDs) != 4 { - t.Fatalf("the media should have 4 ids, only %d found", len(m.IDs)) - } - - // Delete it - if err := media.Delete(db); err != nil { - t.Fatalf("failed to delete the external media: %q", err) - } - - // Search it and expect that it's not found - m, err = Get(db, media.Type, media.Source, media.Category) - if err == nil { - t.Fatal("there should be an error, was the external media deleted ?") - } - if err != sql.ErrNoRows { - t.Fatalf("unexpected error: %q", err) - } - }) -} diff --git a/src/internal/external_medias/handlers.go b/src/internal/external_medias/handlers.go index 83a55ed..4d1dd04 100644 --- a/src/internal/external_medias/handlers.go +++ b/src/internal/external_medias/handlers.go @@ -1,130 +1,107 @@ package extmedias import ( - "database/sql" "errors" - "fmt" "net/http" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "github.com/Sirupsen/logrus" polochon "github.com/odwrtw/polochon/lib" - eztvExplorer "github.com/odwrtw/polochon/modules/eztv" - traktExplorer "github.com/odwrtw/polochon/modules/trakttv" - ytsExplorer "github.com/odwrtw/polochon/modules/yts" ) -// GetMediaIDs will get some media IDs -func GetMediaIDs(env *web.Env, mediaType string, source string, category string, force bool) ([]string, error) { +// RefreshHandler refresh the explored movies +func RefreshHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { log := env.Log.WithFields(logrus.Fields{ - "mediaType": mediaType, - "source": source, - "category": category, - "function": "extmedias.GetMediaIds", + "function": "extmedias.RefreshHandler", }) + log.Debugf("refreshing shows and movies") + Refresh(env) + log.Debugf("done refreshing shows and movies") + return nil +} - var err error - media, err := Get(env.Database, mediaType, source, category) - switch err { - case nil: - log.Debugf("%s medias found in database", mediaType) - if !force { - log.Debug("returning medias from db") - return media.IDs, nil - } - case sql.ErrNoRows: - log.Debugf("%s medias not found in database", mediaType) - default: - // Unexpected error - return nil, err - } +// RefreshMoviesHandler refresh the explored movies +func RefreshMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "extmedias.RefreshMoviesHandler", + }) + log.Debugf("refreshing movies") + RefreshMovies(env) + log.Debugf("done refreshing movies") + return nil +} - explorer, err := NewExplorer(env, source) - if err != nil { - return nil, err - } - - var ids []string - if mediaType == "movie" { - medias, err := explorer.GetMovieList(polochon.ExploreByRate, log) - if err != nil { - return nil, err - } - for _, media := range medias { - ids = append(ids, media.ImdbID) - } - } else { - medias, err := explorer.GetShowList(polochon.ExploreByRate, log) - if err != nil { - return nil, err - } - for i, media := range medias { - if i > 5 { - break - } - ids = append(ids, media.ImdbID) - } - } - - log.Debugf("got %d medias from %s", len(ids), source) - - media = &Media{ - Type: mediaType, - Source: source, - Category: category, - IDs: ids, - } - - err = media.Upsert(env.Database) - if err != nil { - return nil, err - } - - log.Debug("medias updated in database") - - return ids, nil +// RefreshShowsHandler refresh the explored shows +func RefreshShowsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + log := env.Log.WithFields(logrus.Fields{ + "function": "extmedias.RefreshShowsHandler", + }) + log.Debugf("refreshing shows") + RefreshShows(env) + log.Debugf("done refreshing shows") + return nil } // GetMovies get some movies -func GetMovies(env *web.Env, user *users.User, source string, category string, force bool) ([]*movies.Movie, error) { - movieIds, err := GetMediaIDs(env, "movie", source, category, force) +func GetMovies(env *web.Env, user *users.User, source string, category string) ([]*movies.Movie, error) { + log := env.Log.WithFields(logrus.Fields{ + "source": source, + "category": category, + "function": "extmedias.GetMovies", + }) + log.Debugf("getting movies") + media, err := backend.Explore(env.Database, "movie", source, category) if err != nil { return nil, err } - // Get the URLs from polochon, we don't really care if it fails for now - var urls map[string]string - urls, err = movies.GetPolochonMoviesURLs(user) + // Create a papi client + client, err := user.NewPapiClient() if err != nil { - env.Log.Errorf("error while getting polochon movies url: %s", err) + return nil, err + } + + // Get the user's polochon movies + pMovies, err := client.GetMovies() + if err != nil { + return nil, err + } + + // Get the user's wishlisted movies + moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID) + if err != nil { + return nil, err } movieList := []*movies.Movie{} - for _, id := range movieIds { - movie := movies.New(id) - err := movie.GetDetails(env, user, force) + // Fill all the movies infos from the list of IDs + for _, id := range media.IDs { + pMovie, _ := pMovies.Has(id) + movie := movies.New(id, client, pMovie, moviesWishlist.IsMovieInWishlist(id), env.Config.PublicDir) + // First check in the DB + before := []polochon.Detailer{env.Backend.Detailer} + // Then with the default detailers + after := env.Config.MovieDetailers + err := movie.GetAndFetch(env, before, after) if err != nil { env.Log.Errorf("error while getting movie details : %s", err) continue } - err = movie.GetTorrents(env, force) + + // Look only for torrents in db + torrenters := []polochon.Torrenter{env.Backend.Torrenter} + err = movie.GetTorrents(env, torrenters) if err != nil { env.Log.Errorf("error while getting movie torrents : %s", err) continue } - if urls != nil { - if url, ok := urls[id]; ok { - movie.PolochonURL = url - } - } - movieList = append(movieList, movie) } return movieList, nil @@ -132,31 +109,59 @@ func GetMovies(env *web.Env, user *users.User, source string, category string, f // GetShows get some shows func GetShows(env *web.Env, user *users.User, source string, category string, force bool) ([]*shows.Show, error) { - showIds, err := GetMediaIDs(env, "show", source, category, force) + log := env.Log.WithFields(logrus.Fields{ + "source": source, + "category": category, + "function": "extmedias.GetShows", + }) + log.Debugf("getting shows") + // Get the user's polochon config + media, err := backend.Explore(env.Database, "show", source, category) + if err != nil { + return nil, err + } + + // Create a papi client + client, err := user.NewPapiClient() + if err != nil { + return nil, err + } + + // Get the polochon's shows + pShows, err := client.GetShows() + if err != nil { + return nil, err + } + + // Get the user's wishlisted shows + wShows, err := backend.GetShowWishlist(env.Database, user.ID) if err != nil { return nil, err } showList := []*shows.Show{} - for _, id := range showIds { - show := shows.New(id) - err := show.GetDetails(env, user, force) + // Fill all the shows infos from the list of IDs + for _, id := range media.IDs { + pShow, _ := pShows.Has(id) + wShow, _ := wShows.IsShowInWishlist(id) + show := shows.NewWithClient(id, client, pShow, wShow, env.Config.PublicDir) + + // First check in the DB + before := []polochon.Detailer{env.Backend.Detailer} + // Then with the default detailers + after := env.Config.ShowDetailers + err := show.GetAndFetch(env, before, after) if err != nil { env.Log.Errorf("error while getting show details : %s", err) continue } - err = show.GetTorrents(env, force) - if err != nil { - env.Log.Errorf("error while getting show torrents : %s", err) - continue - } showList = append(showList, show) } return showList, nil } -// Explore will explore some movies -func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error { +// ExploreMovies will explore some movies from the db +func ExploreMovies(env *web.Env, w http.ResponseWriter, r *http.Request) error { err := r.ParseForm() if err != nil { return err @@ -181,7 +186,7 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error { } // Get the medias without trying to refresh them - movies, err := GetMovies(env, user, source, category, false) + movies, err := GetMovies(env, user, source, category) if err != nil { return env.RenderError(w, err) } @@ -222,94 +227,3 @@ func ExploreShows(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderJSON(w, shows) } - -// MediaSources represents the implemented media sources -var MediaSources = []string{ - "trakttv", - "yts", -} - -// ShowMediaSources represents the implemented media sources for shows -var ShowMediaSources = []string{ - "eztv", -} - -// Refresh will refresh the movie list -func Refresh(env *web.Env, w http.ResponseWriter, r *http.Request) error { - env.Log.Debugf("refreshing infos ...") - source := r.FormValue("source") - if source == "" { - source = "yts" - } - - category := r.FormValue("category") - if category == "" { - category = "popular" - } - - v := auth.GetCurrentUser(r, env.Log) - user, ok := v.(*users.User) - if !ok { - return env.RenderError(w, fmt.Errorf("invalid user type")) - } - - // We'll refresh the medias for each sources - for _, source := range MediaSources { - env.Log.Debugf("refreshing %s", source) - // GetMedias and refresh them - _, err := GetMovies(env, user, source, category, true) - if err != nil { - return env.RenderError(w, err) - } - } - - return env.RenderJSON(w, map[string]string{"message": "Refresh is done"}) -} - -// RefreshShows will refresh the movie list -func RefreshShows(env *web.Env, w http.ResponseWriter, r *http.Request) error { - env.Log.Debugf("refreshing shows ...") - source := r.FormValue("source") - if source == "" { - source = "eztv" - } - - category := r.FormValue("category") - if category == "" { - category = "popular" - } - - v := auth.GetCurrentUser(r, env.Log) - user, ok := v.(*users.User) - if !ok { - return env.RenderError(w, fmt.Errorf("invalid user type")) - } - - // We'll refresh the medias for each sources - for _, source := range ShowMediaSources { - env.Log.Debugf("refreshing %s", source) - // GetMedias and refresh them - _, err := GetShows(env, user, source, category, true) - if err != nil { - return env.RenderError(w, err) - } - } - - return env.RenderJSON(w, map[string]string{"message": "Refresh is done"}) -} - -// NewExplorer returns a polochon.Explorer -func NewExplorer(env *web.Env, source string) (polochon.Explorer, error) { - switch source { - case "trakttv": - return traktExplorer.NewExplorer(&traktExplorer.Params{ - ClientID: env.Config.TraktTVClientID, - }) - case "yts": - return ytsExplorer.NewExplorer() - case "eztv": - return eztvExplorer.NewExplorer() - default: - return nil, fmt.Errorf("unknown explorer") - } -} diff --git a/src/internal/movies/handlers.go b/src/internal/movies/handlers.go index 5cda431..39a83c1 100644 --- a/src/internal/movies/handlers.go +++ b/src/internal/movies/handlers.go @@ -4,9 +4,8 @@ import ( "encoding/json" "errors" "fmt" - "net" + "log" "net/http" - "net/url" "github.com/Sirupsen/logrus" "github.com/gorilla/mux" @@ -15,84 +14,28 @@ import ( "github.com/odwrtw/polochon/modules/pam" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" ) -// ErrPolochonUnavailable is an error returned if the polochon server is not available -var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address") - -func getPolochonMovies(user *users.User) ([]*Movie, error) { - movies := []*Movie{} - - var polochonConfig config.UserPolochon - err := user.GetConfig("polochon", &polochonConfig) - if err != nil { - return movies, err - } - - client, err := papi.New(polochonConfig.URL) - if err != nil { - return movies, err - } - - if polochonConfig.Token != "" { - client.SetToken(polochonConfig.Token) - } - - pmovies, err := client.GetMovies() - if err != nil { - // Catch network error for accessing specified polochon address - if uerr, ok := err.(*url.Error); ok { - if nerr, ok := uerr.Err.(*net.OpError); ok { - if nerr.Op == "dial" { - return movies, ErrPolochonUnavailable - } - - } - } - return movies, err - } - for _, pmovie := range pmovies { - movie := New(pmovie.ImdbID) - movie.PolochonURL, err = client.DownloadURL(&papi.Movie{ImdbID: movie.ImdbID}) - if err != nil { - return nil, err - } - movies = append(movies, movie) - } - return movies, nil -} - -// GetPolochonMoviesURLs returns the polochon urls associated with an imdb id -func GetPolochonMoviesURLs(user *users.User) (map[string]string, error) { - movies, err := getPolochonMovies(user) - if err != nil { - return nil, err - } - - urls := make(map[string]string, len(movies)) - for _, movie := range movies { - urls[movie.ImdbID] = movie.PolochonURL - } - - return urls, nil -} - -// FromPolochon will returns movies from Polochon -func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { +// PolochonMoviesHandler will returns movies from Polochon +func PolochonMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + // Get the user from the request v := auth.GetCurrentUser(r, env.Log) user, ok := v.(*users.User) if !ok { - return fmt.Errorf("invalid user type") + return env.RenderError(w, fmt.Errorf("invalid user type")) } - movies, err := getPolochonMovies(user) + // Get the polochon movies of the user + movies, err := getPolochonMovies(user, env) if err != nil { return env.RenderError(w, err) } + // Create a new polochon Detailer to get infos about the movies we just got var polochonConfig config.UserPolochon err = user.GetConfig("polochon", &polochonConfig) if err != nil { @@ -104,9 +47,13 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { Token: polochonConfig.Token, }) + // Get details with the polochon Detailer for each polochon.Movies we have for _, m := range movies { - m.Detailers = []polochon.Detailer{detailer} - err := m.GetDetails(env, user, false) + // First try from the db + first := []polochon.Detailer{env.Backend.Detailer} + // Then try from the polochon detailer + detailers := []polochon.Detailer{detailer} + err := m.GetAndFetch(env, first, detailers) if err != nil { env.Log.Error(err) } @@ -115,20 +62,47 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderJSON(w, movies) } -// GetDetailsHandler retrieves details for a movie -func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { +// RefreshMovieHandler refreshes details for a movie +func RefreshMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) id := vars["id"] + // Get the user v := auth.GetCurrentUser(r, env.Log) user, ok := v.(*users.User) if !ok { return fmt.Errorf("invalid user type") } - m := New(id) - if err := m.GetDetails(env, user, true); err != nil { - return err + // Create a new papi client + client, err := user.NewPapiClient() + if err != nil { + return env.RenderError(w, err) + } + + // Get the polochon movie + pMovie, err := client.GetMovie(id) + if err != nil && err != papi.ErrResourceNotFound { + log.Println("Error getting movie ", err) + } + + // Check if the movie is wishlisted + isWishlisted, err := backend.IsMovieWishlisted(env.Database, user.ID, id) + if err != nil { + log.Println("Error getting wishlisted movie ", err) + } + + // Create a new movie + m := New(id, client, pMovie, isWishlisted, env.Config.PublicDir) + + // Refresh the movie's infos + if err := m.Refresh(env, env.Config.MovieDetailers); err != nil { + env.Log.Error(err) + } + + // Refresh the movie's torrents + if err := m.RefreshTorrents(env, env.Config.MovieTorrenters); err != nil { + env.Log.Error(err) } return env.RenderJSON(w, m) @@ -154,8 +128,27 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderError(w, errors.New("invalid user")) } + // Create a new papi client + client, err := user.NewPapiClient() + if err != nil { + return env.RenderError(w, err) + } + + // Get the user's polochon movies + pMovies, err := client.GetMovies() + if err != nil { + return env.RenderError(w, err) + } + + // Get the user's wishlisted movies + moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID) + if err != nil { + return env.RenderError(w, err) + } + var movies []*polochon.Movie searchers := env.Config.MovieSearchers + // Search for the movie with all the Searchers for _, searcher := range searchers { result, err := searcher.SearchMovie(data.Key, env.Log) if err != nil { @@ -165,42 +158,45 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error { movies = append(movies, result...) } - // Get the URLs from polochon, we don't really care if it fails for now - var urls map[string]string - urls, err = GetPolochonMoviesURLs(user) - if err != nil { - env.Log.Errorf("error while getting polochon movies url: %s", err) - } - env.Log.Debugf("got %d movies doing search %q", len(movies), data.Key) movieList := []*Movie{} + // For each movie found, fill the details for _, m := range movies { - movie := New(m.ImdbID) - err := movie.GetDetails(env, user, false) + pMovie, _ := pMovies.Has(m.ImdbID) + movie := New( + m.ImdbID, + client, + pMovie, + moviesWishlist.IsMovieInWishlist(m.ImdbID), + env.Config.PublicDir, + ) + + // First check in the DB + before := []polochon.Detailer{env.Backend.Detailer} + // Then with the default detailers + after := env.Config.MovieDetailers + err := movie.GetAndFetch(env, before, after) if err != nil { env.Log.Errorf("error while getting movie details : %s", err) continue } - err = movie.GetTorrents(env, false) + + // Look only for torrents in db + torrenters := []polochon.Torrenter{env.Backend.Torrenter} + err = movie.GetTorrents(env, torrenters) if err != nil { env.Log.Errorf("error while getting movie torrents : %s", err) continue } - if urls != nil { - if url, ok := urls[m.ImdbID]; ok { - movie.PolochonURL = url - } - } - movieList = append(movieList, movie) } return env.RenderJSON(w, movieList) } -// DeleteHandler deletes the movie from polochon -func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { +// PolochonDeleteHandler deletes the movie from polochon +func PolochonDeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) id := vars["id"] @@ -210,27 +206,20 @@ func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { }) log.Debugf("deleting movie") + // Get the user v := auth.GetCurrentUser(r, env.Log) user, ok := v.(*users.User) if !ok { return env.RenderError(w, errors.New("invalid user type")) } - var polochonConfig config.UserPolochon - err := user.GetConfig("polochon", &polochonConfig) + // Create a new papi client + client, err := user.NewPapiClient() if err != nil { return env.RenderError(w, err) } - client, err := papi.New(polochonConfig.URL) - if err != nil { - return env.RenderError(w, err) - } - - if polochonConfig.Token != "" { - client.SetToken(polochonConfig.Token) - } - + // Delete the movie return client.Delete(&papi.Movie{ImdbID: id}) } @@ -245,8 +234,7 @@ func AddToWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderError(w, errors.New("invalid user type")) } - m := New(id) - if err := m.AddToWishlist(env, user); err != nil { + if err := backend.AddMovieToWishlist(env.Database, user.ID, id); err != nil { return env.RenderError(w, err) } @@ -264,8 +252,7 @@ func DeleteFromWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) er return env.RenderError(w, errors.New("invalid user type")) } - m := New(id) - if err := m.DeleteFromWishlist(env, user); err != nil { + if err := backend.DeleteMovieFromWishlist(env.Database, user.ID, id); err != nil { return env.RenderError(w, err) } @@ -280,10 +267,55 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er return env.RenderError(w, errors.New("invalid user type")) } - movies, err := GetWishlist(env, user) + // Create a new papi client + client, err := user.NewPapiClient() if err != nil { return env.RenderError(w, err) } - return env.RenderJSON(w, movies) + // Get the user's polochon movies + pMovies, err := client.GetMovies() + if err != nil { + return env.RenderError(w, err) + } + + // Get the user's wishlisted movies + moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID) + if err != nil { + return env.RenderError(w, err) + } + + movieList := []*Movie{} + // For each movie found, fill the details + for _, imdbID := range moviesWishlist.List() { + pMovie, _ := pMovies.Has(imdbID) + movie := New( + imdbID, + client, + pMovie, + moviesWishlist.IsMovieInWishlist(imdbID), + env.Config.PublicDir, + ) + // First check in the DB + before := []polochon.Detailer{env.Backend.Detailer} + // Then with the default detailers + after := env.Config.MovieDetailers + err := movie.GetAndFetch(env, before, after) + if err != nil { + env.Log.Errorf("error while getting movie details : %s", err) + continue + } + + // Look only for torrents in db + torrenters := []polochon.Torrenter{env.Backend.Torrenter} + err = movie.GetTorrents(env, torrenters) + if err != nil { + env.Log.Errorf("error while getting movie torrents : %s", err) + continue + } + + movieList = append(movieList, movie) + } + + return env.RenderJSON(w, movieList) } diff --git a/src/internal/movies/movies.go b/src/internal/movies/movies.go index 34ac7fb..d2aac66 100644 --- a/src/internal/movies/movies.go +++ b/src/internal/movies/movies.go @@ -2,331 +2,198 @@ package movies import ( "database/sql" + "encoding/json" "fmt" "os" "path/filepath" - "time" "github.com/Sirupsen/logrus" - "github.com/jmoiron/sqlx" - "github.com/lib/pq" + "github.com/odwrtw/papi" "github.com/odwrtw/polochon/lib" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" ) -const ( - upsertMovieQuery = ` - INSERT INTO movies (imdb_id, title, rating, votes, plot, tmdb_id, year, - genres, original_title, runtime, sort_title, tagline) - VALUES (:imdb_id, :title, :rating, :votes, :plot, :tmdb_id, :year, :genres, - :original_title, :runtime, :sort_title, :tagline) - ON CONFLICT (imdb_id) - DO UPDATE - SET imdb_id=:imdb_id, title=:title, rating=:rating, votes=:votes, - plot=:plot, tmdb_id=:tmdb_id, year=:year, genres=:genres, - original_title=:original_title, runtime=:runtime, sort_title=:sort_title, - tagline=:tagline - RETURNING id;` - - getUserMovieQueryByImdbID = ` - SELECT - movies.id, - movies.title, - movies.imdb_id, - movies.tmdb_id, - movies.votes, - movies.rating, - movies.plot, - movies.year, - movies.original_title, - movies.runtime, - movies.genres, - movies.sort_title, - movies.tagline, - movies.created_at, - movies.updated_at, - movies_tracked.user_id - FROM movies LEFT JOIN movies_tracked - ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$2 - WHERE movies.imdb_id=$1;` - - getUserMovieQueryByID = ` - SELECT - movies.id, - movies.title, - movies.imdb_id, - movies.tmdb_id, - movies.votes, - movies.rating, - movies.plot, - movies.year, - movies.original_title, - movies.runtime, - movies.genres, - movies.sort_title, - movies.tagline, - movies.created_at, - movies.updated_at, - movies_tracked.user_id - FROM movies LEFT JOIN movies_tracked - ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$2 - WHERE movies.id=$1;` - - getMovieQueryByImdbID = ` - SELECT * - FROM movies - WHERE imdb_id=$1;` - - getMovieQueryByID = ` - SELECT * - FROM movies - WHERE movies.id=$1;` - - deleteMovieQuery = `DELETE FROM movies WHERE id=$1;` -) - -// MovieDB represents the Movie in the DB -type MovieDB struct { - ID string `db:"id"` - ImdbID string `db:"imdb_id"` - TmdbID int `db:"tmdb_id"` - UserID *string `db:"user_id"` - Title string `db:"title"` - OriginalTitle string `db:"original_title"` - SortTitle string `db:"sort_title"` - Rating float32 `db:"rating"` - Votes int `db:"votes"` - Plot string `db:"plot"` - Year int `db:"year"` - Runtime int `db:"runtime"` - Tagline string `db:"tagline"` - Genres pq.StringArray `db:"genres"` - Created time.Time `db:"created_at"` - Updated time.Time `db:"updated_at"` -} - -// NewMovieDB returns a Movie ready to be put in DB from a -// Movie -func NewMovieDB(m *Movie) MovieDB { - genres := []string{} - if m.Genres != nil { - genres = m.Genres - } - return MovieDB{ - ID: m.ID, - ImdbID: m.ImdbID, - Title: m.Title, - Rating: m.Rating, - Votes: m.Votes, - Plot: m.Plot, - TmdbID: m.TmdbID, - Year: m.Year, - OriginalTitle: m.OriginalTitle, - Runtime: m.Runtime, - SortTitle: m.SortTitle, - Tagline: m.Tagline, - Genres: genres, - Created: m.Created, - Updated: m.Updated, - } -} - -// FillFromDB returns a Movie from a MovieDB extracted from the DB -func (m *Movie) FillFromDB(mDB *MovieDB) { - m.Created = mDB.Created - m.Updated = mDB.Updated - m.ID = mDB.ID - m.ImdbID = mDB.ImdbID - m.Title = mDB.Title - m.Rating = mDB.Rating - m.Votes = mDB.Votes - m.Plot = mDB.Plot - m.TmdbID = mDB.TmdbID - m.Year = mDB.Year - m.OriginalTitle = mDB.OriginalTitle - m.Runtime = mDB.Runtime - m.Genres = mDB.Genres - m.SortTitle = mDB.SortTitle - m.Tagline = mDB.Tagline - if mDB.UserID != nil { - m.Wishlisted = true - } -} - // Movie represents a movie type Movie struct { - sqly.BaseModel - polochon.Movie - Wishlisted bool `json:"wishlisted"` - PolochonURL string `json:"polochon_url"` - PosterURL string `json:"poster_url"` + *polochon.Movie + client *papi.Client + pMovie *papi.Movie + publicDir string + Wishlisted bool `json:"wishlisted"` } -// New returns a new Movie with an ImDB id -func New(imdbID string) *Movie { +// MarshalJSON implements the Marshal interface +func (m *Movie) MarshalJSON() ([]byte, error) { + type Alias Movie + + var downloadURL string + // If the episode is present, fill the downloadURL + if m.pMovie != nil { + downloadURL, _ = m.client.DownloadURL(m.pMovie) + } + + // Marshal the movie with its polochon_url + movieToMarshal := &struct { + *Alias + PolochonURL string `json:"polochon_url"` + PosterURL string `json:"poster_url"` + }{ + Alias: (*Alias)(m), + PolochonURL: downloadURL, + PosterURL: m.PosterURL(), + } + + return json.Marshal(movieToMarshal) +} + +// New returns a new Movie with all the needed infos +func New(imdbID string, client *papi.Client, pMovie *papi.Movie, isWishlisted bool, publicDir string) *Movie { return &Movie{ - Movie: polochon.Movie{ + client: client, + pMovie: pMovie, + publicDir: publicDir, + Wishlisted: isWishlisted, + Movie: &polochon.Movie{ ImdbID: imdbID, }, } } -// Get returns show details in database from id or imdbid or an error -func (m *Movie) Get(env *web.Env, user *users.User) error { - var mDB MovieDB - var err error - if m.ID != "" { - err = env.Database.QueryRowx(getUserMovieQueryByID, m.ID, user.ID).StructScan(&mDB) - } else if m.ImdbID != "" { - err = env.Database.QueryRowx(getUserMovieQueryByImdbID, m.ImdbID, user.ID).StructScan(&mDB) - } else { - err = fmt.Errorf("Can't get movie details, you have to specify an ID or ImdbID") - } - if err != nil { - return err - } - - // Set the poster url - m.PosterURL = m.GetPosterURL(env) - - m.FillFromDB(&mDB) - return nil -} - -// GetMovie returns show details in database from id or imdbid or an error -func (m *Movie) GetMovie(db *sqlx.DB) error { - var mDB MovieDB - var err error - if m.ID != "" { - err = db.QueryRowx(getMovieQueryByID, m.ID).StructScan(&mDB) - } else if m.ImdbID != "" { - err = db.QueryRowx(getMovieQueryByImdbID, m.ImdbID).StructScan(&mDB) - } else { - err = fmt.Errorf("Can't get movie details, you have to specify an ID or ImdbID") - } - if err != nil { - return err - } - - // Set the poster url - // m.PosterURL = m.GetPosterURL(env) - - m.FillFromDB(&mDB) - return nil -} -// if not exists, use polochon.Detailer and save informations in the database -// for future use -// -// If force is used, the detailer will be used even if the movie is found in -// database -func (m *Movie) GetDetails(env *web.Env, user *users.User, force bool) error { +// GetDetails retrieves details for the movie with the given detailers +func (m *Movie) GetDetails(env *web.Env, detailers []polochon.Detailer) error { log := env.Log.WithFields(logrus.Fields{ "imdb_id": m.ImdbID, "function": "movies.GetDetails", }) log.Debugf("getting details") - if len(m.Detailers) == 0 { - m.Detailers = env.Config.MovieDetailers - } - - var err error - err = m.Get(env, user) - switch err { - case nil: - log.Debug("movie found in database") - case sql.ErrNoRows: - log.Debug("movie not found in database") - default: - // Unexpected error - return err - } - // If force is not specified, don't go further - if !force { - // Will return ErrNoRows if the movie wasn't found - return err - } + m.Detailers = detailers // GetDetail - err = m.Movie.GetDetails(env.Log) + err := m.Movie.GetDetails(env.Log) if err != nil { return err } log.Debug("got details from detailers") - err = m.Upsert(env.Database) - if err != nil { - log.Debug("error while doing db func") - return err - } - - log.Debug("movie added in database") - - // Download poster - err = web.Download(m.Thumb, m.imgFile(env)) - if err != nil { - return err - } - - log.Debug("poster downloaded") - - // Set the poster url - m.PosterURL = m.GetPosterURL(env) - return nil } -// GetTorrents retrieves torrents for the movie, first try to get info from db, -// if not exists, use polochon.Torrenter and save informations in the database -// for future use -// -// If force is used, the torrenter will be used even if the torrent is found in +// GetAndFetch retrieves details for the movie with the given +// detailers 'before' +// If found, return +// If not, retrives details with the detailers 'after' and update them in // database -func (m *Movie) GetTorrents(env *web.Env, force bool) 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", + }) + + // Try to get details with the first batch of Detailers + err := m.GetDetails(env, before) + if err == nil { + log.Debug("movie found in first try") + return nil + } + log.Debugf("movie not found in database: %s", err) + + // If not found, try the second batch and upsert + return m.Refresh(env, after) +} + +// Refresh retrieves details for the movie with the given detailers +// and update them in database +func (m *Movie) Refresh(env *web.Env, detailers []polochon.Detailer) error { + // Refresh + err := m.GetDetails(env, detailers) + if err != nil { + return err + } + + // Download poster + err = web.Download(m.Thumb, m.imgFile()) + if err != nil { + return err + } + + env.Log.Debug("poster downloaded") + + // If found, update in database + return backend.UpsertMovie(env.Database, m.Movie) +} + +// GetTorrents retrieves torrents for the movie with the given torrenters +func (m *Movie) GetTorrents(env *web.Env, torrenters []polochon.Torrenter) error { log := env.Log.WithFields(logrus.Fields{ "imdb_id": m.ImdbID, "function": "movies.GetTorrents", }) log.Debugf("getting torrents") - if len(m.Torrenters) == 0 { - m.Torrenters = env.Config.MovieTorrenters - } + m.Torrenters = torrenters - movieTorrents, err := torrents.GetMovieTorrents(env.Database, m.ImdbID) - switch err { - case nil: - log.Debug("torrents found in database") - case sql.ErrNoRows: - log.Debug("torrent not found in database") - default: - // Unexpected error - return err - } - if !force { - log.Debugf("returning %d torrents from db", len(movieTorrents)) - // Add the torrents to the movie - for _, t := range movieTorrents { - m.Torrents = append(m.Torrents, t.Torrent) - } - return nil - } - - err = m.Movie.GetTorrents(env.Log) + err := m.Movie.GetTorrents(env.Log) if err != nil { return err } log.Debugf("got %d torrents from torrenters", len(m.Movie.Torrents)) + return nil +} + +// GetAndFetchTorrents retrieves torrents for the movie with the given +// torrenters 'before' +// If found, return +// If not, retrives torrents with the torrenters 'after' and update them in +// database +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", + }) + + // Try to get torrents with the first batch of Torrenters + err := m.GetTorrents(env, before) + switch err { + case nil: + log.Debug("movie torrent's found in first try") + return nil + case sql.ErrNoRows: + log.Debug("movie's torrents not found in database") + default: + // Unexpected error + return err + } + + // If not found, try the second batch and upsert + return m.RefreshTorrents(env, after) +} + +// RefreshTorrents retrieves torrents for the movie with the given torrenters +// and update them in database +func (m *Movie) RefreshTorrents(env *web.Env, torrenters []polochon.Torrenter) error { + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": m.ImdbID, + "function": "movies.RefreshTorrents", + }) + + // Get torrents with de torrenters + err := m.GetTorrents(env, torrenters) + if err != nil { + return err + } + + log.Debugf("got %d torrents from torrenters", len(m.Movie.Torrents)) + + // Update them in database for _, t := range m.Movie.Torrents { - torrent := torrents.NewMovie(m.ImdbID, t) - err = torrent.Upsert(env.Database) + err = backend.UpsertMovieTorrent(env.Database, &t, m.ImdbID) if err != nil { log.Error("error while adding torrent", err) continue @@ -336,51 +203,59 @@ func (m *Movie) GetTorrents(env *web.Env, force bool) error { return nil } -// Upsert a movie in the database -func (m *Movie) Upsert(db *sqlx.DB) error { - mDB := NewMovieDB(m) - var id string - r, err := db.NamedQuery(upsertMovieQuery, mDB) - if err != nil { - return err - } - for r.Next() { - r.Scan(&id) - } - m.ID = id - - return nil -} - -// Delete movie from database -func (m *Movie) Delete(db *sqlx.DB) error { - r, err := db.Exec(deleteMovieQuery, m.ID) - if err != nil { - return err - } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} - // imgURL returns the default image url -func (m *Movie) imgURL(env *web.Env) string { +func (m *Movie) imgURL() string { return fmt.Sprintf("img/movies/%s.jpg", m.ImdbID) } // imgFile returns the image location on disk -func (m *Movie) imgFile(env *web.Env) string { - return filepath.Join(env.Config.PublicDir, m.imgURL(env)) +func (m *Movie) imgFile() string { + return filepath.Join(m.publicDir, m.imgURL()) } -// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded -func (m *Movie) GetPosterURL(env *web.Env) string { +// 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(env)); os.IsNotExist(err) { + if _, err := os.Stat(m.imgFile()); os.IsNotExist(err) { // TODO image in the config ? return "img/noimage.png" } - return m.imgURL(env) + return m.imgURL() +} + +// getPolochonMovies returns an array of the user's polochon movies +func getPolochonMovies(user *users.User, env *web.Env) ([]*Movie, error) { + movies := []*Movie{} + + // Create a papi client + client, err := user.NewPapiClient() + if err != nil { + return movies, err + } + + // Retrieve the user's polochon movies + pmovies, err := client.GetMovies() + if err != nil { + return movies, err + } + + // Get the user's wishlisted movies + moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID) + if err != nil { + return movies, err + } + + // Create Movies objects from the movies retrieved + for _, pmovie := range pmovies.List() { + movie := New( + pmovie.ImdbID, + client, + pmovie, + moviesWishlist.IsMovieInWishlist(pmovie.ImdbID), + env.Config.PublicDir, + ) + movies = append(movies, movie) + } + + return movies, nil } diff --git a/src/internal/movies/movies_test.go b/src/internal/movies/movies_test.go deleted file mode 100644 index d92fe39..0000000 --- a/src/internal/movies/movies_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package movies - -import ( - "database/sql" - "fmt" - "os" - "testing" - - "github.com/Sirupsen/logrus" - "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" - _ "github.com/mattes/migrate/driver/postgres" - "github.com/odwrtw/polochon/lib" - "github.com/odwrtw/polochon/modules/mock" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" -) - -var db *sqlx.DB -var pgdsn string - -func init() { - var err error - - pgdsn = os.Getenv("POSTGRES_DSN") - db, err = sqlx.Connect("postgres", pgdsn) - - if err != nil { - fmt.Printf("Unavailable PG tests:\n %v\n", err) - os.Exit(1) - } -} - -func TestIntegrate(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - log := logrus.NewEntry(logrus.New()) - env := web.NewEnv(web.EnvParams{ - Database: db, - // Auth: authorizer, - Log: log, - Config: &config.Config{ - PublicDir: "/tmp", - }, - }) - detailer, _ := mock.NewDetailer(nil) - polochonConfig := polochon.MovieConfig{ - Detailers: []polochon.Detailer{ - detailer, - }, - } - movie := Movie{ - Movie: polochon.Movie{ - MovieConfig: polochonConfig, - ImdbID: "tt12345", - Genres: []string{}, - }, - } - - err := movie.GetDetails(env, false) - if err != nil { - t.Fatal(err) - } - - // Get it - movie = Movie{ - Movie: polochon.Movie{ - MovieConfig: polochonConfig, - ImdbID: "tt12345", - }, - } - err = movie.Get(env) - if err != nil { - t.Fatal(err) - } - if movie.Title != fmt.Sprintf("Movie %s", movie.ImdbID) { - t.Fatalf("Unexpected movie's title: %s", movie.Title) - } - - // Delete it - err = movie.Delete(db) - if err != nil { - t.Fatal(err) - } - - // Get it again - err = movie.Get(env) - if err != sql.ErrNoRows { - t.Fatalf("Unexpected error: %q", err) - } - }) -} diff --git a/src/internal/movies/wishlist.go b/src/internal/movies/wishlist.go deleted file mode 100644 index dd6c3e5..0000000 --- a/src/internal/movies/wishlist.go +++ /dev/null @@ -1,83 +0,0 @@ -package movies - -import ( - "fmt" - - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" -) - -const ( - upsertWishlistQuery = ` - INSERT INTO movies_tracked (imdb_id, user_id) - VALUES ($1, $2) - ON CONFLICT (imdb_id, user_id) - DO UPDATE - SET imdb_id=$1, user_id=$2;` - - getWishlistQueryByUserID = ` - SELECT - movies.id, - movies.title, - movies.imdb_id, - movies.tmdb_id, - movies.votes, - movies.rating, - movies.plot, - movies.year, - movies.original_title, - movies.runtime, - movies.genres, - movies.sort_title, - movies.tagline, - movies.created_at, - movies.updated_at, - movies_tracked.user_id - FROM movies INNER JOIN movies_tracked - ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$1;` - - deleteWishlistedQueryByID = `DELETE FROM movies_tracked WHERE imdb_id=$1 AND user_id=$2;` -) - -// AddToWishlist Adds a movie to a user's wishlist -func (m *Movie) AddToWishlist(env *web.Env, user *users.User) error { - _, err := env.Database.Exec(upsertWishlistQuery, m.ImdbID, user.ID) - if err != nil { - return err - } - return nil -} - -// DeleteFromWishlist deletes a movie from a user's wishlist -func (m *Movie) DeleteFromWishlist(env *web.Env, user *users.User) error { - r, err := env.Database.Exec(deleteWishlistedQueryByID, m.ImdbID, user.ID) - if err != nil { - return err - } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} - -// GetWishlist returns a list of movies wishlisted by user -func GetWishlist(env *web.Env, user *users.User) ([]*Movie, error) { - var moviesDB = []*MovieDB{} - err := env.Database.Select(&moviesDB, getWishlistQueryByUserID, user.ID) - if err != nil { - return nil, err - } - - var movies []*Movie - for _, movieDB := range moviesDB { - movie := New(movieDB.ImdbID) - // Set the poster url - movie.PosterURL = movie.GetPosterURL(env) - - movie.FillFromDB(movieDB) - movies = append(movies, movie) - } - - return movies, nil -} diff --git a/src/internal/random/random.go b/src/internal/random/random.go index 86ea8e5..fa9f057 100644 --- a/src/internal/random/random.go +++ b/src/internal/random/random.go @@ -18,6 +18,7 @@ const ( var src = rand.NewSource(time.Now().UnixNano()) +// String returns n random strings func String(n int) string { b := make([]byte, n) // A src.Int63() generates 63 random bits, enough for letterIdxMax characters! diff --git a/src/internal/shows/episodes.go b/src/internal/shows/episodes.go index 7b751a7..dba960b 100644 --- a/src/internal/shows/episodes.go +++ b/src/internal/shows/episodes.go @@ -1,181 +1,140 @@ package shows import ( - "database/sql" - "time" + "encoding/json" "github.com/Sirupsen/logrus" - "github.com/jmoiron/sqlx" + "github.com/odwrtw/papi" polochon "github.com/odwrtw/polochon/lib" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents" + + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" ) -const ( - upsertEpisodeQuery = ` - INSERT INTO episodes (show_imdb_id, show_tvdb_id, title, season, episode, tvdb_id, aired, plot, runtime, rating, imdb_id) - VALUES (:show_imdb_id, :show_tvdb_id, :title, :season, :episode, :tvdb_id, :aired, :plot, :runtime, :rating, :imdb_id) - ON CONFLICT (show_imdb_id, season, episode) - DO UPDATE - SET show_imdb_id=:show_imdb_id, show_tvdb_id=:show_tvdb_id, title=:title, - season=:season, episode=:episode, tvdb_id=:tvdb_id, aired=:aired, - plot=:plot, runtime=:runtime, rating=:rating, imdb_id=:imdb_id - RETURNING id;` - - getEpisodesQuery = ` - SELECT * - FROM episodes WHERE show_imdb_id=$1;` - - getEpisodeQuery = ` - SELECT * - FROM episodes WHERE show_imdb_id=$1 AND season=$2 AND episode=$3;` -) - // Episode represents an episode type Episode struct { - sqly.BaseModel - polochon.ShowEpisode - PolochonURL string `json:"polochon_url"` + client *papi.Client + pShow *papi.Show + *polochon.ShowEpisode } -// EpisodeDB represents the Episode in the DB -type EpisodeDB struct { - ID string `db:"id"` - TvdbID int `db:"tvdb_id"` - ImdbID string `db:"imdb_id"` - ShowImdbID string `db:"show_imdb_id"` - ShowTvdbID int `db:"show_tvdb_id"` - Season int `db:"season"` - Episode int `db:"episode"` - Title string `db:"title"` - Rating float32 `db:"rating"` - Plot string `db:"plot"` - Thumb string `db:"thumb"` - Runtime int `db:"runtime"` - Aired string `db:"aired"` - ReleaseGroup string `db:"release_group"` - Created time.Time `db:"created_at"` - Updated time.Time `db:"updated_at"` +// MarshalJSON implements the Marshal interface +func (e *Episode) MarshalJSON() ([]byte, error) { + type alias Episode + + var downloadURL string + // If the episode is present, fill the downloadURL + if e.pShow != nil && e.pShow.HasEpisode(e.Season, e.Episode) { + downloadURL, _ = e.client.DownloadURL( + &papi.Episode{ + ShowImdbID: e.ShowImdbID, + Episode: e.Episode, + Season: e.Season, + }, + ) + } + + // Marshal the episode with its polochon_url + episodeToMarshal := &struct { + *alias + PolochonURL string `json:"polochon_url"` + }{ + alias: (*alias)(e), + PolochonURL: downloadURL, + } + + return json.Marshal(episodeToMarshal) } // NewEpisode returns an Episode -func NewEpisode() *Episode { - return &Episode{} -} - -// NewEpisodeDB returns an Episode ready to be put in DB from an -// Episode -func NewEpisodeDB(e *Episode) EpisodeDB { - return EpisodeDB{ - ID: e.ID, - TvdbID: e.TvdbID, - ImdbID: e.EpisodeImdbID, - ShowImdbID: e.ShowImdbID, - ShowTvdbID: e.ShowTvdbID, - Season: e.Season, - Episode: e.Episode, - Title: e.Title, - Rating: e.Rating, - Plot: e.Plot, - Thumb: e.Thumb, - Runtime: e.Runtime, - Aired: e.Aired, - ReleaseGroup: e.ReleaseGroup, - Created: e.Created, - Updated: e.Updated, +func NewEpisode(client *papi.Client, pShow *papi.Show, imdbID string, season, episode int) *Episode { + return &Episode{ + client: client, + pShow: pShow, + ShowEpisode: &polochon.ShowEpisode{ + ShowImdbID: imdbID, + Season: season, + Episode: episode, + }, } } -// FillFromDB returns a Show from a ShowDB extracted from the DB -func (e *Episode) FillFromDB(eDB *EpisodeDB) { - e.ID = eDB.ID - e.TvdbID = eDB.TvdbID - e.EpisodeImdbID = eDB.ImdbID - e.ShowImdbID = eDB.ShowImdbID - e.ShowTvdbID = eDB.ShowTvdbID - e.Season = eDB.Season - e.Episode = eDB.Episode - e.Title = eDB.Title - e.Rating = eDB.Rating - e.Plot = eDB.Plot - e.Thumb = eDB.Thumb - e.Runtime = eDB.Runtime - e.Aired = eDB.Aired - e.Created = eDB.Created - e.Updated = eDB.Updated -} - -// Upsert episode to the database -func (e *Episode) Upsert(db *sqlx.DB) error { - eDB := NewEpisodeDB(e) - var id string - r, err := db.NamedQuery(upsertEpisodeQuery, eDB) - if err != nil { - return err - } - for r.Next() { - r.Scan(&id) - } - e.ID = id - return nil -} - -// GetTorrents retrieves torrents for the show, first try to get info from db, -// if not exists, use polochon.Torrenter and save informations in the database -// for future use -// -// If force is used, the torrenter will be used even if the torrent is found in +// GetAndFetchTorrents retrieves torrents for the episode with the given +// torrenters 'before' +// If found, return +// If not, retrives torrents with the torrenters 'after' and update them in // database -func (e *Episode) GetTorrents(env *web.Env, force bool) error { +func (e *Episode) GetAndFetchTorrents(env *web.Env, before []polochon.Torrenter, after []polochon.Torrenter) error { log := env.Log.WithFields(logrus.Fields{ - "imdb_id": e.ShowEpisode.ShowImdbID, - "season": e.ShowEpisode.Season, - "episode": e.ShowEpisode.Episode, - "function": "shows.GetTorrents", + "imdb_id": e.ShowImdbID, + "season": e.Season, + "episode": e.Episode, + "function": "episodes.GetAndFetchTorrents", + }) + + // Try to get details with the first batch of Detailers + err := e.GetTorrents(env, before) + if err == nil { + log.Debug("episode torrents found in first try") + return nil + } + log.Debugf("episode torrent not found in database: %s", err) + + // If not found, try the second batch and upsert + return e.RefreshTorrents(env, after) +} + +// GetTorrents retrieves torrents for the episode with the given torrenters +func (e *Episode) GetTorrents(env *web.Env, torrenters []polochon.Torrenter) error { + return GetTorrents(env, e.ShowEpisode, torrenters) +} + +// GetTorrents retrieves torrents for the episode with the given torrenters +func GetTorrents(env *web.Env, showEpisode *polochon.ShowEpisode, torrenters []polochon.Torrenter) error { + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": showEpisode.ShowImdbID, + "season": showEpisode.Season, + "episode": showEpisode.Episode, + "function": "episodes.GetTorrents", }) log.Debugf("getting torrents") - if len(e.Torrenters) == 0 { - e.Torrenters = env.Config.ShowTorrenters - } + showEpisode.Torrenters = torrenters - episodeTorrents, err := torrents.GetEpisodeTorrents( - env.Database, - e.ShowEpisode.ShowImdbID, - e.ShowEpisode.Season, - e.ShowEpisode.Episode, - ) - switch err { - case nil: - log.Debug("torrents found in database") - case sql.ErrNoRows: - log.Debug("torrent not found in database") - default: - // Unexpected error - return err - } - // If force is not specified, don't go further - if !force { - log.Debugf("returning %d torrents from db", len(episodeTorrents)) - // Add the torrents to the episode - for _, t := range episodeTorrents { - e.Torrents = append(e.Torrents, t.Torrent) - } - // Will return ErrNoRows if the torrent wasn't found - return err - } - - err = e.ShowEpisode.GetTorrents(env.Log) + err := showEpisode.GetTorrents(env.Log) if err != nil { return err } - log.Debugf("got %d torrents from torrenters", len(e.ShowEpisode.Torrents)) + log.Debugf("got %d torrents from torrenters", len(showEpisode.Torrents)) - for _, t := range e.ShowEpisode.Torrents { - torrent := torrents.NewEpisodeFromPolochon(e.ShowEpisode, t) - err = torrent.Upsert(env.Database) + return nil +} + +// RefreshTorrents refresh the episode torrents +func (e *Episode) RefreshTorrents(env *web.Env, torrenters []polochon.Torrenter) error { + return RefreshTorrents(env, e.ShowEpisode, torrenters) +} + +// RefreshTorrents refresh the episode torrents +func RefreshTorrents(env *web.Env, showEpisode *polochon.ShowEpisode, torrenters []polochon.Torrenter) error { + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": showEpisode.ShowImdbID, + "season": showEpisode.Season, + "episode": showEpisode.Episode, + "function": "episodes.RefreshTorrents", + }) + log.Debugf("refreshing torrents") + + // Refresh + err := GetTorrents(env, showEpisode, torrenters) + if err != nil { + return err + } + + // Upsert all the torrents we got + for _, t := range showEpisode.Torrents { + err = backend.UpsertEpisodeTorrent(env.Database, &t, showEpisode.ShowImdbID, showEpisode.Season, showEpisode.Episode) if err != nil { log.Errorf("error while adding torrent : %s", err) continue @@ -185,20 +144,35 @@ func (e *Episode) GetTorrents(env *web.Env, force bool) error { return nil } -// Get returns an episode -func (e *Episode) Get(env *web.Env) error { - return e.GetEpisode(env.Database) -} - -// GetEpisode returns an episode -func (e *Episode) GetEpisode(db *sqlx.DB) error { - var episodeDB EpisodeDB - err := db.QueryRowx(getEpisodeQuery, e.ShowImdbID, e.Season, e.Episode).StructScan(&episodeDB) +// Refresh retrieves details for the episode with the given detailers +// and update them in database +func (e *Episode) Refresh(env *web.Env, detailers []polochon.Detailer) error { + // Refresh + err := e.GetEpisodeDetails(env, detailers) if err != nil { return err } - e.FillFromDB(&episodeDB) + return backend.UpsertEpisode(env.Database, e.ShowEpisode) +} + +// GetEpisodeDetails retrieves details for the episode with the given detailers +func (e *Episode) GetEpisodeDetails(env *web.Env, detailers []polochon.Detailer) error { + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": e.ShowImdbID, + "function": "episodes.GetDetails", + }) + log.Debugf("getting details") + + e.ShowEpisode.Detailers = detailers + + // Get the details + err := e.ShowEpisode.GetDetails(env.Log) + if err != nil { + return err + } + + log.Debug("got details from detailers") return nil } diff --git a/src/internal/shows/handlers.go b/src/internal/shows/handlers.go index 56f5909..e874758 100644 --- a/src/internal/shows/handlers.go +++ b/src/internal/shows/handlers.go @@ -4,37 +4,26 @@ import ( "encoding/json" "errors" "fmt" - "net" - "net/http" - "net/url" + "log" "strconv" - "github.com/gorilla/mux" - customError "github.com/odwrtw/errors" - "github.com/odwrtw/papi" - polochon "github.com/odwrtw/polochon/lib" - "github.com/odwrtw/polochon/modules/pam" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" + + "net/http" + + "github.com/gorilla/mux" + "github.com/odwrtw/papi" + polochon "github.com/odwrtw/polochon/lib" ) // ErrPolochonUnavailable is an error returned if the polochon server is not available var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address") -// GetDetailsHandler retrieves details of a show +// GetDetailsHandler handles details of a show func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { - return DetailsHandler(env, w, r, false) -} - -// RefreahDetailsHandler refresh details of a show -func RefreshDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { - return DetailsHandler(env, w, r, true) -} - -// DetailsHandler handles details of an episode -func DetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force bool) error { vars := mux.Vars(r) id := vars["id"] @@ -44,31 +33,94 @@ func DetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force return env.RenderError(w, errors.New("invalid user type")) } - s := New(id) - if err := s.GetDetails(env, user, force); err != nil { - return env.RenderError(w, err) - } - if err := s.GetEpisodes(env, force); err != nil { - return env.RenderError(w, err) - } - // Get the show from the polochon of the user - pShow, err := getPolochonShow(user, s.ImdbID) + client, err := user.NewPapiClient() if err != nil { - env.Log.Warnf("error while getting polochon show %s : %s", s.ImdbID, err) + return env.RenderError(w, err) } - // For each of the user's polochon episodes, add a direct link to it - for _, pEpisode := range pShow.Episodes { - for _, e := range s.Episodes { - if e.Season != pEpisode.Season || e.Episode != pEpisode.Episode { - continue - } - e.PolochonURL = pEpisode.PolochonURL + + pShow, err := client.GetShow(id) + if err != nil && err != papi.ErrResourceNotFound { + log.Println("Got error getting show ", err) + } + + wShow, err := backend.IsShowWishlisted(env.Database, user.ID, id) + if err != nil && err != papi.ErrResourceNotFound { + log.Println("Got error getting wishlisted show ", err) + } + + s := NewWithClient(id, client, pShow, wShow, env.Config.PublicDir) + // First try from the db + first := []polochon.Detailer{env.Backend.Detailer} + // Then try from the polochon detailers + detailers := env.Config.ShowDetailers + err = s.GetAndFetch(env, first, detailers) + if err != nil { + env.Log.Error(err) + return err + } + + env.Log.Debug("getting episodes torrents") + for _, e := range s.Show.Episodes { + // Get torrents from the db + backend := []polochon.Torrenter{env.Backend.Torrenter} + err := GetTorrents(env, e, backend) + if err != nil { + env.Log.Error(err) } } return env.RenderJSON(w, s) } +// RefreshShowHandler refreshes details of a show + torrents +func RefreshShowHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + id := vars["id"] + + v := auth.GetCurrentUser(r, env.Log) + user, ok := v.(*users.User) + if !ok { + return env.RenderError(w, errors.New("invalid user type")) + } + + client, err := user.NewPapiClient() + if err != nil { + return env.RenderError(w, err) + } + + pShow, err := client.GetShow(id) + if err != nil && err != papi.ErrResourceNotFound { + log.Println("Got error getting show ", err) + } + + wShow, err := backend.IsShowWishlisted(env.Database, user.ID, id) + if err != nil && err != papi.ErrResourceNotFound { + log.Println("Got error getting wishlisted show ", err) + } + + s := NewWithClient(id, client, pShow, wShow, env.Config.PublicDir) + // Refresh the polochon detailers + detailers := env.Config.ShowDetailers + err = s.Refresh(env, detailers) + if err != nil { + env.Log.Error(err) + return err + } + + env.Log.Debug("getting episodes torrents") + for _, e := range s.Episodes { + // Get torrents from the db + err := RefreshTorrents(env, e, env.Config.ShowTorrenters) + if err != nil { + env.Log.Error(err) + } + } + + env.Log.Debug("getting polochon show") + + return env.RenderJSON(w, s) +} + // SearchShow will search a show func SearchShow(env *web.Env, w http.ResponseWriter, r *http.Request) error { var data struct { @@ -90,23 +142,50 @@ func SearchShow(env *web.Env, w http.ResponseWriter, r *http.Request) error { var shows []*polochon.Show searchers := env.Config.ShowSearchers + // Iterate on all the searchers to search for the show for _, searcher := range searchers { result, err := searcher.SearchShow(data.Key, env.Log) if err != nil { env.Log.Errorf("error while searching show : %s", err) continue } + // Add the results to the list of results shows = append(shows, result...) } env.Log.Debugf("got %d shows doing search %q", len(shows), data.Key) + + client, err := user.NewPapiClient() + if err != nil { + return env.RenderError(w, err) + } + + // Get the polochon's shows + pShows, err := client.GetShows() + if err != nil { + return env.RenderError(w, err) + } + + // Get the user's wishlisted shows + wShows, err := backend.GetShowWishlist(env.Database, user.ID) + if err != nil { + return env.RenderError(w, err) + } + showList := []*Show{} + // Now iterate over all the shows to get details for _, s := range shows { - show := New(s.ImdbID) - err := show.GetDetails(env, user, false) + pShow, _ := pShows.Has(s.ImdbID) + wShow, _ := wShows.IsShowInWishlist(s.ImdbID) + show := NewWithClient(s.ImdbID, client, pShow, wShow, env.Config.PublicDir) + + // First try from the db + first := []polochon.Detailer{env.Backend.Detailer} + // Then try from the polochon detailers + detailers := env.Config.ShowDetailers + err := show.GetAndFetch(env, first, detailers) if err != nil { - env.Log.Errorf("error while getting show details : %s", err) - continue + env.Log.Error(err) } showList = append(showList, show) } @@ -135,8 +214,7 @@ func AddToWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderError(w, errors.New("invalid user type")) } - s := New(id) - if err := s.AddToWishlist(env, user, data.Season, data.Episode); err != nil { + if err := backend.AddShowToWishlist(env.Database, user.ID, id, data.Season, data.Episode); err != nil { env.Log.Warnf("Error while adding to db : %s", err) return env.RenderError(w, err) } @@ -155,8 +233,7 @@ func DeleteFromWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) er return env.RenderError(w, errors.New("invalid user type")) } - s := New(id) - if err := s.DeleteFromWishlist(env, user); err != nil { + if err := backend.DeleteShowFromWishlist(env.Database, user.ID, id); err != nil { env.Log.Warnf("Error while deleting to db : %s", err) return env.RenderError(w, err) } @@ -172,111 +249,64 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er return env.RenderError(w, errors.New("invalid user type")) } - shows, err := GetWishlist(env, user) + client, err := user.NewPapiClient() if err != nil { return env.RenderError(w, err) } - return env.RenderJSON(w, shows) + // Get the polochon's shows + pShows, err := client.GetShows() + if err != nil { + return env.RenderError(w, err) + } + + wShows, err := backend.GetShowWishlist(env.Database, user.ID) + if err != nil { + return env.RenderError(w, err) + } + + showList := []*Show{} + for _, wishedShow := range wShows.List() { + pShow, _ := pShows.Has(wishedShow.ImdbID) + show := NewWithClient(wishedShow.ImdbID, client, pShow, wishedShow, env.Config.PublicDir) + + // First check in the DB + before := []polochon.Detailer{env.Backend.Detailer} + // Then with the default detailers + after := env.Config.ShowDetailers + err := show.GetAndFetch(env, before, after) + if err != nil { + env.Log.Errorf("error while getting show details : %s", err) + continue + } + showList = append(showList, show) + } + + return env.RenderJSON(w, showList) } -// getPolochonShows returns all the Shows from the polochon of a user -func getPolochonShows(user *users.User) ([]*Show, error) { - shows := []*Show{} - - var polochonConfig config.UserPolochon - err := user.GetConfig("polochon", &polochonConfig) - if err != nil { - return shows, err - } - - client, err := papi.New(polochonConfig.URL) - if err != nil { - return shows, err - } - - if polochonConfig.Token != "" { - client.SetToken(polochonConfig.Token) - } - - pshows, err := client.GetShows() - if err != nil { - // Catch network error for accessing specified polochon address - if uerr, ok := err.(*url.Error); ok { - if nerr, ok := uerr.Err.(*net.OpError); ok { - if nerr.Op == "dial" { - return shows, ErrPolochonUnavailable - } - - } - } - return shows, err - } - for _, pshow := range pshows { - show := New(pshow.ImdbID) - for _, season := range pshow.Seasons { - for _, episode := range season.Episodes { - e := NewEpisode() - e.Season = episode.Season - e.Episode = episode.Episode - e.ShowImdbID = episode.ShowImdbID - e.PolochonURL, _ = client.DownloadURL( - &papi.Episode{ - ShowImdbID: show.ImdbID, - Episode: e.Episode, - Season: e.Season, - }, - ) - - show.Episodes = append(show.Episodes, e) - } - } - shows = append(shows, show) - } - return shows, nil -} - -// getPolochonShow returns a Show with its epidodes from the polochon of a user -func getPolochonShow(user *users.User, imdbID string) (Show, error) { - shows, err := getPolochonShows(user) - if err != nil { - return Show{}, err - } - for _, s := range shows { - if s.ImdbID == imdbID { - return *s, nil - } - } - return Show{}, nil -} - -// FromPolochon will returns shows from Polochon -func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { +// PolochonShowsHandler will returns shows from Polochon +func PolochonShowsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { v := auth.GetCurrentUser(r, env.Log) user, ok := v.(*users.User) if !ok { return env.RenderError(w, errors.New("invalid user type")) } - shows, err := getPolochonShows(user) + // Get the polochon's shows + shows, err := getPolochonShows(env, user) if err != nil { return env.RenderError(w, err) } - var polochonConfig config.UserPolochon - err = user.GetConfig("polochon", &polochonConfig) - if err != nil { - return env.RenderError(w, err) - } - - detailer, err := pam.New(&pam.Params{ - Endpoint: polochonConfig.URL, - Token: polochonConfig.Token, - }) - + // Get details in DB for each shows + // Fetch the details if not found for _, s := range shows { - s.Detailers = []polochon.Detailer{detailer} - err := s.GetDetails(env, user, false) + // First try from the db + first := []polochon.Detailer{env.Backend.Detailer} + // Then try from the polochon detailer + detailers := env.Config.ShowDetailers + err := s.GetAndFetch(env, first, detailers) if err != nil { env.Log.Error(err) } @@ -287,11 +317,6 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error { // RefreshEpisodeHandler refresh details of an episode func RefreshEpisodeHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { - return EpisodeDetailsHandler(env, w, r, true) -} - -// EpisodeDetailsHandler handles details of a show -func EpisodeDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force bool) error { vars := mux.Vars(r) id := vars["id"] @@ -306,29 +331,28 @@ func EpisodeDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, return env.RenderError(w, errors.New("invalid user type")) } - s := New(id) - e, err := s.GetEpisodeDetails(env, season, episode, force) + client, err := user.NewPapiClient() if err != nil { return env.RenderError(w, err) } - err = e.GetTorrents(env, force) - if err != nil && customError.IsFatal(err) { + pShow, err := client.GetShow(id) + if err != nil && err != papi.ErrResourceNotFound { + env.Log.Warnf("Error getting show ", err) + } + + e := NewEpisode(client, pShow, id, season, episode) + // Refresh the episode + err = e.Refresh(env, env.Config.ShowDetailers) + if err != nil { + env.Log.Error(err) return env.RenderError(w, err) } - // Get the show from the polochon of the user - pShow, err := getPolochonShow(user, id) + // Refresh the torrents + err = e.RefreshTorrents(env, env.Config.ShowTorrenters) if err != nil { - env.Log.Warnf("error while getting polochon episode %s S%02dE%02d : %s", id, season, episode, err) - } - // Find if the user has a the episode in its polochon to add the - // DownloadURL - for _, pEpisode := range pShow.Episodes { - if e.Season == pEpisode.Season && e.Episode == pEpisode.Episode { - e.PolochonURL = pEpisode.PolochonURL - break - } + env.Log.Error(err) } return env.RenderJSON(w, e) diff --git a/src/internal/shows/shows.go b/src/internal/shows/shows.go index e0e4187..02ea1ae 100644 --- a/src/internal/shows/shows.go +++ b/src/internal/shows/shows.go @@ -1,419 +1,215 @@ package shows import ( - "database/sql" + "encoding/json" "fmt" "os" "path/filepath" - "time" + "strings" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "github.com/Sirupsen/logrus" - "github.com/jmoiron/sqlx" + "github.com/odwrtw/papi" "github.com/odwrtw/polochon/lib" ) -const ( - upsertShowQuery = ` - INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired) - VALUES (:imdb_id, :title, :rating, :plot, :tvdb_id, :year, :first_aired) - ON CONFLICT (imdb_id) - DO UPDATE - SET imdb_id=:imdb_id, title=:title, rating=:rating, plot=:plot, - tvdb_id=:tvdb_id, year=:year, first_aired=:first_aired - RETURNING id;` - - getShowQueryByImdbID = ` - SELECT * - FROM shows WHERE imdb_id=$1;` - - getShowQueryByID = ` - SELECT * - FROM shows WHERE id=$1;` - - deleteShowQueryByID = `DELETE FROM shows WHERE id=$1;` - - getShowWithUserQueryByImdbID = ` - SELECT - shows.id, - shows.imdb_id, - shows.title, - shows.rating, - shows.plot, - shows.tvdb_id, - shows.year, - shows.first_aired, - shows_tracked.season, - shows_tracked.episode, - shows.updated_at, - shows.created_at - FROM shows LEFT JOIN shows_tracked - ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$2 - WHERE shows.imdb_id=$1;` - - getShowWithUserQueryByID = ` - SELECT - shows.id, - shows.imdb_id, - shows.title, - shows.rating, - shows.plot, - shows.tvdb_id, - shows.year, - shows.first_aired, - shows_tracked.season, - shows_tracked.episode, - shows.updated_at, - shows.created_at - FROM shows LEFT JOIN shows_tracked ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$2 - WHERE shows.id=$1;` -) - -var ( - // ErrNotFound error returned when show not found in database - ErrNotFound = fmt.Errorf("Not found") -) - // Show represents a show type Show struct { - sqly.BaseModel - polochon.Show - Episodes []*Episode `json:"episodes"` - TrackedSeason *int `json:"tracked_season"` - TrackedEpisode *int `json:"tracked_episode"` - BannerURL string `json:"banner_url"` - FanartURL string `json:"fanart_url"` - PosterURL string `json:"poster_url"` + client *papi.Client + pShow *papi.Show + *polochon.Show + TrackedSeason *int `json:"tracked_season"` + TrackedEpisode *int `json:"tracked_episode"` + publicDir string } -// ShowDB represents the Show in the DB -type ShowDB struct { - ID string `db:"id"` - ImdbID string `db:"imdb_id"` - TvdbID int `db:"tvdb_id"` - TrackedSeason *int `db:"season"` - TrackedEpisode *int `db:"episode"` - Title string `db:"title"` - Rating float32 `db:"rating"` - Plot string `db:"plot"` - Year int `db:"year"` - FirstAired time.Time `db:"first_aired"` - Created time.Time `db:"created_at"` - Updated time.Time `db:"updated_at"` -} - -// NewShowDB returns a Show ready to be put in DB from a -// Show -func NewShowDB(s *Show) ShowDB { - sDB := ShowDB{ - ID: s.ID, - ImdbID: s.ImdbID, - Title: s.Title, - Rating: s.Rating, - Plot: s.Plot, - TvdbID: s.TvdbID, - Year: s.Year, - Created: s.Created, - Updated: s.Updated, +// MarshalJSON implements the Marshal interface +func (s *Show) MarshalJSON() ([]byte, error) { + type alias Show + // Create the structure that we want to marshal + showToMarshal := &struct { + *alias + Episodes []Episode `json:"episodes"` + BannerURL string `json:"banner_url"` + FanartURL string `json:"fanart_url"` + PosterURL string `json:"poster_url"` + }{ + alias: (*alias)(s), + BannerURL: s.GetImageURL("banner"), + FanartURL: s.GetImageURL("fanart"), + PosterURL: s.GetImageURL("poster"), } - if s.FirstAired != nil { - sDB.FirstAired = *s.FirstAired - } - return sDB -} -// FillFromDB returns a Show from a ShowDB extracted from the DB -func (s *Show) FillFromDB(sDB *ShowDB) { - s.ID = sDB.ID - s.ImdbID = sDB.ImdbID - s.Title = sDB.Title - s.Rating = sDB.Rating - s.Plot = sDB.Plot - s.TvdbID = sDB.TvdbID - s.Year = sDB.Year - s.FirstAired = &sDB.FirstAired - s.Created = sDB.Created - s.Updated = sDB.Updated - s.TrackedSeason = sDB.TrackedSeason - s.TrackedEpisode = sDB.TrackedEpisode + // Create Episode obj from polochon.Episodes and add them to the object to + // marshal + for _, e := range s.Show.Episodes { + showToMarshal.Episodes = append(showToMarshal.Episodes, Episode{ + ShowEpisode: e, + client: s.client, + pShow: s.pShow, + }) + } + return json.Marshal(showToMarshal) } // New returns a new Show with a polochon ShowConfig func New(imdbID string) *Show { return &Show{ - Show: polochon.Show{ + Show: &polochon.Show{ ImdbID: imdbID, }, } } -// Get returns a show with user info like tracked -func (s *Show) Get(env *web.Env, user *users.User) error { - var err error - var sDB ShowDB - if s.ID != "" { - err = env.Database.QueryRowx(getShowWithUserQueryByID, s.ID, user.ID).StructScan(&sDB) - } else if s.ImdbID != "" { - err = env.Database.QueryRowx(getShowWithUserQueryByImdbID, s.ImdbID, user.ID).StructScan(&sDB) - } else { - err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID") +// NewWithClient returns a new Show with a polochon ShowConfig +func NewWithClient(imdbID string, client *papi.Client, pShow *papi.Show, wShow *backend.WishedShow, publicDir string) *Show { + s := &Show{ + Show: &polochon.Show{ + ImdbID: imdbID, + }, + client: client, + pShow: pShow, + publicDir: publicDir, } - if err != nil { - return err + if wShow != nil { + s.TrackedSeason = &wShow.Season + s.TrackedEpisode = &wShow.Episode } - // Set the poster url - s.PosterURL = s.GetPosterURL(env) - - s.FillFromDB(&sDB) - return nil + return s } -// GetShow returns a show with user info like tracked -func (s *Show) GetShow(db *sqlx.DB) error { - var err error - var sDB ShowDB - if s.ID != "" { - err = db.QueryRowx(getShowQueryByID, s.ID).StructScan(&sDB) - } else if s.ImdbID != "" { - err = db.QueryRowx(getShowQueryByImdbID, s.ImdbID).StructScan(&sDB) - } else { - err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID") - } - if err != nil { - return err - } - // Set the poster url - // s.PosterURL = s.GetPosterURL(env) - - s.FillFromDB(&sDB) - return nil -} - -// GetDetails retrieves details for the show, first try to -// get info from db, if not exists, use polochon.Detailer -// and save informations in the database for future use -func (s *Show) GetDetails(env *web.Env, user *users.User, force bool) error { +// GetDetails retrieves details for the show with the given detailers +func (s *Show) GetDetails(env *web.Env, detailers []polochon.Detailer) error { log := env.Log.WithFields(logrus.Fields{ "imdb_id": s.ImdbID, "function": "shows.GetDetails", }) - log.Debugf("getting details") - - if len(s.Detailers) == 0 { - s.Detailers = env.Config.ShowDetailers + var detailersName []string + for _, d := range detailers { + detailersName = append(detailersName, d.Name()) } + log.Debugf("getting details with %s", strings.Join(detailersName, ", ")) - var err error - err = s.Get(env, user) - switch err { - case nil: - log.Debug("show found in database") - case sql.ErrNoRows: - log.Debug("show not found in database") - default: - // Unexpected error - return err - } - // If force is not specified, don't go further - if !force { - // Will return ErrNoRows if the show wasn't found - return err - } + s.Detailers = detailers - // GetDetail - err = s.Show.GetDetails(env.Log) + // Get the details + err := s.Show.GetDetails(env.Log) if err != nil { return err } - log.Debug("got details from detailers") - s.Episodes = []*Episode{} - for _, pe := range s.Show.Episodes { - s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe}) - } - - err = s.Upsert(env.Database) - if err != nil { - log.Debug("error while doing show upsert func", err) - return err - } - - log.Debug("show added in database") - - // Download show images - s.downloadImages(env) - - log.Debug("images downloaded") - - // Set the poster url - s.PosterURL = s.GetPosterURL(env) + log.Debugf("got details from detailers ") return nil } -// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded -func (s *Show) GetPosterURL(env *web.Env) string { +// GetAndFetch retrieves details for the show with the given +// detailers 'before' +// If found, return +// If not, retrives details with the detailers 'after' and update them in +// database +func (s *Show) GetAndFetch(env *web.Env, before []polochon.Detailer, after []polochon.Detailer) error { + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": s.ImdbID, + "function": "shows.GetAndFetch", + }) + + // Try to get details with the first batch of Detailers + err := s.GetDetails(env, before) + if err == nil { + log.Debug("show found in first try") + return nil + } + log.Debugf("show not found in database: %s", err) + + // If not found, try the second batch and upsert + return s.Refresh(env, after) +} + +// Refresh retrieves details for the show with the given detailers +// and update them in database +func (s *Show) Refresh(env *web.Env, detailers []polochon.Detailer) error { + // Refresh + err := s.GetDetails(env, detailers) + if err != nil { + return err + } + + // Download show images + s.downloadImages(env) + + env.Log.Debug("images downloaded") + + // If found, update in database + return backend.UpsertShow(env.Database, s.Show) +} + +// GetImageURL returns the image URL or the default image if the poster is not yet downloaded +func (s *Show) GetImageURL(imgType string) string { // Check if the show image exists - if _, err := os.Stat(s.imgFile(env, "poster")); os.IsNotExist(err) { + if _, err := os.Stat(s.imgFile(imgType)); os.IsNotExist(err) { // TODO image in the config ? return "img/noimage.png" } - return s.imgURL(env, "poster") + return s.imgURL(imgType) } // downloadImages will download the show images func (s *Show) downloadImages(env *web.Env) { // Download the banner - err := web.Download(s.Show.Banner, s.imgFile(env, "banner")) + err := web.Download(s.Show.Banner, s.imgFile("banner")) if err != nil { env.Log.Errorf("failed to dowload banner: %s", err) } - err = web.Download(s.Show.Fanart, s.imgFile(env, "fanart")) + err = web.Download(s.Show.Fanart, s.imgFile("fanart")) if err != nil { env.Log.Errorf("failed to dowload fanart: %s", err) } - err = web.Download(s.Show.Poster, s.imgFile(env, "poster")) + err = web.Download(s.Show.Poster, s.imgFile("poster")) if err != nil { env.Log.Errorf("failed to dowload poster: %s", err) } } // imgFile returns the image location on disk -func (s *Show) imgFile(env *web.Env, imgType string) string { +func (s *Show) imgFile(imgType string) string { fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) - return filepath.Join(env.Config.PublicDir, fileURL) + return filepath.Join(s.publicDir, fileURL) } // imgURL returns the default image url -func (s *Show) imgURL(env *web.Env, imgType string) string { +func (s *Show) imgURL(imgType string) string { return fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) } -// Upsert a show in the database -func (s *Show) Upsert(db *sqlx.DB) error { - sDB := NewShowDB(s) - var id string - r, err := db.NamedQuery(upsertShowQuery, sDB) +// getPolochonShows returns all the Shows from the polochon of a user +func getPolochonShows(env *web.Env, user *users.User) ([]*Show, error) { + shows := []*Show{} + + client, err := user.NewPapiClient() if err != nil { - return err + return shows, err } - for r.Next() { - r.Scan(&id) - } - s.ID = id - for _, e := range s.Episodes { - e.ShowImdbID = s.ImdbID - err = e.Upsert(db) - if err != nil { - return err - } - } - return nil -} - -// Delete show from database -func (s *Show) Delete(db *sqlx.DB) error { - r, err := db.Exec(deleteShowQueryByID, s.ID) + // Get the polochon's shows + pshows, err := client.GetShows() if err != nil { - return err + return shows, err } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} -// GetEpisodes from database -func (s *Show) GetEpisodes(env *web.Env, force bool) error { - // We retrive episode's info from database populate the s.Episodes member - var episodesDB = []*EpisodeDB{} - err := env.Database.Select(&episodesDB, getEpisodesQuery, s.ImdbID) + wShows, err := backend.GetShowWishlist(env.Database, user.ID) if err != nil { - return err - } - if len(episodesDB) == 0 { - return nil + return shows, err } - for _, episodeDB := range episodesDB { - episode := NewEpisode() - episode.FillFromDB(episodeDB) - s.Episodes = append(s.Episodes, episode) - err = episode.GetTorrents(env, force) - if err != nil { - env.Log.Debugf("error while getting episode torrent: %q", err) - } + // Create Shows objects from the shows retrieved + for _, pShow := range pshows.List() { + wShow, _ := wShows.IsShowInWishlist(pShow.ImdbID) + show := NewWithClient(pShow.ImdbID, client, pShow, wShow, env.Config.PublicDir) + shows = append(shows, show) } - - return nil -} - -// GetEpisode from database -func (s *Show) GetEpisodeDetails(env *web.Env, seasonNb, episodeNb int, force bool) (*Episode, error) { - log := env.Log.WithFields(logrus.Fields{ - "imdb_id": s.ImdbID, - "season": seasonNb, - "episode": episodeNb, - "function": "show.GetEpisodeDetails", - }) - log.Debugf("getting episode details") - - e := NewEpisode() - e.ShowImdbID = s.ImdbID - e.Season = seasonNb - e.Episode = episodeNb - - if len(e.Detailers) == 0 { - e.Detailers = env.Config.ShowDetailers - } - - var err error - err = e.Get(env) - switch err { - case nil: - log.Debug("episode found in database") - case sql.ErrNoRows: - log.Debug("episode not found in database") - default: - // Unexpected error - return nil, err - } - // If force is not specified, don't go further - if !force { - // Will return ErrNoRows if the episode wasn't found - return e, err - } - - // GetDetail of the episode - err = e.ShowEpisode.GetDetails(env.Log) - if err != nil { - return nil, err - } - - // Upsert the episode - err = e.Upsert(env.Database) - if err != nil { - log.Debug("error while doing episode upsert func", err) - return nil, err - } - - log.Debug("episode inserted/updated in database") - - return e, nil -} - -// GetTorrents from the database or fetch them if needed -func (s *Show) GetTorrents(env *web.Env, force bool) error { - for _, e := range s.Episodes { - err := e.GetTorrents(env, force) - if err != nil { - env.Log.Errorf("error while getting episode torrent: %s", err) - } - } - return nil + return shows, nil } diff --git a/src/internal/shows/shows_test.go b/src/internal/shows/shows_test.go deleted file mode 100644 index e3e9e71..0000000 --- a/src/internal/shows/shows_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package shows - -import ( - "fmt" - "os" - "testing" - - "github.com/Sirupsen/logrus" - "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" - _ "github.com/mattes/migrate/driver/postgres" - "github.com/odwrtw/polochon/lib" - "github.com/odwrtw/polochon/modules/mock" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" -) - -var db *sqlx.DB -var pgdsn string - -func init() { - var err error - - pgdsn = os.Getenv("POSTGRES_DSN") - db, err = sqlx.Connect("postgres", pgdsn) - - if err != nil { - fmt.Printf("Unavailable PG tests:\n %v\n", err) - os.Exit(1) - } -} - -func TestIntegrate(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - detailer, _ := mock.NewDetailer(nil) - polochonConfig := polochon.ShowConfig{ - Detailers: []polochon.Detailer{ - detailer, - }, - } - - show := Show{ - Show: polochon.Show{ - ShowConfig: polochonConfig, - ImdbID: "tt12345", - }, - } - - log := logrus.NewEntry(logrus.New()) - err := show.GetDetails(db, log) - if err != nil { - t.Fatal(err) - } - - if len(show.Episodes) != 50 { - t.Fatalf("Unexpected number of episodes: %d", len(show.Episodes)) - } - - // Get from db - show2 := Show{ - Show: polochon.Show{ - ShowConfig: polochonConfig, - ImdbID: "tt12345", - }, - } - - err = show2.Get(db) - if err != nil { - t.Fatal(err) - } - - err = show2.GetEpisodes(db) - if err != nil { - t.Fatal(err) - } - - if len(show2.Episodes) != 50 { - t.Fatalf("Unexpected number of episodes: %d", len(show2.Episodes)) - } - - err = show.Delete(db) - if err != nil { - t.Fatal(err) - } - - err = show.Get(db) - if err != ErrNotFound { - t.Fatalf("Unexpected error: %q", err) - } - }) -} - -// UserBackend represents the data backend to get the user -type UserBackend struct { - Database *sqlx.DB -} - -// Get gets the username from the UserBackend -func (b *UserBackend) Get(username string) (auth.User, error) { - return users.Get(b.Database, username) -} - -func getEnv(db *sqlx.DB) *web.Env { - uBackend := &UserBackend{Database: db} - - authParams := auth.Params{ - Backend: uBackend, - Pepper: "pepper", - Cost: 10, - Secret: "secret", - } - authorizer := auth.New(authParams) - - log := logrus.NewEntry(logrus.New()) - env := web.NewEnv(web.EnvParams{ - Database: db, - Auth: authorizer, - Log: log, - // Config: cf, - }) - - return env -} - -func TestTrackedShow(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - detailer, _ := mock.NewDetailer(nil) - polochonConfig := polochon.ShowConfig{ - Detailers: []polochon.Detailer{ - detailer, - }, - } - - show := Show{ - Show: polochon.Show{ - ShowConfig: polochonConfig, - ImdbID: "tt12345", - }, - } - - err := show.Upsert(db) - if err != nil { - t.Fatal(err) - } - - u := &users.User{Name: "plop"} - env := getEnv(db) - - u.Hash, err = env.Auth.GenHash("pass") - if err != nil { - t.Fatal(err) - } - - err = u.NewConfig() - if err != nil { - t.Fatal(err) - } - - err = u.Add(db) - if err != nil { - t.Fatal(err) - } - - err = show.GetAsUser(db, u) - if err != nil { - t.Fatal(err) - } - if show.IsTracked() { - t.Fatal("Tracked must be false here") - } - - q := `INSERT INTO shows_tracked (show_id, user_id, season, episode) VALUES ($1, $2, $3, $4);` - _, err = db.Exec(q, show.ID, u.ID, 1, 1) - if err != nil { - t.Fatal(err) - } - err = show.GetAsUser(db, u) - if err != nil { - t.Fatal(err) - } - if !show.IsTracked() { - t.Fatal("Tracked must be true here") - } - }) -} diff --git a/src/internal/shows/wishlist.go b/src/internal/shows/wishlist.go deleted file mode 100644 index b8db094..0000000 --- a/src/internal/shows/wishlist.go +++ /dev/null @@ -1,87 +0,0 @@ -package shows - -import ( - "fmt" - - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" -) - -const ( - upsertWishlistQuery = ` - INSERT INTO shows_tracked (imdb_id, user_id, season, episode) - VALUES ($1, $2, $3, $4) - ON CONFLICT (imdb_id, user_id) - DO UPDATE - SET imdb_id=$1, user_id=$2, season=$3, episode=$4;` - - getWishlistQueryByUserID = ` - SELECT - shows.id, - shows.imdb_id, - shows.title, - shows.rating, - shows.plot, - shows.tvdb_id, - shows.year, - shows.first_aired, - shows_tracked.season, - shows_tracked.episode, - shows.updated_at, - shows.created_at - FROM shows INNER JOIN shows_tracked - ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$1;` - - deleteWishlistedQueryByID = `DELETE FROM shows_tracked WHERE imdb_id=$1 AND user_id=$2;` -) - -// AddToWishlist Adds a show to a user's wishlist -func (s *Show) AddToWishlist(env *web.Env, user *users.User, season, episode int) error { - _, err := env.Database.Exec(upsertWishlistQuery, s.ImdbID, user.ID, season, episode) - if err != nil { - return err - } - return nil -} - -// IsTracked returns true if the show is tracked use this function -// after retrieve the show with GetAsUser or other *AsUser functions -func (s *Show) IsTracked() bool { - if s.TrackedSeason != nil && s.TrackedEpisode != nil { - return true - } - return false -} - -// DeleteFromWishlist deletes a show from a user's wishlist -func (s *Show) DeleteFromWishlist(env *web.Env, user *users.User) error { - r, err := env.Database.Exec(deleteWishlistedQueryByID, s.ImdbID, user.ID) - if err != nil { - return err - } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} - -// GetWishlist returns a list of shows tracked by user -func GetWishlist(env *web.Env, user *users.User) ([]*Show, error) { - var showsDB = []*ShowDB{} - err := env.Database.Select(&showsDB, getWishlistQueryByUserID, user.ID) - if err != nil { - return nil, err - } - - var shows []*Show - for _, showDB := range showsDB { - // Set the poster url - show := New(showDB.ImdbID) - show.PosterURL = show.GetPosterURL(env) - show.FillFromDB(showDB) - shows = append(shows, show) - } - - return shows, nil -} diff --git a/src/internal/torrents/episode_torrents.go b/src/internal/torrents/episode_torrents.go deleted file mode 100644 index 486ac02..0000000 --- a/src/internal/torrents/episode_torrents.go +++ /dev/null @@ -1,160 +0,0 @@ -package torrents - -import ( - "database/sql" - "fmt" - "time" - - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - - "github.com/jmoiron/sqlx" - polochon "github.com/odwrtw/polochon/lib" -) - -const ( - upsertEpisodeTorrentQuery = ` - INSERT INTO episode_torrents (imdb_id, url, source, quality, upload_user, - season, episode, seeders, leechers) - VALUES (:imdb_id, :url, :source, :quality, :upload_user, :season, :episode, - :seeders, :leechers) - ON CONFLICT (imdb_id, season, episode, quality, source) - DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality, - upload_user=:upload_user, season=:season, episode=:episode, - seeders=:seeders, leechers=:leechers - RETURNING id;` - - getEpisodeTorrentQuery = ` - SELECT * - FROM episode_torrents WHERE imdb_id=$1 AND season=$2 AND episode=$3;` - - getEpisodeTorrentQueryByID = ` - SELECT * - FROM episode_torrents WHERE id=$1;` - - deleteEpisodeTorrentQuery = `DELETE FROM movie_torrents WHERE id=$1;` -) - -// EpisodeTorrent represents an episode torrent -type EpisodeTorrent struct { - sqly.BaseModel - polochon.Torrent - ShowImdbID string `json:"show_imdb_id"` - Season int `json:"season"` - Episode int `json:"episode"` -} - -// EpisodeTorrentDB represents the EpisodeTorrent in the DB -type EpisodeTorrentDB struct { - ID string `db:"id"` - ImdbID string `db:"imdb_id"` - URL string `db:"url"` - Source string `db:"source"` - Quality string `db:"quality"` - UploadUser string `db:"upload_user"` - Season int `db:"season"` - Episode int `db:"episode"` - Seeders int `db:"seeders"` - Leechers int `db:"leechers"` - Created time.Time `db:"created_at"` - Updated time.Time `db:"updated_at"` -} - -// NewEpisodeFromPolochon returns a new EpisodeTorrent from a polochon.ShowEpisode -func NewEpisodeFromPolochon(se polochon.ShowEpisode, poTo polochon.Torrent) *EpisodeTorrent { - return &EpisodeTorrent{ - ShowImdbID: se.ShowImdbID, - Season: se.Season, - Episode: se.Episode, - Torrent: poTo, - } -} - -// NewEpisode returns a new EpisodeTorrent -func NewEpisode() *EpisodeTorrent { - return &EpisodeTorrent{} -} - -// GetEpisodeTorrents returns show details in database from id or imdbid or an error -func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]*EpisodeTorrent, error) { - var torrentsDB = []*EpisodeTorrentDB{} - err := db.Select(&torrentsDB, getEpisodeTorrentQuery, imdbID, season, episode) - if err != nil { - return nil, err - } - - if len(torrentsDB) == 0 { - return nil, sql.ErrNoRows - } - - var torrents []*EpisodeTorrent - for _, torrentDB := range torrentsDB { - episode := NewEpisode() - episode.FillFromDB(torrentDB) - torrents = append(torrents, episode) - } - - return torrents, nil -} - -// Delete episode from database -func (e *EpisodeTorrent) Delete(db *sqlx.DB) error { - r, err := db.Exec(deleteEpisodeTorrentQuery, e.ID) - if err != nil { - return err - } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} - -// FillFromDB will fill a MovieTorrent from a MovieTorrentDB extracted from the DB -func (e *EpisodeTorrent) FillFromDB(eDB *EpisodeTorrentDB) { - q, _ := polochon.StringToQuality(eDB.Quality) - e.ShowImdbID = eDB.ImdbID - e.Created = eDB.Created - e.Updated = eDB.Updated - e.ID = eDB.ID - e.URL = eDB.URL - e.Source = eDB.Source - e.Quality = *q - e.UploadUser = eDB.UploadUser - e.Seeders = eDB.Seeders - e.Leechers = eDB.Leechers -} - -// Upsert an episode torrent in the database -func (e *EpisodeTorrent) Upsert(db *sqlx.DB) error { - eDB := NewEpisodeTorrentDB(e) - var id string - r, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB) - if err != nil { - return err - } - for r.Next() { - r.Scan(&id) - } - e.ID = id - - return nil -} - -// NewEpisodeTorrentDB returns an EpisodeTorrent ready to be put in DB from a -// EspisodeTorrent -func NewEpisodeTorrentDB(e *EpisodeTorrent) EpisodeTorrentDB { - return EpisodeTorrentDB{ - ID: e.ID, - ImdbID: e.ShowImdbID, - Season: e.Season, - Episode: e.Episode, - URL: e.URL, - Source: e.Source, - Quality: string(e.Quality), - UploadUser: e.UploadUser, - Seeders: e.Seeders, - Leechers: e.Leechers, - Created: e.Created, - Updated: e.Updated, - } -} diff --git a/src/internal/torrents/handlers.go b/src/internal/torrents/handlers.go index 21cae8f..20a51c8 100644 --- a/src/internal/torrents/handlers.go +++ b/src/internal/torrents/handlers.go @@ -6,11 +6,8 @@ import ( "net/http" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" - - "github.com/odwrtw/papi" ) // DownloadHandler downloads a movie via polochon @@ -33,19 +30,9 @@ func DownloadHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error return env.RenderError(w, errors.New("invalid user type")) } - var polochonConfig config.UserPolochon - err = user.GetConfig("polochon", &polochonConfig) + client, err := user.NewPapiClient() if err != nil { - return env.RenderError(w, errors.New("problem when getting user config")) - } - - client, err := papi.New(polochonConfig.URL) - if err != nil { - return env.RenderError(w, errors.New("problem when getting papi client")) - } - - if polochonConfig.Token != "" { - client.SetToken(polochonConfig.Token) + return env.RenderError(w, err) } err = client.AddTorrent(data.URL) diff --git a/src/internal/torrents/movie_torrents.go b/src/internal/torrents/movie_torrents.go deleted file mode 100644 index a872ff1..0000000 --- a/src/internal/torrents/movie_torrents.go +++ /dev/null @@ -1,171 +0,0 @@ -package torrents - -import ( - "database/sql" - "fmt" - "time" - - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - - "github.com/jmoiron/sqlx" - polochon "github.com/odwrtw/polochon/lib" -) - -const ( - upsertMovieTorrentQuery = ` - INSERT INTO movie_torrents (imdb_id, url, source, quality, upload_user, - seeders, leechers) - VALUES (:imdb_id, :url, :source, :quality, :upload_user, :seeders, - :leechers) - ON CONFLICT (imdb_id, quality, source) - DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality, - upload_user=:upload_user, seeders=:seeders, leechers=:leechers - RETURNING id;` - - getMovieTorrentQueryByImdbID = ` - SELECT * - FROM movie_torrents WHERE imdb_id=$1;` - - getMovieTorrentQueryByID = ` - SELECT * - FROM movie_torrents WHERE id=$1;` - - deleteMovieTorrentQuery = `DELETE FROM movie_torrents WHERE id=$1;` -) - -// MovieTorrent represents a movie torrent -type MovieTorrent struct { - sqly.BaseModel - polochon.Torrent - ImdbID string `json:"imdb_id"` -} - -// MovieTorrentDB represents the MovieTorrent in the DB -type MovieTorrentDB struct { - ID string `db:"id"` - ImdbID string `db:"imdb_id"` - URL string `db:"url"` - Source string `db:"source"` - Quality string `db:"quality"` - UploadUser string `db:"upload_user"` - Seeders int `db:"seeders"` - Leechers int `db:"leechers"` - Created time.Time `db:"created_at"` - Updated time.Time `db:"updated_at"` -} - -// NewMovieFromImDB returns a new MovieTorrent with an ImDB id -func NewMovieFromImDB(imdbID string) *MovieTorrent { - return &MovieTorrent{ - ImdbID: imdbID, - } -} - -// NewMovie returns a new MovieTorrent with an ImDB id -func NewMovie(imdbID string, poTo polochon.Torrent) *MovieTorrent { - return &MovieTorrent{ - ImdbID: imdbID, - Torrent: poTo, - } -} - -// Get returns show details in database from id or imdbid or an error -func (m *MovieTorrent) Get(db *sqlx.DB) error { - var mDB MovieTorrentDB - var err error - if m.ID != "" { - err = db.QueryRowx(getMovieTorrentQueryByID, m.ID).StructScan(&mDB) - } else if m.ImdbID != "" { - err = db.QueryRowx(getMovieTorrentQueryByImdbID, m.ImdbID).StructScan(&mDB) - } else { - err = fmt.Errorf("can't get movie torrent details, you have to specify an ID or ImdbID") - } - if err != nil { - return err - } - m.FillFromDB(&mDB) - return nil -} - -// GetMovieTorrents returns show details in database from id or imdbid or an error -func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]*MovieTorrent, error) { - var torrentsDB = []*MovieTorrentDB{} - err := db.Select(&torrentsDB, getMovieTorrentQueryByImdbID, imdbID) - if err != nil { - return nil, err - } - - if len(torrentsDB) == 0 { - return nil, sql.ErrNoRows - } - - var torrents []*MovieTorrent - for _, torrentDB := range torrentsDB { - movie := NewMovieFromImDB(imdbID) - movie.FillFromDB(torrentDB) - torrents = append(torrents, movie) - } - - return torrents, nil -} - -// Upsert a movie torrent in the database -func (m *MovieTorrent) Upsert(db *sqlx.DB) error { - mDB := NewMovieTorrentDB(m) - var id string - r, err := db.NamedQuery(upsertMovieTorrentQuery, mDB) - if err != nil { - return err - } - for r.Next() { - r.Scan(&id) - } - m.ID = id - - return nil -} - -// Delete movie from database -func (m *MovieTorrent) Delete(db *sqlx.DB) error { - r, err := db.Exec(deleteMovieTorrentQuery, m.ID) - if err != nil { - return err - } - count, _ := r.RowsAffected() - if count != 1 { - return fmt.Errorf("Unexpected number of row deleted: %d", count) - } - return nil -} - -// NewMovieTorrentDB returns a MovieTorrent ready to be put in DB from a -// MovieTorrent -func NewMovieTorrentDB(m *MovieTorrent) MovieTorrentDB { - return MovieTorrentDB{ - ID: m.ID, - ImdbID: m.ImdbID, - URL: m.URL, - Source: m.Source, - Quality: string(m.Quality), - UploadUser: m.UploadUser, - Seeders: m.Seeders, - Leechers: m.Leechers, - Created: m.Created, - Updated: m.Updated, - } -} - -// FillFromDB will fill a MovieTorrent from a MovieTorrentDB extracted from the DB -func (m *MovieTorrent) FillFromDB(mDB *MovieTorrentDB) { - q, _ := polochon.StringToQuality(mDB.Quality) - m.ImdbID = mDB.ImdbID - m.Created = mDB.Created - m.Updated = mDB.Updated - m.ID = mDB.ID - m.URL = mDB.URL - m.Source = mDB.Source - m.Quality = *q - m.UploadUser = mDB.UploadUser - m.Seeders = mDB.Seeders - m.Leechers = mDB.Leechers -} diff --git a/src/internal/users/handlers.go b/src/internal/users/handlers.go index eda6af9..6adf924 100644 --- a/src/internal/users/handlers.go +++ b/src/internal/users/handlers.go @@ -13,6 +13,7 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" ) +// SignupPOSTHandler handles the user's Signup func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error { var data struct { Username string `json:"username"` diff --git a/src/internal/users/users.go b/src/internal/users/users.go index 80aa56b..c975923 100644 --- a/src/internal/users/users.go +++ b/src/internal/users/users.go @@ -2,10 +2,14 @@ package users import ( "encoding/json" + "errors" "fmt" "github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx/types" + "github.com/odwrtw/papi" + + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/random" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" ) @@ -23,7 +27,9 @@ const ( ) const ( - UserRole = "user" + // UserRole represents the user's role + UserRole = "user" + // AdminRole represents the admin's role AdminRole = "admin" ) @@ -89,6 +95,25 @@ func (u *User) NewConfig() error { return u.RawConfig.UnmarshalJSON(b) } +// NewPapiClient creates a new papi client for the given user +func (u *User) NewPapiClient() (*papi.Client, error) { + var polochonConfig config.UserPolochon + err := u.GetConfig("polochon", &polochonConfig) + if err != nil { + return nil, errors.New("missing polochon config") + } + + client, err := papi.New(polochonConfig.URL) + if err != nil { + return nil, errors.New("error getting papi client") + } + + if polochonConfig.Token != "" { + client.SetToken(polochonConfig.Token) + } + return client, nil +} + // Token represents a token type Token struct { sqly.BaseModel @@ -201,6 +226,7 @@ func (u *User) GetName() string { return u.Name } +// HasRole checks if a user as a role func (u *User) HasRole(role string) bool { if role == AdminRole && !u.Admin { return false @@ -208,6 +234,7 @@ func (u *User) HasRole(role string) bool { return true } +// IsAdmin checks if a user is admin func (u *User) IsAdmin() bool { return u.HasRole(AdminRole) } diff --git a/src/internal/users/users_test.go b/src/internal/users/users_test.go deleted file mode 100644 index b1add23..0000000 --- a/src/internal/users/users_test.go +++ /dev/null @@ -1,337 +0,0 @@ -package users - -import ( - "fmt" - "os" - "testing" - - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" - - "github.com/Sirupsen/logrus" - "github.com/jmoiron/sqlx" - "github.com/lib/pq" - _ "github.com/mattes/migrate/driver/postgres" -) - -var db *sqlx.DB -var pgdsn string - -func init() { - var err error - - pgdsn = os.Getenv("POSTGRES_DSN") - db, err = sqlx.Connect("postgres", pgdsn) - - if err != nil { - fmt.Printf("Unavailable PG tests:\n %v\n", err) - os.Exit(1) - } -} - -func TestUser(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - - // Add a new user - u := &User{Name: "plop", Hash: "plop"} - var err error - env := getEnv(db) - - u.Hash, err = env.Auth.GenHash("test") - if err != nil { - t.Fatal(err) - } - - err = u.NewConfig() - if err != nil { - t.Fatal(err) - } - err = u.Add(db) - if err != nil { - t.Fatal(err) - } - - // Try add it twice - err = u.Add(db) - if err != nil { - if err, ok := err.(*pq.Error); ok { - if err.Code.Name() != "unique_violation" { - t.Fatal(err) - } - } - } else { - t.Fatal(err) - } - - // Get it - u2, err := Get(db, "plop") - if err != nil { - t.Fatal(err) - } - if u2.ID != u.ID { - t.Fatal("ID are different") - } - - // Update it - u2.Name = "toto" - err = u2.Update(db) - if err != nil { - t.Fatal(err) - } - - u3, err := Get(db, "toto") - if err != nil { - t.Fatal(err) - } - if u3.ID != u2.ID { - t.Fatal("ID are different") - } - if u3.Name != "toto" { - t.Fatalf("Unexpected name %q", u3.Name) - } - - // Delete it - err = u3.Delete(db) - if err != nil { - t.Fatal(err) - } - - u, err = Get(db, "toto") - if err == nil { - t.Fatal("We expect an error here, the user didn't exist anymore") - } - if err != ErrUnknownUser { - t.Fatalf("Unexpected error: %q", err) - } - if u != nil { - t.Fatal("User have to be nil here") - } - }) -} - -func TestTokenAddDelete(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - // Add a new user - u := &User{Name: "plop", Hash: "plop"} - var err error - env := getEnv(db) - - u.Hash, err = env.Auth.GenHash("test") - if err != nil { - t.Fatal(err) - } - - err = u.NewConfig() - if err != nil { - t.Fatal(err) - } - err = u.Add(db) - if err != nil { - t.Fatal(err) - } - - // Add many token - _, err = u.NewToken(db) - _, err = u.NewToken(db) - token, err := u.NewToken(db) - if err != nil { - t.Fatal(err) - } - - // Get token - tokens, err := u.GetTokens(db) - if err != nil { - t.Fatal(err) - } - if len(tokens) != 3 { - t.Fatalf("Unexpected number of token: %q", len(tokens)) - } - - // Delete token - err = u.DeleteToken(db, token.Value) - if err != nil { - t.Fatal(err) - } - tokens, _ = u.GetTokens(db) - if len(tokens) != 2 { - t.Fatalf("Unexpected number of token: %q", len(tokens)) - } - - // Delete a fake token - err = u.DeleteToken(db, "plop") - if err == nil { - t.Fatalf("We expect an error when try to delete a fake token") - } - if err.Error() != "Unexpected number of row deleted: 0" { - t.Fatalf("Unexpected error: %q", err) - } - - // Remove user and test auto delete token - u.Delete(db) - q := `SELECT count(*) FROM tokens WHERE user_id=$1;` - var count int - err = db.QueryRowx(q, u.ID).Scan(&count) - if err != nil { - t.Fatal(err) - } - if count != 0 { - t.Fatalf("Unexpected number of token: %d", count) - } - - }) -} - -// UserBackend represents the data backend to get the user -type UserBackend struct { - Database *sqlx.DB -} - -// Get gets the username from the UserBackend -func (b *UserBackend) Get(username string) (auth.User, error) { - return Get(b.Database, username) -} - -func getEnv(db *sqlx.DB) *web.Env { - uBackend := &UserBackend{Database: db} - - authParams := auth.Params{ - Backend: uBackend, - Pepper: "pepper", - Cost: 10, - Secret: "secret", - } - authorizer := auth.New(authParams) - - log := logrus.NewEntry(logrus.New()) - env := web.NewEnv(web.EnvParams{ - Database: db, - Auth: authorizer, - Log: log, - // Config: cf, - }) - - return env -} - -func TestTokenCheck(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - u := &User{Name: "plop", Hash: "plop"} - var err error - env := getEnv(db) - - u.Hash, err = env.Auth.GenHash("test") - if err != nil { - t.Fatal(err) - } - - err = u.NewConfig() - if err != nil { - t.Fatal(err) - } - if err = u.Add(db); err != nil { - t.Fatal(err) - } - token, err := u.NewToken(db) - if err != nil { - t.Fatal(err) - } - - ok, err := u.CheckToken(db, token.Value) - if err != nil { - t.Fatal(err) - } - if !ok { - t.Fatalf("Token not found") - } - - // test fake token - ok, err = u.CheckToken(db, "plop") - if err != nil { - t.Fatal(err) - } - if ok { - t.Fatalf("Token found otherwise we don't expect") - } - - }) -} - -func TestAutoUpdateCols(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - u := &User{Name: "plop"} - var err error - env := getEnv(db) - - u.Hash, err = env.Auth.GenHash("pass") - if err != nil { - t.Fatal(err) - } - - err = u.NewConfig() - if err != nil { - t.Fatal(err) - } - - err = u.Add(db) - if err != nil { - t.Fatal(err) - } - u.Name = "toto" - err = u.Update(db) - if err != nil { - t.Fatal(err) - } - - if !u.Created.Before(u.Updated) { - t.Fatalf("colum updated not auto updated on table users") - } - }) -} - -func TestConfig(t *testing.T) { - sqly.RunWithLastestMigration(db, pgdsn, t, func(db *sqlx.DB, t *testing.T) { - u := &User{Name: "plop", Hash: "plop"} - env := getEnv(db) - - var err error - u.Hash, err = env.Auth.GenHash("pass") - if err != nil { - t.Fatal(err) - } - - err = u.NewConfig() - if err != nil { - t.Fatal(err) - } - - err = u.Add(db) - if err != nil { - t.Fatal(err) - } - - u, err = Get(db, "plop") - if err != nil { - t.Fatal(err) - } - type test struct { - Test string - } - te := test{"coucou"} - err = u.SetConfig("test1", te) - if err != nil { - t.Fatal(err) - } - - var te2 test - err = u.GetConfig("test1", &te2) - if err != nil { - t.Fatal(err) - } - - if te != te2 { - t.Fatal("unexpected config") - } - - }) -} diff --git a/src/internal/web/env.go b/src/internal/web/env.go index 3b80e54..0867ee3 100644 --- a/src/internal/web/env.go +++ b/src/internal/web/env.go @@ -7,12 +7,14 @@ import ( "github.com/Sirupsen/logrus" "github.com/gorilla/mux" "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" "github.com/unrolled/render" "github.com/urfave/negroni" ) // Env describes an environement object passed to all handlers type Env struct { + Backend DetailerTorrenter Database *sqlx.DB Log *logrus.Entry Router *mux.Router @@ -28,6 +30,14 @@ type EnvParams struct { Auth *auth.Authorizer Log *logrus.Entry Config *config.Config + Backend DetailerTorrenter +} + +// DetailerTorrenter represents an object implementing polochon.Detailer and +// polochon.Torrenter +type DetailerTorrenter struct { + Detailer polochon.Detailer + Torrenter polochon.Torrenter } // NewEnv returns a new *Env @@ -39,24 +49,29 @@ func NewEnv(p EnvParams) *Env { Auth: p.Auth, Config: p.Config, Render: render.New(), + Backend: p.Backend, } } +// Route represents a route type Route struct { env *Env mRoute *mux.Route } +// Name returns the name of a route func (r *Route) Name(name string) *Route { r.mRoute.Name(name) return r } +// Methods returns a list of methods for a route func (r *Route) Methods(methods ...string) *Route { r.mRoute.Methods(methods...) return r } +// WithRole sets a given role for a route func (r *Route) WithRole(role string) *Route { handler := r.mRoute.GetHandler() newHandler := negroni.New( diff --git a/src/main.go b/src/main.go index 78fc2c2..d59621e 100644 --- a/src/main.go +++ b/src/main.go @@ -7,17 +7,14 @@ import ( "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents" - "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" + extmedias "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "github.com/Sirupsen/logrus" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "github.com/phyber/negroni-gzip/gzip" + "github.com/robfig/cron" "github.com/urfave/negroni" ) @@ -61,43 +58,38 @@ func main() { Auth: authorizer, Log: log, Config: cf, + Backend: web.DetailerTorrenter{ + Torrenter: backend, + Detailer: backend, + }, }) authMiddleware := auth.NewMiddleware(env.Auth, log) - env.Handle("/users/login", users.LoginPOSTHandler).Methods("POST") - env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST") - env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole).Methods("GET") - env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole).Methods("POST") + // Setup the routes + setupRoutes(env) - env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole).Methods("GET") - env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.GetDetailsHandler).WithRole(users.UserRole).Methods("POST") - env.Handle("/movies/{id:tt[0-9]+}", movies.DeleteHandler).WithRole(users.AdminRole).Methods("DELETE") - env.Handle("/movies/explore", extmedias.Explore).WithRole(users.UserRole).Methods("GET") - env.Handle("/movies/refresh", extmedias.Refresh).WithRole(users.UserRole).Methods("POST") - env.Handle("/movies/search", movies.SearchMovie).WithRole(users.UserRole).Methods("POST") + // Create the cron object + c := cron.New() - env.Handle("/shows/polochon", shows.FromPolochon).WithRole(users.UserRole).Methods("GET") - env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler).WithRole(users.UserRole).Methods("GET") - env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(users.UserRole).Methods("POST") - env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshDetailsHandler).WithRole(users.UserRole).Methods("POST") - env.Handle("/shows/refresh", extmedias.RefreshShows).WithRole(users.UserRole).Methods("POST") - env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET") - env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST") + // Refresh the library every 6h + c.AddFunc("@every 6h", func() { + env.Log.Infof("Running refresh cron!") + extmedias.Refresh(env) + }) - env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") + // Start the cron + c.Start() - env.Handle("/wishlist/shows", shows.GetWishlistHandler).WithRole(users.UserRole).Methods("GET") - env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.AddToWishlist).WithRole(users.UserRole).Methods("POST") - env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.DeleteFromWishlist).WithRole(users.UserRole).Methods("DELETE") - - env.Handle("/wishlist/movies", movies.GetWishlistHandler).WithRole(users.UserRole).Methods("GET") - env.Handle("/wishlist/movies/{id:tt[0-9]+}", movies.AddToWishlist).WithRole(users.UserRole).Methods("POST") - env.Handle("/wishlist/movies/{id:tt[0-9]+}", movies.DeleteFromWishlist).WithRole(users.UserRole).Methods("DELETE") + // Stop the cron + defer c.Stop() n := negroni.Classic() + // Middleware for authentication n.Use(authMiddleware) + // Serve static files n.Use(negroni.NewStatic(http.Dir(cf.PublicDir))) + // Compress responses n.Use(gzip.Gzip(gzip.DefaultCompression)) n.UseHandler(env.Router) n.Run(":" + cf.Port) diff --git a/src/routes.go b/src/routes.go new file mode 100644 index 0000000..ef5fc48 --- /dev/null +++ b/src/routes.go @@ -0,0 +1,51 @@ +package main + +import ( + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" +) + +func setupRoutes(env *web.Env) { + // User's route + env.Handle("/users/login", users.LoginPOSTHandler).Methods("POST") + env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST") + env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole).Methods("POST") + + // Movies routes + env.Handle("/movies/polochon", movies.PolochonMoviesHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/movies/explore", extmedias.ExploreMovies).WithRole(users.UserRole).Methods("GET") + env.Handle("/movies/search", movies.SearchMovie).WithRole(users.UserRole).Methods("POST") + env.Handle("/movies/{id:tt[0-9]+}", movies.PolochonDeleteHandler).WithRole(users.AdminRole).Methods("DELETE") + env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.RefreshMovieHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/movies/refresh", extmedias.RefreshMoviesHandler).WithRole(users.AdminRole).Methods("POST") + + // Shows routes + env.Handle("/shows/polochon", shows.PolochonShowsHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET") + env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST") + env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshShowHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(users.UserRole).Methods("POST") + env.Handle("/shows/refresh", extmedias.RefreshShowsHandler).WithRole(users.AdminRole).Methods("POST") + + // Wishlist routes for shows + env.Handle("/wishlist/shows", shows.GetWishlistHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.AddToWishlist).WithRole(users.UserRole).Methods("POST") + env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.DeleteFromWishlist).WithRole(users.UserRole).Methods("DELETE") + + // Wishlist routes for movies + env.Handle("/wishlist/movies", movies.GetWishlistHandler).WithRole(users.UserRole).Methods("GET") + env.Handle("/wishlist/movies/{id:tt[0-9]+}", movies.AddToWishlist).WithRole(users.UserRole).Methods("POST") + env.Handle("/wishlist/movies/{id:tt[0-9]+}", movies.DeleteFromWishlist).WithRole(users.UserRole).Methods("DELETE") + + // Torrents routes + env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST") + + // Route to refresh all movies and shows + env.Handle("/refresh", extmedias.RefreshHandler).WithRole(users.AdminRole).Methods("POST") +} From 334b4be47e7363ef55ac458f38dcc9c6cf18552c Mon Sep 17 00:00:00 2001 From: Lucas BEE Date: Tue, 21 Mar 2017 13:56:54 +0000 Subject: [PATCH 4/4] Fix SQL connection not getting closed Fix small bug in js as id field is not being returned anymore --- src/internal/backend/episode_torrents.go | 3 ++- src/internal/backend/episodes.go | 6 +----- src/internal/backend/explorer.go | 3 ++- src/internal/backend/movie_torrents.go | 3 ++- src/internal/backend/movie_wishlist.go | 13 ++++++++----- src/internal/backend/movies.go | 3 ++- src/internal/backend/show_wishlist.go | 5 ++++- src/internal/backend/shows.go | 3 ++- src/internal/external_medias/external_medias.go | 4 +--- src/internal/external_medias/handlers.go | 4 ++-- src/internal/movies/handlers.go | 8 +------- src/internal/shows/shows.go | 3 ++- src/public/js/reducers/shows.js | 2 +- 13 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/internal/backend/episode_torrents.go b/src/internal/backend/episode_torrents.go index acd3320..b8530bf 100644 --- a/src/internal/backend/episode_torrents.go +++ b/src/internal/backend/episode_torrents.go @@ -96,10 +96,11 @@ func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]polo func UpsertEpisodeTorrent(db *sqlx.DB, torrent *polochon.Torrent, imdbID string, season, episode int) error { // Create the EpisodeTorrent ready to be put in db eDB := NewEpisodeTorrentDB(torrent, imdbID, season, episode) - _, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB) + r, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB) if err != nil { return err } + defer r.Close() return nil } diff --git a/src/internal/backend/episodes.go b/src/internal/backend/episodes.go index e7a521a..afa42a7 100644 --- a/src/internal/backend/episodes.go +++ b/src/internal/backend/episodes.go @@ -128,14 +128,10 @@ func GetEpisodes(db *sqlx.DB, pShow *polochon.Show, log *logrus.Entry) error { // UpsertEpisode upserts the episode func UpsertEpisode(db *sqlx.DB, showEpisode *polochon.ShowEpisode) error { e := NewEpisodeFromPolochon(showEpisode) - var id string r, err := db.NamedQuery(upsertEpisodeQuery, e) if err != nil { return err } - for r.Next() { - r.Scan(&id) - } - e.ID = id + defer r.Close() return nil } diff --git a/src/internal/backend/explorer.go b/src/internal/backend/explorer.go index e8a1160..d59d9e7 100644 --- a/src/internal/backend/explorer.go +++ b/src/internal/backend/explorer.go @@ -39,9 +39,10 @@ func Explore(db *sqlx.DB, mtype, msrc, mcat string) (*Media, error) { // Upsert adds or updates the Media in the database func (m *Media) Upsert(db *sqlx.DB) error { - _, err := db.NamedQuery(upsertExternalMediaQuery, m) + r, err := db.NamedQuery(upsertExternalMediaQuery, m) if err != nil { return err } + defer r.Close() return nil } diff --git a/src/internal/backend/movie_torrents.go b/src/internal/backend/movie_torrents.go index a204f7c..071cd67 100644 --- a/src/internal/backend/movie_torrents.go +++ b/src/internal/backend/movie_torrents.go @@ -92,10 +92,11 @@ func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]polochon.Torrent, error) { // UpsertMovieTorrent adds or updates MovieTorrent in db func UpsertMovieTorrent(db *sqlx.DB, t *polochon.Torrent, imdbID string) error { mDB := NewMovieTorrentDB(t, imdbID) - _, err := db.NamedQuery(upsertMovieTorrentQuery, mDB) + r, err := db.NamedQuery(upsertMovieTorrentQuery, mDB) if err != nil { return err } + defer r.Close() return nil } diff --git a/src/internal/backend/movie_wishlist.go b/src/internal/backend/movie_wishlist.go index 8b2615d..7def493 100644 --- a/src/internal/backend/movie_wishlist.go +++ b/src/internal/backend/movie_wishlist.go @@ -16,7 +16,7 @@ const ( isMovieWishlistedQueryByUserID = ` SELECT - movies.imdb_id + COUNT(*) FROM movies INNER JOIN movies_tracked ON movies.imdb_id=movies_tracked.imdb_id @@ -89,13 +89,13 @@ func (w *MovieWishlist) IsMovieInWishlist(imdbID string) bool { // IsMovieWishlisted returns true if the movie is wishlisted func IsMovieWishlisted(db *sqlx.DB, userID, imdbID string) (bool, error) { - var movies = []string{} + var count int // Check if the movie is wishlisted by the user - err := db.Select(&movies, isMovieWishlistedQueryByUserID, imdbID, userID) + err := db.Get(&count, isMovieWishlistedQueryByUserID, imdbID, userID) if err != nil { return false, err } - if len(movies) > 0 { + if count > 0 { return true, nil } @@ -117,7 +117,10 @@ func DeleteMovieFromWishlist(db *sqlx.DB, userID, imdbID string) error { if err != nil { return err } - count, _ := r.RowsAffected() + count, err := r.RowsAffected() + if err != nil { + return err + } if count != 1 { return fmt.Errorf("Unexpected number of row deleted: %d", count) } diff --git a/src/internal/backend/movies.go b/src/internal/backend/movies.go index 15880a8..a987328 100644 --- a/src/internal/backend/movies.go +++ b/src/internal/backend/movies.go @@ -87,10 +87,11 @@ func FillFromDB(mDB *MovieDB, pMovie *polochon.Movie) { // UpsertMovie upsert a polochon Movie in the database func UpsertMovie(db *sqlx.DB, pMovie *polochon.Movie) error { mDB := NewMovieDB(pMovie) - _, err := db.NamedQuery(upsertMovieQuery, mDB) + r, err := db.NamedQuery(upsertMovieQuery, mDB) if err != nil { return err } + defer r.Close() return nil } diff --git a/src/internal/backend/show_wishlist.go b/src/internal/backend/show_wishlist.go index ab0e6fc..33c9d8c 100644 --- a/src/internal/backend/show_wishlist.go +++ b/src/internal/backend/show_wishlist.go @@ -132,7 +132,10 @@ func DeleteShowFromWishlist(db *sqlx.DB, userID, imdbID string) error { if err != nil { return err } - count, _ := r.RowsAffected() + count, err := r.RowsAffected() + if err != nil { + return err + } if count != 1 { return fmt.Errorf("Unexpected number of row deleted: %d", count) } diff --git a/src/internal/backend/shows.go b/src/internal/backend/shows.go index fd4611a..bb11213 100644 --- a/src/internal/backend/shows.go +++ b/src/internal/backend/shows.go @@ -43,10 +43,11 @@ type ShowDB struct { func UpsertShow(db *sqlx.DB, s *polochon.Show) error { sDB := NewShowFromPolochon(s) // Upsert the show - _, err := db.NamedQuery(upsertShowQuery, sDB) + r, err := db.NamedQuery(upsertShowQuery, sDB) if err != nil { return err } + defer r.Close() for _, e := range s.Episodes { // Upsert its episodes diff --git a/src/internal/external_medias/external_medias.go b/src/internal/external_medias/external_medias.go index d2de14c..6d8bb5f 100644 --- a/src/internal/external_medias/external_medias.go +++ b/src/internal/external_medias/external_medias.go @@ -114,10 +114,9 @@ func RefreshShows(env *web.Env) { } } } - env.Log.Info("========================================================") // Iterate over the map of shows to refresh them for id := range showMap { - show := shows.New(id) + show := shows.New(id, env.Config.PublicDir) // Refresh the shows err := show.Refresh(env, env.Config.ShowDetailers) if err != nil { @@ -160,7 +159,6 @@ func RefreshMovies(env *web.Env) { } } } - env.Log.Info("========================================================") // Iterate over the map of movies to refresh them for id := range movieMap { movie := movies.New(id, nil, nil, false, env.Config.PublicDir) diff --git a/src/internal/external_medias/handlers.go b/src/internal/external_medias/handlers.go index 4d1dd04..76b5511 100644 --- a/src/internal/external_medias/handlers.go +++ b/src/internal/external_medias/handlers.go @@ -170,7 +170,7 @@ func ExploreMovies(env *web.Env, w http.ResponseWriter, r *http.Request) error { source := r.FormValue("source") // Default source if source == "" { - source = "yts" + source = "trakttv" } category := r.FormValue("category") @@ -210,7 +210,7 @@ func ExploreShows(env *web.Env, w http.ResponseWriter, r *http.Request) error { category := r.FormValue("category") // Default category if category == "" { - category = "popular" + category = "rating" } v := auth.GetCurrentUser(r, env.Log) diff --git a/src/internal/movies/handlers.go b/src/internal/movies/handlers.go index 39a83c1..f293bf0 100644 --- a/src/internal/movies/handlers.go +++ b/src/internal/movies/handlers.go @@ -11,7 +11,6 @@ import ( "github.com/gorilla/mux" "github.com/odwrtw/papi" polochon "github.com/odwrtw/polochon/lib" - "github.com/odwrtw/polochon/modules/pam" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend" @@ -42,17 +41,12 @@ func PolochonMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) return err } - detailer, err := pam.New(&pam.Params{ - Endpoint: polochonConfig.URL, - Token: polochonConfig.Token, - }) - // Get details with the polochon Detailer for each polochon.Movies we have for _, m := range movies { // First try from the db first := []polochon.Detailer{env.Backend.Detailer} // Then try from the polochon detailer - detailers := []polochon.Detailer{detailer} + detailers := env.Config.MovieDetailers err := m.GetAndFetch(env, first, detailers) if err != nil { env.Log.Error(err) diff --git a/src/internal/shows/shows.go b/src/internal/shows/shows.go index 02ea1ae..20d19a0 100644 --- a/src/internal/shows/shows.go +++ b/src/internal/shows/shows.go @@ -56,11 +56,12 @@ func (s *Show) MarshalJSON() ([]byte, error) { } // New returns a new Show with a polochon ShowConfig -func New(imdbID string) *Show { +func New(imdbID string, publicDir string) *Show { return &Show{ Show: &polochon.Show{ ImdbID: imdbID, }, + publicDir: publicDir, } } diff --git a/src/public/js/reducers/shows.js b/src/public/js/reducers/shows.js index 3349b09..d6ef5cb 100644 --- a/src/public/js/reducers/shows.js +++ b/src/public/js/reducers/shows.js @@ -95,7 +95,7 @@ function updateEpisode(show, fetching, data = null) { let seasonIndex = show.seasons.map((el) => el.season).indexOf(data.season.toString()); let episodeIndex = show.seasons[seasonIndex].episodes.map((el) => el.episode).indexOf(data.episode); - if ('id' in data) { + if ('imdb_id' in data) { show.seasons[seasonIndex].episodes[episodeIndex] = data; } show.seasons[seasonIndex].episodes[episodeIndex].fetching = fetching;