Merge branch 'newCanape' into 'master'

Massive Rewrite

See merge request !48
This commit is contained in:
Grégoire Delattre 2017-03-21 14:00:34 +00:00
commit f68c2e7100
41 changed files with 2355 additions and 2510 deletions

64
INTERNAL.md Normal file
View File

@ -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)

View File

@ -47,13 +47,7 @@ yarn start
## Connect to the database ## Connect to the database
``` ```
docker run -it --rm --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test docker run -it --rm -e PGPASSWORD=test --link canape_postgresql_dev:postgres postgres:9.5 psql -h postgres -U test -d dev
```
You'll need to connect to the dev database with this command:
```
\c dev
``` ```
## Default users ## Default users

View File

@ -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;

View File

@ -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;

View File

@ -21,7 +21,7 @@ var (
// UserBackend interface for user backend // UserBackend interface for user backend
type UserBackend interface { type UserBackend interface {
Get(username string) (User, error) GetUser(username string) (User, error)
} }
// User interface for user // User interface for user
@ -64,7 +64,7 @@ func (a *Authorizer) GenHash(password string) (string, error) {
// Login cheks password and creates a jwt token // Login cheks password and creates a jwt token
func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) (User, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -109,7 +109,7 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use
} }
// Get the user // Get the user
u, err := a.Backend.Get(tokenClaims.Username) u, err := a.Backend.GetUser(tokenClaims.Username)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -13,6 +13,8 @@ type Middleware struct {
log *logrus.Entry log *logrus.Entry
} }
type authContextKey string
// NewMiddleware returns a new authentication middleware // NewMiddleware returns a new authentication middleware
func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware { func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware {
return &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") 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) r = r.WithContext(ctx)
next(w, r) 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 { func GetCurrentUser(r *http.Request, log *logrus.Entry) User {
log.Debug("getting user from context") log.Debug("getting user from context")
u := r.Context().Value("auth.user") ctxKey := authContextKey("auth.user")
u := r.Context().Value(ctxKey)
if u == nil { if u == nil {
return nil return nil
} }

View File

@ -0,0 +1,24 @@
package backend
import (
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"github.com/jmoiron/sqlx"
)
// Backend represents the data backend
type Backend struct {
Database *sqlx.DB
}
// Name implements the Module interface
func (b *Backend) Name() string {
return "canape-backend"
}
// 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)
}

View File

@ -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
}

View File

@ -0,0 +1,106 @@
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)
r, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB)
if err != nil {
return err
}
defer r.Close()
return nil
}

View File

@ -0,0 +1,137 @@
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)
r, err := db.NamedQuery(upsertEpisodeQuery, e)
if err != nil {
return err
}
defer r.Close()
return nil
}

View File

@ -0,0 +1,48 @@
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 {
r, err := db.NamedQuery(upsertExternalMediaQuery, m)
if err != nil {
return err
}
defer r.Close()
return nil
}

View File

@ -0,0 +1,102 @@
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)
r, err := db.NamedQuery(upsertMovieTorrentQuery, mDB)
if err != nil {
return err
}
defer r.Close()
return nil
}

View File

@ -0,0 +1,128 @@
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
COUNT(*)
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 count int
// Check if the movie is wishlisted by the user
err := db.Get(&count, isMovieWishlistedQueryByUserID, imdbID, userID)
if err != nil {
return false, err
}
if count > 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, err := r.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return fmt.Errorf("Unexpected number of row deleted: %d", count)
}
return nil
}

View File

@ -0,0 +1,120 @@
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)
r, err := db.NamedQuery(upsertMovieQuery, mDB)
if err != nil {
return err
}
defer r.Close()
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,
}
}

View File

@ -0,0 +1,143 @@
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, err := r.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return fmt.Errorf("Unexpected number of row deleted: %d", count)
}
return nil
}

View File

@ -0,0 +1,105 @@
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
r, err := db.NamedQuery(upsertShowQuery, sDB)
if err != nil {
return err
}
defer r.Close()
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
}

View File

@ -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
}

View File

@ -7,12 +7,14 @@ import (
polochon "github.com/odwrtw/polochon/lib" polochon "github.com/odwrtw/polochon/lib"
"github.com/odwrtw/polochon/modules/eztv" "github.com/odwrtw/polochon/modules/eztv"
"github.com/odwrtw/polochon/modules/tmdb" "github.com/odwrtw/polochon/modules/tmdb"
"github.com/odwrtw/polochon/modules/trakttv"
"github.com/odwrtw/polochon/modules/tvdb" "github.com/odwrtw/polochon/modules/tvdb"
"github.com/odwrtw/polochon/modules/yts" "github.com/odwrtw/polochon/modules/yts"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// Config represents the Config of the canape app
type Config struct { type Config struct {
Authorizer AuthorizerConfig `yaml:"authorizer"` Authorizer AuthorizerConfig `yaml:"authorizer"`
PGDSN string `yaml:"pgdsn"` PGDSN string `yaml:"pgdsn"`
@ -22,21 +24,26 @@ type Config struct {
// TODO improve the detailers and torrenters configurations // TODO improve the detailers and torrenters configurations
TmdbAPIKey string `yaml:"tmdb_api_key"` TmdbAPIKey string `yaml:"tmdb_api_key"`
MovieExplorers []polochon.Explorer
MovieDetailers []polochon.Detailer MovieDetailers []polochon.Detailer
MovieTorrenters []polochon.Torrenter MovieTorrenters []polochon.Torrenter
MovieSearchers []polochon.Searcher MovieSearchers []polochon.Searcher
ShowExplorers []polochon.Explorer
ShowDetailers []polochon.Detailer ShowDetailers []polochon.Detailer
ShowTorrenters []polochon.Torrenter ShowTorrenters []polochon.Torrenter
ShowSearchers []polochon.Searcher ShowSearchers []polochon.Searcher
} }
// AuthorizerConfig is the config for the authentication
type AuthorizerConfig struct { type AuthorizerConfig struct {
Pepper string `yaml:"pepper"` Pepper string `yaml:"pepper"`
Cost int `yaml:"cost"` Cost int `yaml:"cost"`
Secret string `yaml:"secret"` Secret string `yaml:"secret"`
} }
// Load loads a config file from a path and return a Config object
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
// Open the file
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -45,26 +52,51 @@ func Load(path string) (*Config, error) {
cf := &Config{} cf := &Config{}
// Read it
b, err := ioutil.ReadAll(file) b, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Unmarshal its content
err = yaml.Unmarshal(b, cf) err = yaml.Unmarshal(b, cf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Initialize the default values
// Default movie detailers // Default movie detailers
cf.MovieDetailers = []polochon.Detailer{} cf.MovieDetailers = []polochon.Detailer{}
if cf.TmdbAPIKey != "" { if cf.TmdbAPIKey != "" {
d, err := tmdb.New(&tmdb.Params{cf.TmdbAPIKey}) d, err := tmdb.New(
&tmdb.Params{
ApiKey: cf.TmdbAPIKey,
},
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cf.MovieDetailers = append(cf.MovieDetailers, d) 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 // Default movie torrenters
cf.MovieTorrenters = []polochon.Torrenter{} cf.MovieTorrenters = []polochon.Torrenter{}
d, err := yts.New() d, err := yts.New()
@ -89,6 +121,23 @@ func Load(path string) (*Config, error) {
} }
cf.ShowSearchers = append(cf.ShowSearchers, showSearcher) 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 // Default show detailers
cf.ShowDetailers = []polochon.Detailer{} cf.ShowDetailers = []polochon.Detailer{}
showDetailer, err := tvdb.NewDetailer() showDetailer, err := tvdb.NewDetailer()

View File

@ -1,58 +1,187 @@
package extmedias package extmedias
import ( import (
"github.com/jmoiron/sqlx" "fmt"
"github.com/lib/pq"
"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/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 ( // NewExplorer returns a polochon.Explorer from the list of Explorers in the config
upsertExternalMediaQuery = ` func NewExplorer(env *web.Env, mediaType, name string) (polochon.Explorer, error) {
INSERT INTO external_medias (type, source, category, ids) var explorers []polochon.Explorer
VALUES (:type, :source, :category, :ids) if mediaType == "movie" {
ON CONFLICT (type, source, category) explorers = env.Config.MovieExplorers
DO UPDATE SET type=:type, source=:source, category=:category, ids=:ids } else {
RETURNING id;` explorers = env.Config.ShowExplorers
}
deleteExternalMediaQuery = `DELETE FROM external_medias WHERE id=:id;` for _, e := range explorers {
getExternalMediaQuery = `SELECT * FROM external_medias WHERE type=$1 AND source=$2 AND category=$3 LIMIT 1;` // Check the name of the explorer
) if e.Name() == name {
return e, nil
// Media represents an external media }
type Media struct { }
sqly.BaseModel return nil, fmt.Errorf("no such explorer %s", name)
Type string `db:"type"`
Source string `db:"source"`
Category string `db:"category"`
IDs pq.StringArray `db:"ids"`
} }
// Upsert adds or updates or adds the Media in the database // GetMediaIDs will get some media IDs
func (m *Media) Upsert(db *sqlx.DB) error { func GetMediaIDs(env *web.Env, mediaType string, source string, category string) ([]string, error) {
var id string log := env.Log.WithFields(logrus.Fields{
r, err := db.NamedQuery(upsertExternalMediaQuery, m) "mediaType": mediaType,
"source": source,
"category": category,
"function": "extmedias.GetMediaIds",
})
// Get the explorer that we need
explorer, err := NewExplorer(env, mediaType, source)
if err != nil { 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 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{}{}
}
}
}
// Iterate over the map of shows to refresh them
for id := range showMap {
show := shows.New(id, env.Config.PublicDir)
// 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{}{}
}
}
}
// 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")
} }

View File

@ -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)
}
})
}

View File

@ -1,130 +1,107 @@
package extmedias package extmedias
import ( import (
"database/sql"
"errors" "errors"
"fmt"
"net/http" "net/http"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "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/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows" "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/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
polochon "github.com/odwrtw/polochon/lib" 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 // RefreshHandler refresh the explored movies
func GetMediaIDs(env *web.Env, mediaType string, source string, category string, force bool) ([]string, error) { func RefreshHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
log := env.Log.WithFields(logrus.Fields{ log := env.Log.WithFields(logrus.Fields{
"mediaType": mediaType, "function": "extmedias.RefreshHandler",
"source": source,
"category": category,
"function": "extmedias.GetMediaIds",
}) })
log.Debugf("refreshing shows and movies")
Refresh(env)
log.Debugf("done refreshing shows and movies")
return nil
}
var err error // RefreshMoviesHandler refresh the explored movies
media, err := Get(env.Database, mediaType, source, category) func RefreshMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
switch err { log := env.Log.WithFields(logrus.Fields{
case nil: "function": "extmedias.RefreshMoviesHandler",
log.Debugf("%s medias found in database", mediaType) })
if !force { log.Debugf("refreshing movies")
log.Debug("returning medias from db") RefreshMovies(env)
return media.IDs, nil log.Debugf("done refreshing movies")
} return nil
case sql.ErrNoRows: }
log.Debugf("%s medias not found in database", mediaType)
default:
// Unexpected error
return nil, err
}
explorer, err := NewExplorer(env, source) // RefreshShowsHandler refresh the explored shows
if err != nil { func RefreshShowsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return nil, err log := env.Log.WithFields(logrus.Fields{
} "function": "extmedias.RefreshShowsHandler",
})
var ids []string log.Debugf("refreshing shows")
if mediaType == "movie" { RefreshShows(env)
medias, err := explorer.GetMovieList(polochon.ExploreByRate, log) log.Debugf("done refreshing shows")
if err != nil { return 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
} }
// GetMovies get some movies // GetMovies get some movies
func GetMovies(env *web.Env, user *users.User, source string, category string, force bool) ([]*movies.Movie, error) { func GetMovies(env *web.Env, user *users.User, source string, category string) ([]*movies.Movie, error) {
movieIds, err := GetMediaIDs(env, "movie", source, category, force) 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 { if err != nil {
return nil, err return nil, err
} }
// Get the URLs from polochon, we don't really care if it fails for now // Create a papi client
var urls map[string]string client, err := user.NewPapiClient()
urls, err = movies.GetPolochonMoviesURLs(user)
if err != nil { 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{} movieList := []*movies.Movie{}
for _, id := range movieIds { // Fill all the movies infos from the list of IDs
movie := movies.New(id) for _, id := range media.IDs {
err := movie.GetDetails(env, user, force) 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 { if err != nil {
env.Log.Errorf("error while getting movie details : %s", err) env.Log.Errorf("error while getting movie details : %s", err)
continue 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 { if err != nil {
env.Log.Errorf("error while getting movie torrents : %s", err) env.Log.Errorf("error while getting movie torrents : %s", err)
continue continue
} }
if urls != nil {
if url, ok := urls[id]; ok {
movie.PolochonURL = url
}
}
movieList = append(movieList, movie) movieList = append(movieList, movie)
} }
return movieList, nil return movieList, nil
@ -132,31 +109,59 @@ func GetMovies(env *web.Env, user *users.User, source string, category string, f
// GetShows get some shows // GetShows get some shows
func GetShows(env *web.Env, user *users.User, source string, category string, force bool) ([]*shows.Show, error) { 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 { if err != nil {
return nil, err return nil, err
} }
showList := []*shows.Show{} showList := []*shows.Show{}
for _, id := range showIds { // Fill all the shows infos from the list of IDs
show := shows.New(id) for _, id := range media.IDs {
err := show.GetDetails(env, user, force) 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 { if err != nil {
env.Log.Errorf("error while getting show details : %s", err) env.Log.Errorf("error while getting show details : %s", err)
continue continue
} }
err = show.GetTorrents(env, force)
if err != nil {
env.Log.Errorf("error while getting show torrents : %s", err)
continue
}
showList = append(showList, show) showList = append(showList, show)
} }
return showList, nil return showList, nil
} }
// Explore will explore some movies // ExploreMovies will explore some movies from the db
func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error { func ExploreMovies(env *web.Env, w http.ResponseWriter, r *http.Request) error {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return err return err
@ -165,7 +170,7 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
source := r.FormValue("source") source := r.FormValue("source")
// Default source // Default source
if source == "" { if source == "" {
source = "yts" source = "trakttv"
} }
category := r.FormValue("category") category := r.FormValue("category")
@ -181,7 +186,7 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
} }
// Get the medias without trying to refresh them // 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 { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
@ -205,7 +210,7 @@ func ExploreShows(env *web.Env, w http.ResponseWriter, r *http.Request) error {
category := r.FormValue("category") category := r.FormValue("category")
// Default category // Default category
if category == "" { if category == "" {
category = "popular" category = "rating"
} }
v := auth.GetCurrentUser(r, env.Log) v := auth.GetCurrentUser(r, env.Log)
@ -222,94 +227,3 @@ func ExploreShows(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderJSON(w, shows) 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")
}
}

View File

@ -4,109 +4,50 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "log"
"net/http" "net/http"
"net/url"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/odwrtw/papi" "github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib" 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/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/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
) )
// ErrPolochonUnavailable is an error returned if the polochon server is not available // PolochonMoviesHandler will returns movies from Polochon
var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address") func PolochonMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// Get the user from the request
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 {
v := auth.GetCurrentUser(r, env.Log) v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User) user, ok := v.(*users.User)
if !ok { 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 { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
// Create a new polochon Detailer to get infos about the movies we just got
var polochonConfig config.UserPolochon var polochonConfig config.UserPolochon
err = user.GetConfig("polochon", &polochonConfig) err = user.GetConfig("polochon", &polochonConfig)
if err != nil { if err != nil {
return err return err
} }
detailer, err := pam.New(&pam.Params{ // Get details with the polochon Detailer for each polochon.Movies we have
Endpoint: polochonConfig.URL,
Token: polochonConfig.Token,
})
for _, m := range movies { for _, m := range movies {
m.Detailers = []polochon.Detailer{detailer} // First try from the db
err := m.GetDetails(env, user, false) first := []polochon.Detailer{env.Backend.Detailer}
// Then try from the polochon detailer
detailers := env.Config.MovieDetailers
err := m.GetAndFetch(env, first, detailers)
if err != nil { if err != nil {
env.Log.Error(err) env.Log.Error(err)
} }
@ -115,20 +56,47 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderJSON(w, movies) return env.RenderJSON(w, movies)
} }
// GetDetailsHandler retrieves details for a movie // RefreshMovieHandler refreshes details for a movie
func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { func RefreshMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
// Get the user
v := auth.GetCurrentUser(r, env.Log) v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User) user, ok := v.(*users.User)
if !ok { if !ok {
return fmt.Errorf("invalid user type") return fmt.Errorf("invalid user type")
} }
m := New(id) // Create a new papi client
if err := m.GetDetails(env, user, true); err != nil { client, err := user.NewPapiClient()
return err 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) return env.RenderJSON(w, m)
@ -154,8 +122,27 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, errors.New("invalid user")) 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 var movies []*polochon.Movie
searchers := env.Config.MovieSearchers searchers := env.Config.MovieSearchers
// Search for the movie with all the Searchers
for _, searcher := range searchers { for _, searcher := range searchers {
result, err := searcher.SearchMovie(data.Key, env.Log) result, err := searcher.SearchMovie(data.Key, env.Log)
if err != nil { if err != nil {
@ -165,42 +152,45 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error {
movies = append(movies, result...) 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) env.Log.Debugf("got %d movies doing search %q", len(movies), data.Key)
movieList := []*Movie{} movieList := []*Movie{}
// For each movie found, fill the details
for _, m := range movies { for _, m := range movies {
movie := New(m.ImdbID) pMovie, _ := pMovies.Has(m.ImdbID)
err := movie.GetDetails(env, user, false) 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 { if err != nil {
env.Log.Errorf("error while getting movie details : %s", err) env.Log.Errorf("error while getting movie details : %s", err)
continue 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 { if err != nil {
env.Log.Errorf("error while getting movie torrents : %s", err) env.Log.Errorf("error while getting movie torrents : %s", err)
continue continue
} }
if urls != nil {
if url, ok := urls[m.ImdbID]; ok {
movie.PolochonURL = url
}
}
movieList = append(movieList, movie) movieList = append(movieList, movie)
} }
return env.RenderJSON(w, movieList) return env.RenderJSON(w, movieList)
} }
// DeleteHandler deletes the movie from polochon // PolochonDeleteHandler deletes the movie from polochon
func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { func PolochonDeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
@ -210,27 +200,20 @@ func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
}) })
log.Debugf("deleting movie") log.Debugf("deleting movie")
// Get the user
v := auth.GetCurrentUser(r, env.Log) v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User) user, ok := v.(*users.User)
if !ok { if !ok {
return env.RenderError(w, errors.New("invalid user type")) return env.RenderError(w, errors.New("invalid user type"))
} }
var polochonConfig config.UserPolochon // Create a new papi client
err := user.GetConfig("polochon", &polochonConfig) client, err := user.NewPapiClient()
if err != nil { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
client, err := papi.New(polochonConfig.URL) // Delete the movie
if err != nil {
return env.RenderError(w, err)
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
}
return client.Delete(&papi.Movie{ImdbID: id}) return client.Delete(&papi.Movie{ImdbID: id})
} }
@ -245,8 +228,7 @@ func AddToWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, errors.New("invalid user type")) return env.RenderError(w, errors.New("invalid user type"))
} }
m := New(id) if err := backend.AddMovieToWishlist(env.Database, user.ID, id); err != nil {
if err := m.AddToWishlist(env, user); err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
@ -264,8 +246,7 @@ func DeleteFromWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) er
return env.RenderError(w, errors.New("invalid user type")) return env.RenderError(w, errors.New("invalid user type"))
} }
m := New(id) if err := backend.DeleteMovieFromWishlist(env.Database, user.ID, id); err != nil {
if err := m.DeleteFromWishlist(env, user); err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
@ -280,10 +261,55 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
return env.RenderError(w, errors.New("invalid user type")) 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 { if err != nil {
return env.RenderError(w, err) 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)
} }

View File

@ -2,301 +2,198 @@ package movies
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx" "github.com/odwrtw/papi"
"github.com/lib/pq"
"github.com/odwrtw/polochon/lib" "github.com/odwrtw/polochon/lib"
"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/torrents"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "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;`
getMovieQueryByImdbID = `
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;`
getMovieQueryByID = `
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;`
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 // Movie represents a movie
type Movie struct { type Movie struct {
sqly.BaseModel *polochon.Movie
polochon.Movie client *papi.Client
pMovie *papi.Movie
publicDir string
Wishlisted bool `json:"wishlisted"` Wishlisted bool `json:"wishlisted"`
PolochonURL string `json:"polochon_url"`
PosterURL string `json:"poster_url"`
} }
// New returns a new Movie with an ImDB id // MarshalJSON implements the Marshal interface
func New(imdbID string) *Movie { 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{ return &Movie{
Movie: polochon.Movie{ client: client,
pMovie: pMovie,
publicDir: publicDir,
Wishlisted: isWishlisted,
Movie: &polochon.Movie{
ImdbID: imdbID, ImdbID: imdbID,
}, },
} }
} }
// Get returns show details in database from id or imdbid or an error // GetDetails retrieves details for the movie with the given detailers
func (m *Movie) Get(env *web.Env, user *users.User) error { func (m *Movie) GetDetails(env *web.Env, detailers []polochon.Detailer) error {
var mDB MovieDB
var err error
if m.ID != "" {
err = env.Database.QueryRowx(getMovieQueryByID, m.ID, user.ID).StructScan(&mDB)
} else if m.ImdbID != "" {
err = env.Database.QueryRowx(getMovieQueryByImdbID, 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
}
// GetDetails retrieves details for the movie, first try to get info from db,
// 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 {
log := env.Log.WithFields(logrus.Fields{ log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID, "imdb_id": m.ImdbID,
"function": "movies.GetDetails", "function": "movies.GetDetails",
}) })
log.Debugf("getting details") log.Debugf("getting details")
if len(m.Detailers) == 0 { m.Detailers = detailers
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
}
// GetDetail // GetDetail
err = m.Movie.GetDetails(env.Log) err := m.Movie.GetDetails(env.Log)
if err != nil { if err != nil {
return err return err
} }
log.Debug("got details from detailers") 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 return nil
} }
// GetTorrents retrieves torrents for the movie, first try to get info from db, // GetAndFetch retrieves details for the movie with the given
// if not exists, use polochon.Torrenter and save informations in the database // detailers 'before'
// for future use // If found, return
// // If not, retrives details with the detailers 'after' and update them in
// If force is used, the torrenter will be used even if the torrent is found in
// database // 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{ log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID, "imdb_id": m.ImdbID,
"function": "movies.GetTorrents", "function": "movies.GetTorrents",
}) })
log.Debugf("getting torrents") log.Debugf("getting torrents")
if len(m.Torrenters) == 0 { m.Torrenters = torrenters
m.Torrenters = env.Config.MovieTorrenters
}
movieTorrents, err := torrents.GetMovieTorrents(env.Database, m.ImdbID) err := m.Movie.GetTorrents(env.Log)
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)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("got %d torrents from torrenters", len(m.Movie.Torrents)) 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 { for _, t := range m.Movie.Torrents {
torrent := torrents.NewMovie(m.ImdbID, t) err = backend.UpsertMovieTorrent(env.Database, &t, m.ImdbID)
err = torrent.Upsert(env.Database)
if err != nil { if err != nil {
log.Error("error while adding torrent", err) log.Error("error while adding torrent", err)
continue continue
@ -306,51 +203,59 @@ func (m *Movie) GetTorrents(env *web.Env, force bool) error {
return nil 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 // 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) return fmt.Sprintf("img/movies/%s.jpg", m.ImdbID)
} }
// imgFile returns the image location on disk // imgFile returns the image location on disk
func (m *Movie) imgFile(env *web.Env) string { func (m *Movie) imgFile() string {
return filepath.Join(env.Config.PublicDir, m.imgURL(env)) return filepath.Join(m.publicDir, m.imgURL())
} }
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded // PosterURL returns the image URL or the default image if the poster is not yet downloaded
func (m *Movie) GetPosterURL(env *web.Env) string { func (m *Movie) PosterURL() string {
// Check if the movie image exists // 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 ? // TODO image in the config ?
return "img/noimage.png" 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
} }

View File

@ -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)
}
})
}

View File

@ -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
}

View File

@ -18,6 +18,7 @@ const (
var src = rand.NewSource(time.Now().UnixNano()) var src = rand.NewSource(time.Now().UnixNano())
// String returns n random strings
func String(n int) string { func String(n int) string {
b := make([]byte, n) b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters! // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!

View File

@ -1,181 +1,140 @@
package shows package shows
import ( import (
"database/sql" "encoding/json"
"time"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx" "github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib" 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" "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 // Episode represents an episode
type Episode struct { type Episode struct {
sqly.BaseModel client *papi.Client
polochon.ShowEpisode pShow *papi.Show
PolochonURL string `json:"polochon_url"` *polochon.ShowEpisode
} }
// EpisodeDB represents the Episode in the DB // MarshalJSON implements the Marshal interface
type EpisodeDB struct { func (e *Episode) MarshalJSON() ([]byte, error) {
ID string `db:"id"` type alias Episode
TvdbID int `db:"tvdb_id"`
ImdbID string `db:"imdb_id"` var downloadURL string
ShowImdbID string `db:"show_imdb_id"` // If the episode is present, fill the downloadURL
ShowTvdbID int `db:"show_tvdb_id"` if e.pShow != nil && e.pShow.HasEpisode(e.Season, e.Episode) {
Season int `db:"season"` downloadURL, _ = e.client.DownloadURL(
Episode int `db:"episode"` &papi.Episode{
Title string `db:"title"` ShowImdbID: e.ShowImdbID,
Rating float32 `db:"rating"` Episode: e.Episode,
Plot string `db:"plot"` Season: e.Season,
Thumb string `db:"thumb"` },
Runtime int `db:"runtime"` )
Aired string `db:"aired"` }
ReleaseGroup string `db:"release_group"`
Created time.Time `db:"created_at"` // Marshal the episode with its polochon_url
Updated time.Time `db:"updated_at"` episodeToMarshal := &struct {
*alias
PolochonURL string `json:"polochon_url"`
}{
alias: (*alias)(e),
PolochonURL: downloadURL,
}
return json.Marshal(episodeToMarshal)
} }
// NewEpisode returns an Episode // NewEpisode returns an Episode
func NewEpisode() *Episode { func NewEpisode(client *papi.Client, pShow *papi.Show, imdbID string, season, episode int) *Episode {
return &Episode{} return &Episode{
} client: client,
pShow: pShow,
// NewEpisodeDB returns an Episode ready to be put in DB from an ShowEpisode: &polochon.ShowEpisode{
// Episode ShowImdbID: imdbID,
func NewEpisodeDB(e *Episode) EpisodeDB { Season: season,
return EpisodeDB{ Episode: episode,
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,
} }
} }
// FillFromDB returns a Show from a ShowDB extracted from the DB // GetAndFetchTorrents retrieves torrents for the episode with the given
func (e *Episode) FillFromDB(eDB *EpisodeDB) { // torrenters 'before'
e.ID = eDB.ID // If found, return
e.TvdbID = eDB.TvdbID // If not, retrives torrents with the torrenters 'after' and update them in
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
// database // 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{ log := env.Log.WithFields(logrus.Fields{
"imdb_id": e.ShowEpisode.ShowImdbID, "imdb_id": e.ShowImdbID,
"season": e.ShowEpisode.Season, "season": e.Season,
"episode": e.ShowEpisode.Episode, "episode": e.Episode,
"function": "shows.GetTorrents", "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") log.Debugf("getting torrents")
if len(e.Torrenters) == 0 { showEpisode.Torrenters = torrenters
e.Torrenters = env.Config.ShowTorrenters
}
episodeTorrents, err := torrents.GetEpisodeTorrents( err := showEpisode.GetTorrents(env.Log)
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)
if err != nil { if err != nil {
return err 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 { return nil
torrent := torrents.NewEpisodeFromPolochon(e.ShowEpisode, t) }
err = torrent.Upsert(env.Database)
// 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 { if err != nil {
log.Errorf("error while adding torrent : %s", err) log.Errorf("error while adding torrent : %s", err)
continue continue
@ -185,15 +144,35 @@ func (e *Episode) GetTorrents(env *web.Env, force bool) error {
return nil return nil
} }
// Get returns an episode // Refresh retrieves details for the episode with the given detailers
func (e *Episode) Get(env *web.Env) error { // and update them in database
var episodeDB EpisodeDB func (e *Episode) Refresh(env *web.Env, detailers []polochon.Detailer) error {
err := env.Database.QueryRowx(getEpisodeQuery, e.ShowImdbID, e.Season, e.Episode).StructScan(&episodeDB) // Refresh
err := e.GetEpisodeDetails(env, detailers)
if err != nil { if err != nil {
return err 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 return nil
} }

View File

@ -4,37 +4,26 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "log"
"net/http"
"net/url"
"strconv" "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/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/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "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 // ErrPolochonUnavailable is an error returned if the polochon server is not available
var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address") 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 { 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) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
@ -44,28 +33,91 @@ func DetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force
return env.RenderError(w, errors.New("invalid user type")) return env.RenderError(w, errors.New("invalid user type"))
} }
s := New(id) client, err := user.NewPapiClient()
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)
if err != nil { 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 { 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 { for _, e := range s.Episodes {
if e.Season != pEpisode.Season || e.Episode != pEpisode.Episode { // Get torrents from the db
continue err := RefreshTorrents(env, e, env.Config.ShowTorrenters)
} if err != nil {
e.PolochonURL = pEpisode.PolochonURL env.Log.Error(err)
} }
} }
env.Log.Debug("getting polochon show")
return env.RenderJSON(w, s) return env.RenderJSON(w, s)
} }
@ -90,23 +142,50 @@ func SearchShow(env *web.Env, w http.ResponseWriter, r *http.Request) error {
var shows []*polochon.Show var shows []*polochon.Show
searchers := env.Config.ShowSearchers searchers := env.Config.ShowSearchers
// Iterate on all the searchers to search for the show
for _, searcher := range searchers { for _, searcher := range searchers {
result, err := searcher.SearchShow(data.Key, env.Log) result, err := searcher.SearchShow(data.Key, env.Log)
if err != nil { if err != nil {
env.Log.Errorf("error while searching show : %s", err) env.Log.Errorf("error while searching show : %s", err)
continue continue
} }
// Add the results to the list of results
shows = append(shows, result...) shows = append(shows, result...)
} }
env.Log.Debugf("got %d shows doing search %q", len(shows), data.Key) env.Log.Debugf("got %d shows doing search %q", len(shows), data.Key)
showList := []*Show{}
for _, s := range shows { client, err := user.NewPapiClient()
show := New(s.ImdbID)
err := show.GetDetails(env, user, false)
if err != nil { if err != nil {
env.Log.Errorf("error while getting show details : %s", err) return env.RenderError(w, err)
continue }
// 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 {
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.Error(err)
} }
showList = append(showList, show) 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")) return env.RenderError(w, errors.New("invalid user type"))
} }
s := New(id) if err := backend.AddShowToWishlist(env.Database, user.ID, id, data.Season, data.Episode); err != nil {
if err := s.AddToWishlist(env, user, data.Season, data.Episode); err != nil {
env.Log.Warnf("Error while adding to db : %s", err) env.Log.Warnf("Error while adding to db : %s", err)
return env.RenderError(w, 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")) return env.RenderError(w, errors.New("invalid user type"))
} }
s := New(id) if err := backend.DeleteShowFromWishlist(env.Database, user.ID, id); err != nil {
if err := s.DeleteFromWishlist(env, user); err != nil {
env.Log.Warnf("Error while deleting to db : %s", err) env.Log.Warnf("Error while deleting to db : %s", err)
return env.RenderError(w, 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")) return env.RenderError(w, errors.New("invalid user type"))
} }
shows, err := GetWishlist(env, user) client, err := user.NewPapiClient()
if err != nil { if err != nil {
return env.RenderError(w, err) 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 // PolochonShowsHandler will returns shows from Polochon
func getPolochonShows(user *users.User) ([]*Show, error) { func PolochonShowsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) 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 {
v := auth.GetCurrentUser(r, env.Log) v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User) user, ok := v.(*users.User)
if !ok { if !ok {
return env.RenderError(w, errors.New("invalid user type")) 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 { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
var polochonConfig config.UserPolochon // Get details in DB for each shows
err = user.GetConfig("polochon", &polochonConfig) // Fetch the details if not found
if err != nil {
return env.RenderError(w, err)
}
detailer, err := pam.New(&pam.Params{
Endpoint: polochonConfig.URL,
Token: polochonConfig.Token,
})
for _, s := range shows { for _, s := range shows {
s.Detailers = []polochon.Detailer{detailer} // First try from the db
err := s.GetDetails(env, user, false) 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 { if err != nil {
env.Log.Error(err) 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 // RefreshEpisodeHandler refresh details of an episode
func RefreshEpisodeHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { 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) vars := mux.Vars(r)
id := vars["id"] 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")) return env.RenderError(w, errors.New("invalid user type"))
} }
s := New(id) client, err := user.NewPapiClient()
e, err := s.GetEpisodeDetails(env, season, episode, force)
if err != nil { if err != nil {
return env.RenderError(w, err) return env.RenderError(w, err)
} }
err = e.GetTorrents(env, force) pShow, err := client.GetShow(id)
if err != nil && customError.IsFatal(err) { 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) return env.RenderError(w, err)
} }
// Get the show from the polochon of the user // Refresh the torrents
pShow, err := getPolochonShow(user, id) err = e.RefreshTorrents(env, env.Config.ShowTorrenters)
if err != nil { if err != nil {
env.Log.Warnf("error while getting polochon episode %s S%02dE%02d : %s", id, season, episode, err) env.Log.Error(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
}
} }
return env.RenderJSON(w, e) return env.RenderJSON(w, e)

View File

@ -1,398 +1,216 @@
package shows package shows
import ( import (
"database/sql" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "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/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx" "github.com/odwrtw/papi"
"github.com/odwrtw/polochon/lib" "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 // Show represents a show
type Show struct { type Show struct {
sqly.BaseModel client *papi.Client
polochon.Show pShow *papi.Show
Episodes []*Episode `json:"episodes"` *polochon.Show
TrackedSeason *int `json:"tracked_season"` TrackedSeason *int `json:"tracked_season"`
TrackedEpisode *int `json:"tracked_episode"` TrackedEpisode *int `json:"tracked_episode"`
publicDir string
}
// 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"` BannerURL string `json:"banner_url"`
FanartURL string `json:"fanart_url"` FanartURL string `json:"fanart_url"`
PosterURL string `json:"poster_url"` PosterURL string `json:"poster_url"`
} }{
alias: (*alias)(s),
// ShowDB represents the Show in the DB BannerURL: s.GetImageURL("banner"),
type ShowDB struct { FanartURL: s.GetImageURL("fanart"),
ID string `db:"id"` PosterURL: s.GetImageURL("poster"),
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,
} }
if s.FirstAired != nil {
sDB.FirstAired = *s.FirstAired
}
return sDB
}
// FillFromDB returns a Show from a ShowDB extracted from the DB // Create Episode obj from polochon.Episodes and add them to the object to
func (s *Show) FillFromDB(sDB *ShowDB) { // marshal
s.ID = sDB.ID for _, e := range s.Show.Episodes {
s.ImdbID = sDB.ImdbID showToMarshal.Episodes = append(showToMarshal.Episodes, Episode{
s.Title = sDB.Title ShowEpisode: e,
s.Rating = sDB.Rating client: s.client,
s.Plot = sDB.Plot pShow: s.pShow,
s.TvdbID = sDB.TvdbID })
s.Year = sDB.Year }
s.FirstAired = &sDB.FirstAired return json.Marshal(showToMarshal)
s.Created = sDB.Created
s.Updated = sDB.Updated
s.TrackedSeason = sDB.TrackedSeason
s.TrackedEpisode = sDB.TrackedEpisode
} }
// New returns a new Show with a polochon ShowConfig // New returns a new Show with a polochon ShowConfig
func New(imdbID string) *Show { func New(imdbID string, publicDir string) *Show {
return &Show{ return &Show{
Show: polochon.Show{ Show: &polochon.Show{
ImdbID: imdbID, ImdbID: imdbID,
}, },
publicDir: publicDir,
} }
} }
// Get returns a show with user info like tracked // NewWithClient returns a new Show with a polochon ShowConfig
func (s *Show) Get(env *web.Env, user *users.User) error { func NewWithClient(imdbID string, client *papi.Client, pShow *papi.Show, wShow *backend.WishedShow, publicDir string) *Show {
var err error s := &Show{
var sDB ShowDB Show: &polochon.Show{
if s.ID != "" { ImdbID: imdbID,
err = env.Database.QueryRowx(getShowWithUserQueryByID, s.ID, user.ID).StructScan(&sDB) },
} else if s.ImdbID != "" { client: client,
err = env.Database.QueryRowx(getShowWithUserQueryByImdbID, s.ImdbID, user.ID).StructScan(&sDB) pShow: pShow,
} else { publicDir: publicDir,
err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID")
} }
if err != nil { if wShow != nil {
return err s.TrackedSeason = &wShow.Season
s.TrackedEpisode = &wShow.Episode
} }
// Set the poster url return s
s.PosterURL = s.GetPosterURL(env)
s.FillFromDB(&sDB)
return nil
} }
// GetDetails retrieves details for the show, first try to // GetDetails retrieves details for the show with the given detailers
// get info from db, if not exists, use polochon.Detailer func (s *Show) GetDetails(env *web.Env, detailers []polochon.Detailer) error {
// and save informations in the database for future use
func (s *Show) GetDetails(env *web.Env, user *users.User, force bool) error {
log := env.Log.WithFields(logrus.Fields{ log := env.Log.WithFields(logrus.Fields{
"imdb_id": s.ImdbID, "imdb_id": s.ImdbID,
"function": "shows.GetDetails", "function": "shows.GetDetails",
}) })
log.Debugf("getting details") var detailersName []string
for _, d := range detailers {
if len(s.Detailers) == 0 { detailersName = append(detailersName, d.Name())
s.Detailers = env.Config.ShowDetailers
} }
log.Debugf("getting details with %s", strings.Join(detailersName, ", "))
var err error s.Detailers = detailers
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
}
// GetDetail // Get the details
err = s.Show.GetDetails(env.Log) err := s.Show.GetDetails(env.Log)
if err != nil { if err != nil {
return err return err
} }
log.Debug("got details from detailers") log.Debugf("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)
return nil return nil
} }
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded // GetAndFetch retrieves details for the show with the given
func (s *Show) GetPosterURL(env *web.Env) string { // 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 // 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 ? // TODO image in the config ?
return "img/noimage.png" return "img/noimage.png"
} }
return s.imgURL(env, "poster") return s.imgURL(imgType)
} }
// downloadImages will download the show images // downloadImages will download the show images
func (s *Show) downloadImages(env *web.Env) { func (s *Show) downloadImages(env *web.Env) {
// Download the banner // 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 { if err != nil {
env.Log.Errorf("failed to dowload banner: %s", err) 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 { if err != nil {
env.Log.Errorf("failed to dowload fanart: %s", err) 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 { if err != nil {
env.Log.Errorf("failed to dowload poster: %s", err) env.Log.Errorf("failed to dowload poster: %s", err)
} }
} }
// imgFile returns the image location on disk // 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) 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 // 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) return fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType)
} }
// Upsert a show in the database // getPolochonShows returns all the Shows from the polochon of a user
func (s *Show) Upsert(db *sqlx.DB) error { func getPolochonShows(env *web.Env, user *users.User) ([]*Show, error) {
sDB := NewShowDB(s) shows := []*Show{}
var id string
r, err := db.NamedQuery(upsertShowQuery, sDB)
if err != nil {
return err
}
for r.Next() {
r.Scan(&id)
}
s.ID = id
for _, e := range s.Episodes { client, err := user.NewPapiClient()
e.ShowImdbID = s.ImdbID
err = e.Upsert(db)
if err != nil { if err != nil {
return err return shows, err
} }
// Get the polochon's shows
pshows, err := client.GetShows()
if err != nil {
return shows, err
} }
return nil
} wShows, err := backend.GetShowWishlist(env.Database, user.ID)
if err != nil {
// Delete show from database return shows, err
func (s *Show) Delete(db *sqlx.DB) error { }
r, err := db.Exec(deleteShowQueryByID, s.ID)
if err != nil { // Create Shows objects from the shows retrieved
return err for _, pShow := range pshows.List() {
} wShow, _ := wShows.IsShowInWishlist(pShow.ImdbID)
count, _ := r.RowsAffected() show := NewWithClient(pShow.ImdbID, client, pShow, wShow, env.Config.PublicDir)
if count != 1 { shows = append(shows, show)
return fmt.Errorf("Unexpected number of row deleted: %d", count) }
} return shows, nil
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)
if err != nil {
return err
}
if len(episodesDB) == 0 {
return nil
}
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)
}
}
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
} }

View File

@ -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")
}
})
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -6,11 +6,8 @@ import (
"net/http" "net/http"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "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/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/odwrtw/papi"
) )
// DownloadHandler downloads a movie via polochon // 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")) return env.RenderError(w, errors.New("invalid user type"))
} }
var polochonConfig config.UserPolochon client, err := user.NewPapiClient()
err = user.GetConfig("polochon", &polochonConfig)
if err != nil { if err != nil {
return env.RenderError(w, errors.New("problem when getting user config")) return env.RenderError(w, err)
}
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)
} }
err = client.AddTorrent(data.URL) err = client.AddTorrent(data.URL)

View File

@ -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
}

View File

@ -13,6 +13,7 @@ import (
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "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 { func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
var data struct { var data struct {
Username string `json:"username"` Username string `json:"username"`

View File

@ -2,10 +2,14 @@ package users
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/types" "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/random"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
) )
@ -23,7 +27,9 @@ const (
) )
const ( const (
// UserRole represents the user's role
UserRole = "user" UserRole = "user"
// AdminRole represents the admin's role
AdminRole = "admin" AdminRole = "admin"
) )
@ -89,6 +95,25 @@ func (u *User) NewConfig() error {
return u.RawConfig.UnmarshalJSON(b) 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 // Token represents a token
type Token struct { type Token struct {
sqly.BaseModel sqly.BaseModel
@ -201,6 +226,7 @@ func (u *User) GetName() string {
return u.Name return u.Name
} }
// HasRole checks if a user as a role
func (u *User) HasRole(role string) bool { func (u *User) HasRole(role string) bool {
if role == AdminRole && !u.Admin { if role == AdminRole && !u.Admin {
return false return false
@ -208,6 +234,7 @@ func (u *User) HasRole(role string) bool {
return true return true
} }
// IsAdmin checks if a user is admin
func (u *User) IsAdmin() bool { func (u *User) IsAdmin() bool {
return u.HasRole(AdminRole) return u.HasRole(AdminRole)
} }

View File

@ -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")
}
})
}

View File

@ -7,12 +7,14 @@ import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
polochon "github.com/odwrtw/polochon/lib"
"github.com/unrolled/render" "github.com/unrolled/render"
"github.com/urfave/negroni" "github.com/urfave/negroni"
) )
// Env describes an environement object passed to all handlers // Env describes an environement object passed to all handlers
type Env struct { type Env struct {
Backend DetailerTorrenter
Database *sqlx.DB Database *sqlx.DB
Log *logrus.Entry Log *logrus.Entry
Router *mux.Router Router *mux.Router
@ -28,6 +30,14 @@ type EnvParams struct {
Auth *auth.Authorizer Auth *auth.Authorizer
Log *logrus.Entry Log *logrus.Entry
Config *config.Config 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 // NewEnv returns a new *Env
@ -39,24 +49,29 @@ func NewEnv(p EnvParams) *Env {
Auth: p.Auth, Auth: p.Auth,
Config: p.Config, Config: p.Config,
Render: render.New(), Render: render.New(),
Backend: p.Backend,
} }
} }
// Route represents a route
type Route struct { type Route struct {
env *Env env *Env
mRoute *mux.Route mRoute *mux.Route
} }
// Name returns the name of a route
func (r *Route) Name(name string) *Route { func (r *Route) Name(name string) *Route {
r.mRoute.Name(name) r.mRoute.Name(name)
return r return r
} }
// Methods returns a list of methods for a route
func (r *Route) Methods(methods ...string) *Route { func (r *Route) Methods(methods ...string) *Route {
r.mRoute.Methods(methods...) r.mRoute.Methods(methods...)
return r return r
} }
// WithRole sets a given role for a route
func (r *Route) WithRole(role string) *Route { func (r *Route) WithRole(role string) *Route {
handler := r.mRoute.GetHandler() handler := r.mRoute.GetHandler()
newHandler := negroni.New( newHandler := negroni.New(

View File

@ -5,31 +5,19 @@ import (
"os" "os"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "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/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias" extmedias "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" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/phyber/negroni-gzip/gzip" "github.com/phyber/negroni-gzip/gzip"
"github.com/robfig/cron"
"github.com/urfave/negroni" "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() { func main() {
var cfgPath string var cfgPath string
cfgPath = os.Getenv("CONFIG_FILE") cfgPath = os.Getenv("CONFIG_FILE")
@ -48,62 +36,60 @@ func main() {
log.Panic(err) log.Panic(err)
} }
// Connect to the database
db, err := sqlx.Connect("postgres", cf.PGDSN) db, err := sqlx.Connect("postgres", cf.PGDSN)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
uBackend := &UserBackend{Database: db} backend := &backend.Backend{Database: db}
// Generate auth params
authParams := auth.Params{ authParams := auth.Params{
Backend: uBackend, Backend: backend,
Pepper: cf.Authorizer.Pepper, Pepper: cf.Authorizer.Pepper,
Cost: cf.Authorizer.Cost, Cost: cf.Authorizer.Cost,
Secret: cf.Authorizer.Secret, Secret: cf.Authorizer.Secret,
} }
authorizer := auth.New(authParams) authorizer := auth.New(authParams)
// Create web environment needed by the app
env := web.NewEnv(web.EnvParams{ env := web.NewEnv(web.EnvParams{
Database: db, Database: db,
Auth: authorizer, Auth: authorizer,
Log: log, Log: log,
Config: cf, Config: cf,
Backend: web.DetailerTorrenter{
Torrenter: backend,
Detailer: backend,
},
}) })
authMiddleware := auth.NewMiddleware(env.Auth, log) authMiddleware := auth.NewMiddleware(env.Auth, log)
env.Handle("/users/login", users.LoginPOSTHandler).Methods("POST") // Setup the routes
env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST") setupRoutes(env)
env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole).Methods("GET")
env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole).Methods("POST")
env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole).Methods("GET") // Create the cron object
env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.GetDetailsHandler).WithRole(users.UserRole).Methods("POST") c := cron.New()
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")
env.Handle("/shows/polochon", shows.FromPolochon).WithRole(users.UserRole).Methods("GET") // Refresh the library every 6h
env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler).WithRole(users.UserRole).Methods("GET") c.AddFunc("@every 6h", func() {
env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(users.UserRole).Methods("POST") env.Log.Infof("Running refresh cron!")
env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshDetailsHandler).WithRole(users.UserRole).Methods("POST") extmedias.Refresh(env)
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")
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") // Stop the cron
env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.AddToWishlist).WithRole(users.UserRole).Methods("POST") defer c.Stop()
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")
n := negroni.Classic() n := negroni.Classic()
// Middleware for authentication
n.Use(authMiddleware) n.Use(authMiddleware)
// Serve static files
n.Use(negroni.NewStatic(http.Dir(cf.PublicDir))) n.Use(negroni.NewStatic(http.Dir(cf.PublicDir)))
// Compress responses
n.Use(gzip.Gzip(gzip.DefaultCompression)) n.Use(gzip.Gzip(gzip.DefaultCompression))
n.UseHandler(env.Router) n.UseHandler(env.Router)
n.Run(":" + cf.Port) n.Run(":" + cf.Port)

View File

@ -95,7 +95,7 @@ function updateEpisode(show, fetching, data = null) {
let seasonIndex = show.seasons.map((el) => el.season).indexOf(data.season.toString()); 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); 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] = data;
} }
show.seasons[seasonIndex].episodes[episodeIndex].fetching = fetching; show.seasons[seasonIndex].episodes[episodeIndex].fetching = fetching;

51
src/routes.go Normal file
View File

@ -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")
}