Massive rewrite

https://w000t.me/a479573ac6
This commit is contained in:
Lucas BEE 2017-03-20 14:35:40 +00:00
parent 63aa470bf8
commit 8f9ec25808
38 changed files with 2315 additions and 2660 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

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

@ -13,6 +13,8 @@ type Middleware struct {
log *logrus.Entry
}
type authContextKey string
// NewMiddleware returns a new authentication middleware
func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware {
return &Middleware{
@ -34,7 +36,8 @@ func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http
m.log.Debugf("got a nil user in the context")
}
ctx := context.WithValue(r.Context(), "auth.user", user)
ctxKey := authContextKey("auth.user")
ctx := context.WithValue(r.Context(), ctxKey, user)
r = r.WithContext(ctx)
next(w, r)
@ -80,7 +83,8 @@ func (m *MiddlewareRole) ServeHTTP(w http.ResponseWriter, r *http.Request, next
func GetCurrentUser(r *http.Request, log *logrus.Entry) User {
log.Debug("getting user from context")
u := r.Context().Value("auth.user")
ctxKey := authContextKey("auth.user")
u := r.Context().Value(ctxKey)
if u == nil {
return nil
}

View File

@ -1,18 +1,10 @@
package backend
import (
"database/sql"
"errors"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx"
polochon "github.com/odwrtw/polochon/lib"
)
// Backend represents the data backend
@ -20,127 +12,13 @@ type Backend struct {
Database *sqlx.DB
}
// GetUser gets the username from the UserBackend
// Implements the UserBackend interface
func (b *Backend) GetUser(username string) (auth.User, error) {
return users.Get(b.Database, username)
}
// Name implements the Module interface
func (b *Backend) Name() string {
return "canape-backend"
}
// GetDetails implements the polochon Detailer interface
func (b *Backend) GetDetails(media interface{}, log *logrus.Entry) error {
switch t := media.(type) {
case *polochon.Show:
return b.GetShowDetails(t, log)
case *polochon.ShowEpisode:
return b.GetShowEpisodeDetails(t, log)
case *polochon.Movie:
return b.GetMovieDetails(t, log)
default:
return errors.New("invalid type")
}
}
// GetMovieDetails gets details for movies
func (b *Backend) GetMovieDetails(pmovie *polochon.Movie, log *logrus.Entry) error {
m := movies.New(pmovie.ImdbID)
if err := m.GetMovie(b.Database); err != nil {
return err
}
log.Debugf("got movie %s from backend", pmovie.ImdbID)
*pmovie = m.Movie
return nil
}
// GetShowDetails gets details for shows
func (b *Backend) GetShowDetails(pshow *polochon.Show, log *logrus.Entry) error {
s := shows.New(pshow.ImdbID)
if err := s.GetShow(b.Database); err != nil {
return err
}
log.Debugf("got show %s from backend", pshow.ImdbID)
*pshow = s.Show
return nil
}
// GetShowEpisodeDetails gets details for episodes
func (b *Backend) GetShowEpisodeDetails(pepisode *polochon.ShowEpisode, log *logrus.Entry) error {
e := shows.NewEpisode()
e.ShowImdbID = pepisode.ShowImdbID
e.Season = pepisode.Season
e.Episode = pepisode.Episode
if err := e.GetEpisode(b.Database); err != nil {
return err
}
log.Debugf("got episode S%02dE%02d %s from backend", pepisode.Season, pepisode.Episode, pepisode.ShowImdbID)
*pepisode = e.ShowEpisode
return nil
}
// GetTorrents implements the polochon Torrenter interface
func (b *Backend) GetTorrents(media interface{}, log *logrus.Entry) error {
switch t := media.(type) {
case *polochon.ShowEpisode:
return b.GetEpisodeTorrents(t, log)
case *polochon.Movie:
return b.GetMovieTorrents(t, log)
default:
return errors.New("invalid type")
}
}
// GetMovieTorrents fetch Torrents for movies
func (b *Backend) GetMovieTorrents(pmovie *polochon.Movie, log *logrus.Entry) error {
// m := movies.New(pmovie.ImdbID)
movieTorrents, err := torrents.GetMovieTorrents(b.Database, pmovie.ImdbID)
switch err {
case nil:
log.Debug("torrents found in backend")
case sql.ErrNoRows:
log.Debug("torrent not found in backend")
default:
// Unexpected error
return err
}
log.Debugf("got torrents for movie %s from backend", pmovie.ImdbID)
for _, t := range movieTorrents {
pmovie.Torrents = append(pmovie.Torrents, t.Torrent)
}
return nil
}
// GetEpisodeTorrents fetch Torrents for episodes
func (b *Backend) GetEpisodeTorrents(pepisode *polochon.ShowEpisode, log *logrus.Entry) error {
e := shows.NewEpisode()
e.ShowImdbID = pepisode.ShowImdbID
e.Season = pepisode.Season
e.Episode = pepisode.Episode
episodeTorrents, err := torrents.GetEpisodeTorrents(
b.Database,
pepisode.ShowImdbID,
pepisode.Season,
pepisode.Episode,
)
switch err {
case nil:
log.Debug("torrents found in backend")
case sql.ErrNoRows:
log.Debug("torrent not found in backend")
default:
// Unexpected error
return err
}
log.Debugf("got torrents for episode S%02dE%02d %s from backend", pepisode.Season, pepisode.Episode, pepisode.ShowImdbID)
// Add the torrents to the episode
for _, t := range episodeTorrents {
e.Torrents = append(e.Torrents, t.Torrent)
}
return nil
// GetUser gets the username from the UserBackend
// Implements the UserBackend interface
func (b *Backend) GetUser(username string) (auth.User, error) {
return users.Get(b.Database, username)
}

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,105 @@
package backend
import (
"database/sql"
"time"
"github.com/jmoiron/sqlx"
polochon "github.com/odwrtw/polochon/lib"
)
const (
upsertEpisodeTorrentQuery = `
INSERT INTO episode_torrents (imdb_id, url, source, quality, upload_user,
season, episode, seeders, leechers)
VALUES (:imdb_id, :url, :source, :quality, :upload_user, :season, :episode,
:seeders, :leechers)
ON CONFLICT (imdb_id, season, episode, quality, source)
DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality,
upload_user=:upload_user, season=:season, episode=:episode,
seeders=:seeders, leechers=:leechers
RETURNING id;`
getEpisodeTorrentQuery = `
SELECT *
FROM episode_torrents WHERE imdb_id=$1 AND season=$2 AND episode=$3;`
)
// EpisodeTorrentDB represents the EpisodeTorrent in the DB
type EpisodeTorrentDB struct {
ID string `db:"id"`
ImdbID string `db:"imdb_id"`
URL string `db:"url"`
Source string `db:"source"`
Quality string `db:"quality"`
UploadUser string `db:"upload_user"`
Season int `db:"season"`
Episode int `db:"episode"`
Seeders int `db:"seeders"`
Leechers int `db:"leechers"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// NewTorrentFromEpisodeTorrentDB returns a polochon.Torrent from an
// episodeTorrentDB
func NewTorrentFromEpisodeTorrentDB(eDB *EpisodeTorrentDB) *polochon.Torrent {
q, _ := polochon.StringToQuality(eDB.Quality)
return &polochon.Torrent{
Quality: *q,
URL: eDB.URL,
Seeders: eDB.Seeders,
Leechers: eDB.Leechers,
Source: eDB.Source,
UploadUser: eDB.UploadUser,
}
}
// NewEpisodeTorrentDB returns an EpisodeTorrentDB ready to be put in DB from a
// polochon.Torrent
func NewEpisodeTorrentDB(t *polochon.Torrent, imdbID string, season, episode int) *EpisodeTorrentDB {
return &EpisodeTorrentDB{
ImdbID: imdbID,
Season: season,
Episode: episode,
URL: t.URL,
Source: t.Source,
Quality: string(t.Quality),
UploadUser: t.UploadUser,
Seeders: t.Seeders,
Leechers: t.Leechers,
}
}
// GetEpisodeTorrents returns show episodes torrents from database
func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]polochon.Torrent, error) {
var torrentsDB = []*EpisodeTorrentDB{}
err := db.Select(&torrentsDB, getEpisodeTorrentQuery, imdbID, season, episode)
if err != nil {
return nil, err
}
if len(torrentsDB) == 0 {
return nil, sql.ErrNoRows
}
var torrents []polochon.Torrent
for _, torrentDB := range torrentsDB {
torrent := NewTorrentFromEpisodeTorrentDB(torrentDB)
torrents = append(torrents, *torrent)
}
return torrents, nil
}
// UpsertEpisodeTorrent upserts an episode torrent from a polochon torrent
func UpsertEpisodeTorrent(db *sqlx.DB, torrent *polochon.Torrent, imdbID string, season, episode int) error {
// Create the EpisodeTorrent ready to be put in db
eDB := NewEpisodeTorrentDB(torrent, imdbID, season, episode)
_, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,141 @@
package backend
import (
"time"
"github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx"
polochon "github.com/odwrtw/polochon/lib"
)
const (
upsertEpisodeQuery = `
INSERT INTO episodes (show_imdb_id, show_tvdb_id, title, season, episode, tvdb_id, aired, plot, runtime, rating, imdb_id)
VALUES (:show_imdb_id, :show_tvdb_id, :title, :season, :episode, :tvdb_id, :aired, :plot, :runtime, :rating, :imdb_id)
ON CONFLICT (show_imdb_id, season, episode)
DO UPDATE
SET show_imdb_id=:show_imdb_id, show_tvdb_id=:show_tvdb_id, title=:title,
season=:season, episode=:episode, tvdb_id=:tvdb_id, aired=:aired,
plot=:plot, runtime=:runtime, rating=:rating, imdb_id=:imdb_id
RETURNING id;`
getEpisodesQuery = `
SELECT *
FROM episodes WHERE show_imdb_id=$1;`
getEpisodeQuery = `
SELECT *
FROM episodes WHERE show_imdb_id=$1 AND season=$2 AND episode=$3;`
)
// EpisodeDB represents the Episode in the DB
type EpisodeDB struct {
ID string `db:"id"`
TvdbID int `db:"tvdb_id"`
ImdbID string `db:"imdb_id"`
ShowImdbID string `db:"show_imdb_id"`
ShowTvdbID int `db:"show_tvdb_id"`
Season int `db:"season"`
Episode int `db:"episode"`
Title string `db:"title"`
Rating float32 `db:"rating"`
Plot string `db:"plot"`
Thumb string `db:"thumb"`
Runtime int `db:"runtime"`
Aired string `db:"aired"`
ReleaseGroup string `db:"release_group"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// NewEpisodeFromPolochon returns an EpisodeDB from a polochon ShowEpisode
func NewEpisodeFromPolochon(e *polochon.ShowEpisode) *EpisodeDB {
return &EpisodeDB{
TvdbID: e.TvdbID,
ImdbID: e.EpisodeImdbID,
ShowImdbID: e.ShowImdbID,
ShowTvdbID: e.ShowTvdbID,
Season: e.Season,
Episode: e.Episode,
Title: e.Title,
Rating: e.Rating,
Plot: e.Plot,
Thumb: e.Thumb,
Runtime: e.Runtime,
Aired: e.Aired,
ReleaseGroup: e.ReleaseGroup,
}
}
// NewEpisodeFromDB returns a new polochon ShowEpisode from an EpisodeDB
func NewEpisodeFromDB(eDB *EpisodeDB) *polochon.ShowEpisode {
pEpisode := polochon.ShowEpisode{}
FillEpisodeFromDB(eDB, &pEpisode)
return &pEpisode
}
// FillEpisodeFromDB fills a ShowEpisode from an EpisodeDB
func FillEpisodeFromDB(eDB *EpisodeDB, pEpisode *polochon.ShowEpisode) {
pEpisode.TvdbID = eDB.TvdbID
pEpisode.EpisodeImdbID = eDB.ImdbID
pEpisode.ShowImdbID = eDB.ShowImdbID
pEpisode.ShowTvdbID = eDB.ShowTvdbID
pEpisode.Season = eDB.Season
pEpisode.Episode = eDB.Episode
pEpisode.Title = eDB.Title
pEpisode.Rating = eDB.Rating
pEpisode.Plot = eDB.Plot
pEpisode.Thumb = eDB.Thumb
pEpisode.Runtime = eDB.Runtime
pEpisode.Aired = eDB.Aired
}
// GetEpisode gets an episode and fills the polochon episode
func GetEpisode(db *sqlx.DB, pEpisode *polochon.ShowEpisode) error {
var episodeDB EpisodeDB
err := db.QueryRowx(getEpisodeQuery, pEpisode.ShowImdbID, pEpisode.Season, pEpisode.Episode).StructScan(&episodeDB)
if err != nil {
return err
}
FillEpisodeFromDB(&episodeDB, pEpisode)
return nil
}
// GetEpisodes gets show's episodes and fills the polochon show
func GetEpisodes(db *sqlx.DB, pShow *polochon.Show, log *logrus.Entry) error {
// Get the episodes
var episodesDB = []*EpisodeDB{}
err := db.Select(&episodesDB, getEpisodesQuery, pShow.ImdbID)
if err != nil {
return err
}
if len(episodesDB) == 0 {
log.Debug("got no episodes")
return nil
}
log.Debugf("got %d episodes", len(episodesDB))
for _, episodeDB := range episodesDB {
episode := NewEpisodeFromDB(episodeDB)
pShow.Episodes = append(pShow.Episodes, episode)
}
return nil
}
// UpsertEpisode upserts the episode
func UpsertEpisode(db *sqlx.DB, showEpisode *polochon.ShowEpisode) error {
e := NewEpisodeFromPolochon(showEpisode)
var id string
r, err := db.NamedQuery(upsertEpisodeQuery, e)
if err != nil {
return err
}
for r.Next() {
r.Scan(&id)
}
e.ID = id
return nil
}

View File

@ -0,0 +1,47 @@
package backend
import (
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
const (
upsertExternalMediaQuery = `
INSERT INTO external_medias (type, source, category, ids)
VALUES (:type, :source, :category, :ids)
ON CONFLICT (type, source, category)
DO UPDATE SET type=:type, source=:source, category=:category, ids=:ids
RETURNING id;`
getExternalMediaQuery = `SELECT * FROM external_medias WHERE type=$1 AND source=$2 AND category=$3 LIMIT 1;`
)
// Media represents an external media
type Media struct {
sqly.BaseModel
Type string `db:"type"`
Source string `db:"source"`
Category string `db:"category"`
IDs pq.StringArray `db:"ids"`
}
// Explore will return an array of Medias from the DB
func Explore(db *sqlx.DB, mtype, msrc, mcat string) (*Media, error) {
m := &Media{}
if err := db.QueryRowx(getExternalMediaQuery, mtype, msrc, mcat).StructScan(m); err != nil {
return nil, err
}
return m, nil
}
// Upsert adds or updates the Media in the database
func (m *Media) Upsert(db *sqlx.DB) error {
_, err := db.NamedQuery(upsertExternalMediaQuery, m)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,101 @@
package backend
import (
"database/sql"
"time"
"github.com/jmoiron/sqlx"
polochon "github.com/odwrtw/polochon/lib"
)
const (
upsertMovieTorrentQuery = `
INSERT INTO movie_torrents (imdb_id, url, source, quality, upload_user,
seeders, leechers)
VALUES (:imdb_id, :url, :source, :quality, :upload_user, :seeders,
:leechers)
ON CONFLICT (imdb_id, quality, source)
DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality,
upload_user=:upload_user, seeders=:seeders, leechers=:leechers
RETURNING id;`
getMovieTorrentQueryByImdbID = `
SELECT *
FROM movie_torrents WHERE imdb_id=$1;`
)
// MovieTorrentDB represents the MovieTorrent in the DB
type MovieTorrentDB struct {
ID string `db:"id"`
ImdbID string `db:"imdb_id"`
URL string `db:"url"`
Source string `db:"source"`
Quality string `db:"quality"`
UploadUser string `db:"upload_user"`
Seeders int `db:"seeders"`
Leechers int `db:"leechers"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// NewTorrentFromMovieTorrentDB creates a new polochon.Torrent from a
// MovieTorrentDB
func NewTorrentFromMovieTorrentDB(mDB *MovieTorrentDB) *polochon.Torrent {
q, _ := polochon.StringToQuality(mDB.Quality)
return &polochon.Torrent{
Quality: *q,
URL: mDB.URL,
Seeders: mDB.Seeders,
Leechers: mDB.Leechers,
Source: mDB.Source,
UploadUser: mDB.UploadUser,
}
}
// NewMovieTorrentDB returns a MovieTorrent ready to be put in DB from a
// Torrent
func NewMovieTorrentDB(t *polochon.Torrent, imdbID string) MovieTorrentDB {
return MovieTorrentDB{
ImdbID: imdbID,
URL: t.URL,
Source: t.Source,
Quality: string(t.Quality),
UploadUser: t.UploadUser,
Seeders: t.Seeders,
Leechers: t.Leechers,
}
}
// GetMovieTorrents returns polochon.Torrents from the database
func GetMovieTorrents(db *sqlx.DB, imdbID string) ([]polochon.Torrent, error) {
var torrentsDB = []*MovieTorrentDB{}
// Get the torrents from the DB
err := db.Select(&torrentsDB, getMovieTorrentQueryByImdbID, imdbID)
if err != nil {
return nil, err
}
if len(torrentsDB) == 0 {
return nil, sql.ErrNoRows
}
// Create polochon Torrents from the MovieTorrentDB
var torrents []polochon.Torrent
for _, torrentDB := range torrentsDB {
torrent := NewTorrentFromMovieTorrentDB(torrentDB)
torrents = append(torrents, *torrent)
}
return torrents, nil
}
// UpsertMovieTorrent adds or updates MovieTorrent in db
func UpsertMovieTorrent(db *sqlx.DB, t *polochon.Torrent, imdbID string) error {
mDB := NewMovieTorrentDB(t, imdbID)
_, err := db.NamedQuery(upsertMovieTorrentQuery, mDB)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,125 @@
package backend
import (
"fmt"
"github.com/jmoiron/sqlx"
)
const (
upsertMovieWishlistQuery = `
INSERT INTO movies_tracked (imdb_id, user_id)
VALUES ($1, $2)
ON CONFLICT (imdb_id, user_id)
DO UPDATE
SET imdb_id=$1, user_id=$2;`
isMovieWishlistedQueryByUserID = `
SELECT
movies.imdb_id
FROM movies INNER JOIN movies_tracked
ON
movies.imdb_id=movies_tracked.imdb_id
AND
movies_tracked.user_id=$2
WHERE movies.imdb_id=$1;`
getMovieWishlistQueryByUserID = `
SELECT
movies.imdb_id
FROM movies INNER JOIN movies_tracked
ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$1;`
deleteMovieWishlistedQueryByID = `DELETE FROM movies_tracked WHERE imdb_id=$1 AND user_id=$2;`
)
// MovieWishlist represents the list of movies wishlisted by a user
type MovieWishlist struct {
movies map[string]struct{}
}
// NewMovieWishlist returns a new MovieWishlist
func NewMovieWishlist() *MovieWishlist {
return &MovieWishlist{
movies: map[string]struct{}{},
}
}
// add adds movie to the MovieWishlist
func (w *MovieWishlist) add(imdbID string) {
w.movies[imdbID] = struct{}{}
}
// List return a slice of imdbID wishlisted
func (w *MovieWishlist) List() []string {
movies := make([]string, len(w.movies))
i := 0
for imdbID := range w.movies {
movies[i] = imdbID
i++
}
return movies
}
// GetMovieWishlist returns a MovieWishlist obejct for a user
func GetMovieWishlist(db *sqlx.DB, userID string) (*MovieWishlist, error) {
var movies = []string{}
// Get the list of movies
err := db.Select(&movies, getMovieWishlistQueryByUserID, userID)
if err != nil {
return nil, err
}
// Create a new MovieWishlist
wishlist := NewMovieWishlist()
for _, imdbID := range movies {
// add the movie to the wishlist object
wishlist.add(imdbID)
}
return wishlist, nil
}
// IsMovieInWishlist returns true if the movie is in the wishlist
func (w *MovieWishlist) IsMovieInWishlist(imdbID string) bool {
_, ok := w.movies[imdbID]
return ok
}
// IsMovieWishlisted returns true if the movie is wishlisted
func IsMovieWishlisted(db *sqlx.DB, userID, imdbID string) (bool, error) {
var movies = []string{}
// Check if the movie is wishlisted by the user
err := db.Select(&movies, isMovieWishlistedQueryByUserID, imdbID, userID)
if err != nil {
return false, err
}
if len(movies) > 0 {
return true, nil
}
return false, nil
}
// AddMovieToWishlist Adds a movie to a user's wishlist in DB
func AddMovieToWishlist(db *sqlx.DB, userID, imdbID string) error {
_, err := db.Exec(upsertMovieWishlistQuery, imdbID, userID)
if err != nil {
return err
}
return nil
}
// DeleteMovieFromWishlist deletes a movie from a user's wishlist in DB
func DeleteMovieFromWishlist(db *sqlx.DB, userID, imdbID string) error {
r, err := db.Exec(deleteMovieWishlistedQueryByID, imdbID, userID)
if err != nil {
return err
}
count, _ := r.RowsAffected()
if count != 1 {
return fmt.Errorf("Unexpected number of row deleted: %d", count)
}
return nil
}

View File

@ -0,0 +1,119 @@
package backend
import (
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
polochon "github.com/odwrtw/polochon/lib"
)
const (
upsertMovieQuery = `
INSERT INTO movies (imdb_id, title, rating, votes, plot, tmdb_id, year,
genres, original_title, runtime, sort_title, tagline)
VALUES (:imdb_id, :title, :rating, :votes, :plot, :tmdb_id, :year, :genres,
:original_title, :runtime, :sort_title, :tagline)
ON CONFLICT (imdb_id)
DO UPDATE
SET imdb_id=:imdb_id, title=:title, rating=:rating, votes=:votes,
plot=:plot, tmdb_id=:tmdb_id, year=:year, genres=:genres,
original_title=:original_title, runtime=:runtime, sort_title=:sort_title,
tagline=:tagline
RETURNING id;`
getMovieQueryByImdbID = `
SELECT *
FROM movies
WHERE imdb_id=$1;`
)
// MovieDB represents the Movie in the DB
type MovieDB struct {
ID string `db:"id"`
ImdbID string `db:"imdb_id"`
TmdbID int `db:"tmdb_id"`
UserID *string `db:"user_id"`
Title string `db:"title"`
OriginalTitle string `db:"original_title"`
SortTitle string `db:"sort_title"`
Rating float32 `db:"rating"`
Votes int `db:"votes"`
Plot string `db:"plot"`
Year int `db:"year"`
Runtime int `db:"runtime"`
Tagline string `db:"tagline"`
Genres pq.StringArray `db:"genres"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// GetMovie fills show details of a polochon.Movie
func GetMovie(db *sqlx.DB, pMovie *polochon.Movie) error {
var mDB MovieDB
var err error
if pMovie.ImdbID != "" {
// Get the data from the DB
err = db.QueryRowx(getMovieQueryByImdbID, pMovie.ImdbID).StructScan(&mDB)
} else {
err = fmt.Errorf("Can't get movie details, you have to specify an ImdbID")
}
if err != nil {
return err
}
// Fills the polochon.Movie from the MovieDB
FillFromDB(&mDB, pMovie)
return nil
}
// FillFromDB fills a Movie from a MovieDB extracted from the DB
func FillFromDB(mDB *MovieDB, pMovie *polochon.Movie) {
pMovie.ImdbID = mDB.ImdbID
pMovie.Title = mDB.Title
pMovie.Rating = mDB.Rating
pMovie.Votes = mDB.Votes
pMovie.Plot = mDB.Plot
pMovie.TmdbID = mDB.TmdbID
pMovie.Year = mDB.Year
pMovie.OriginalTitle = mDB.OriginalTitle
pMovie.Runtime = mDB.Runtime
pMovie.Genres = mDB.Genres
pMovie.SortTitle = mDB.SortTitle
pMovie.Tagline = mDB.Tagline
}
// UpsertMovie upsert a polochon Movie in the database
func UpsertMovie(db *sqlx.DB, pMovie *polochon.Movie) error {
mDB := NewMovieDB(pMovie)
_, err := db.NamedQuery(upsertMovieQuery, mDB)
if err != nil {
return err
}
return nil
}
// NewMovieDB returns a Movie ready to be put in DB from a
// polochon Movie
func NewMovieDB(m *polochon.Movie) MovieDB {
genres := []string{}
if m.Genres != nil {
genres = m.Genres
}
return MovieDB{
ImdbID: m.ImdbID,
Title: m.Title,
Rating: m.Rating,
Votes: m.Votes,
Plot: m.Plot,
TmdbID: m.TmdbID,
Year: m.Year,
OriginalTitle: m.OriginalTitle,
Runtime: m.Runtime,
SortTitle: m.SortTitle,
Tagline: m.Tagline,
Genres: genres,
}
}

View File

@ -0,0 +1,140 @@
package backend
import (
"fmt"
"github.com/jmoiron/sqlx"
)
const (
upsertShowWishlistQuery = `
INSERT INTO shows_tracked (imdb_id, user_id, season, episode)
VALUES ($1, $2, $3, $4)
ON CONFLICT (imdb_id, user_id)
DO UPDATE
SET imdb_id=$1, user_id=$2, season=$3, episode=$4;`
isShowWishlistedQueryByUserID = `
SELECT
shows.imdb_id
FROM shows INNER JOIN shows_tracked
ON
shows.imdb_id=shows_tracked.imdb_id
AND
shows_tracked.user_id=$2
WHERE shows.imdb_id=$1;`
getShowWishlistQueryByUserID = `
SELECT
shows.imdb_id
FROM shows INNER JOIN shows_tracked
ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$1;`
deleteShowWishlistedQueryByID = `DELETE FROM shows_tracked WHERE imdb_id=$1 AND user_id=$2;`
)
// ShowWishlist represents the show wishlist of a user
type ShowWishlist struct {
shows map[string]*WishedShow
}
// WishedShow represents a wished show of a user
type WishedShow struct {
ImdbID string `db:"imdb_id"`
Season int
Episode int
}
// NewShowWishlist returns a new ShowWishlist
func NewShowWishlist() *ShowWishlist {
return &ShowWishlist{
shows: map[string]*WishedShow{},
}
}
// add adds a show to the ShowWishlist object
func (w *ShowWishlist) add(imdbID string, season, episode int) {
w.shows[imdbID] = &WishedShow{
ImdbID: imdbID,
Season: season,
Episode: episode,
}
}
// List return a slice of WishedShow
func (w *ShowWishlist) List() []*WishedShow {
shows := make([]*WishedShow, len(w.shows))
i := 0
for _, s := range w.shows {
shows[i] = &WishedShow{
ImdbID: s.ImdbID,
Season: s.Season,
Episode: s.Episode,
}
i++
}
return shows
}
// GetShowWishlist returns a ShowWishlist for a user
func GetShowWishlist(db *sqlx.DB, userID string) (*ShowWishlist, error) {
var wishedShows = []*WishedShow{}
// Get the wishlisted shows
err := db.Select(&wishedShows, getShowWishlistQueryByUserID, userID)
if err != nil {
return nil, err
}
// Create the new object
wishlist := NewShowWishlist()
for _, wishedShow := range wishedShows {
// Add the show to the wishlist
wishlist.add(wishedShow.ImdbID, wishedShow.Season, wishedShow.Episode)
}
return wishlist, nil
}
// IsShowInWishlist returns true and the WishedShow if the show is in the
// wishlist
func (w *ShowWishlist) IsShowInWishlist(imdbID string) (*WishedShow, bool) {
show, ok := w.shows[imdbID]
return show, ok
}
// IsShowWishlisted returns true and the WishedShow if the show is wishlisted
func IsShowWishlisted(db *sqlx.DB, userID, imdbID string) (*WishedShow, error) {
var wishedShows = []*WishedShow{}
err := db.Select(&wishedShows, isShowWishlistedQueryByUserID, imdbID, userID)
if err != nil {
return nil, err
}
if len(wishedShows) != 1 {
return nil, nil
}
return wishedShows[0], nil
}
// AddShowToWishlist Adds a show to a user's wishlist
func AddShowToWishlist(db *sqlx.DB, userID, imdbID string, season, episode int) error {
_, err := db.Exec(upsertShowWishlistQuery, imdbID, userID, season, episode)
if err != nil {
return err
}
return nil
}
// DeleteShowFromWishlist deletes a show from a user's wishlist
func DeleteShowFromWishlist(db *sqlx.DB, userID, imdbID string) error {
r, err := db.Exec(deleteShowWishlistedQueryByID, imdbID, userID)
if err != nil {
return err
}
count, _ := r.RowsAffected()
if count != 1 {
return fmt.Errorf("Unexpected number of row deleted: %d", count)
}
return nil
}

View File

@ -0,0 +1,104 @@
package backend
import (
"fmt"
"time"
"github.com/jmoiron/sqlx"
polochon "github.com/odwrtw/polochon/lib"
)
const (
upsertShowQuery = `
INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired)
VALUES (:imdb_id, :title, :rating, :plot, :tvdb_id, :year, :first_aired)
ON CONFLICT (imdb_id)
DO UPDATE
SET imdb_id=:imdb_id, title=:title, rating=:rating, plot=:plot,
tvdb_id=:tvdb_id, year=:year, first_aired=:first_aired
RETURNING id;`
getShowQueryByImdbID = `
SELECT *
FROM shows WHERE imdb_id=$1;`
)
// ShowDB represents the Show in the DB
type ShowDB struct {
ID string `db:"id"`
ImdbID string `db:"imdb_id"`
TvdbID int `db:"tvdb_id"`
TrackedSeason *int `db:"season"`
TrackedEpisode *int `db:"episode"`
Title string `db:"title"`
Rating float32 `db:"rating"`
Plot string `db:"plot"`
Year int `db:"year"`
FirstAired time.Time `db:"first_aired"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// UpsertShow a show in the database
func UpsertShow(db *sqlx.DB, s *polochon.Show) error {
sDB := NewShowFromPolochon(s)
// Upsert the show
_, err := db.NamedQuery(upsertShowQuery, sDB)
if err != nil {
return err
}
for _, e := range s.Episodes {
// Upsert its episodes
err = UpsertEpisode(db, e)
if err != nil {
return err
}
}
return nil
}
// NewShowFromPolochon returns an ShowDB from a polochon Show
func NewShowFromPolochon(s *polochon.Show) *ShowDB {
sDB := ShowDB{
ImdbID: s.ImdbID,
TvdbID: s.TvdbID,
Title: s.Title,
Rating: s.Rating,
Plot: s.Plot,
Year: s.Year,
}
if s.FirstAired != nil {
sDB.FirstAired = *s.FirstAired
}
return &sDB
}
// GetShow fills a show from the DB
func GetShow(db *sqlx.DB, pShow *polochon.Show) error {
var err error
var sDB ShowDB
if pShow.ImdbID != "" {
err = db.QueryRowx(getShowQueryByImdbID, pShow.ImdbID).StructScan(&sDB)
} else {
err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID")
}
if err != nil {
return err
}
// Fill the show from the ShowDB
FillShowFromDB(&sDB, pShow)
return nil
}
// FillShowFromDB returns a Show from a ShowDB extracted from the DB
func FillShowFromDB(sDB *ShowDB, pShow *polochon.Show) {
pShow.ImdbID = sDB.ImdbID
pShow.Title = sDB.Title
pShow.Rating = sDB.Rating
pShow.Plot = sDB.Plot
pShow.TvdbID = sDB.TvdbID
pShow.Year = sDB.Year
pShow.FirstAired = &sDB.FirstAired
}

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

View File

@ -1,58 +1,189 @@
package extmedias
import (
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
"fmt"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus"
polochon "github.com/odwrtw/polochon/lib"
)
const (
upsertExternalMediaQuery = `
INSERT INTO external_medias (type, source, category, ids)
VALUES (:type, :source, :category, :ids)
ON CONFLICT (type, source, category)
DO UPDATE SET type=:type, source=:source, category=:category, ids=:ids
RETURNING id;`
// NewExplorer returns a polochon.Explorer from the list of Explorers in the config
func NewExplorer(env *web.Env, mediaType, name string) (polochon.Explorer, error) {
var explorers []polochon.Explorer
if mediaType == "movie" {
explorers = env.Config.MovieExplorers
} else {
explorers = env.Config.ShowExplorers
}
deleteExternalMediaQuery = `DELETE FROM external_medias WHERE id=:id;`
getExternalMediaQuery = `SELECT * FROM external_medias WHERE type=$1 AND source=$2 AND category=$3 LIMIT 1;`
)
// Media represents an external media
type Media struct {
sqly.BaseModel
Type string `db:"type"`
Source string `db:"source"`
Category string `db:"category"`
IDs pq.StringArray `db:"ids"`
for _, e := range explorers {
// Check the name of the explorer
if e.Name() == name {
return e, nil
}
}
return nil, fmt.Errorf("no such explorer %s", name)
}
// Upsert adds or updates or adds the Media in the database
func (m *Media) Upsert(db *sqlx.DB) error {
var id string
r, err := db.NamedQuery(upsertExternalMediaQuery, m)
// GetMediaIDs will get some media IDs
func GetMediaIDs(env *web.Env, mediaType string, source string, category string) ([]string, error) {
log := env.Log.WithFields(logrus.Fields{
"mediaType": mediaType,
"source": source,
"category": category,
"function": "extmedias.GetMediaIds",
})
// Get the explorer that we need
explorer, err := NewExplorer(env, mediaType, source)
if err != nil {
return err
}
for r.Next() {
r.Scan(&id)
}
m.ID = id
return nil
}
// Delete the media from database or raise an error
func (m *Media) Delete(db *sqlx.DB) error {
_, err := db.NamedExec(deleteExternalMediaQuery, m)
return err
}
// Get gets a media
func Get(db *sqlx.DB, mtype, msrc, mcat string) (*Media, error) {
m := &Media{}
if err := db.QueryRowx(getExternalMediaQuery, mtype, msrc, mcat).StructScan(m); err != nil {
return nil, err
}
return m, nil
var ids []string
if mediaType == "movie" {
// Explore new movies
medias, err := explorer.GetMovieList(category, log)
if err != nil {
return nil, err
}
// Add them in the list of ids
for _, media := range medias {
ids = append(ids, media.ImdbID)
}
} else {
// Explore new shows
medias, err := explorer.GetShowList(category, log)
if err != nil {
return nil, err
}
// Add them in the list of ids
for _, media := range medias {
ids = append(ids, media.ImdbID)
}
}
log.Debugf("got %d medias from %s", len(ids), source)
// Upserts the new Media entry
media := &backend.Media{
Type: mediaType,
Source: source,
Category: category,
IDs: ids,
}
err = media.Upsert(env.Database)
if err != nil {
return nil, err
}
log.Debug("medias updated in database")
return ids, nil
}
// RefreshShows refresh explored shows
// Call all explorers and Refresh
// Retrieve a list of all the ids to be refreshed
// Refresh all the shows + Torrents for all episodes
func RefreshShows(env *web.Env) {
showMap := make(map[string]struct{})
// Iterate over all show explorers
for _, showExplorer := range env.Config.ShowExplorers {
availableOptions := showExplorer.AvailableShowOptions()
// Iterate over all show explorer options
for _, option := range availableOptions {
env.Log.Infof("refreshing shows %s %s", showExplorer.Name(), option)
// Get the new explored shows
showIds, err := GetMediaIDs(env, "show", showExplorer.Name(), option)
if err != nil {
env.Log.Warnf("error while refreshing %s/%s : %s", showExplorer.Name(), option, err)
continue
}
// Add them in a map ( to get every shows only once )
for _, id := range showIds {
showMap[id] = struct{}{}
}
}
}
env.Log.Info("========================================================")
// Iterate over the map of shows to refresh them
for id := range showMap {
show := shows.New(id)
// Refresh the shows
err := show.Refresh(env, env.Config.ShowDetailers)
if err != nil {
env.Log.Warnf("error while refreshing show %s : %s", id, err)
}
// Iterate over all episodes to refresh their torrents
for _, e := range show.Episodes {
// Refresh the torrents
err := shows.RefreshTorrents(env, e, env.Config.ShowTorrenters)
if err != nil {
env.Log.Error(err)
}
}
}
}
// RefreshMovies refresh explored Movies
// Call all explorers and Refresh
// Retrieve a list of all the ids to be refreshed
// Refresh all the movies + Torrents
func RefreshMovies(env *web.Env) {
movieMap := make(map[string]struct{})
// Iterate over all movie explorers
for _, movieExplorer := range env.Config.MovieExplorers {
availableOptions := movieExplorer.AvailableMovieOptions()
// Iterate over all movie explorer options
for _, option := range availableOptions {
env.Log.Infof("refreshing movies %s %s", movieExplorer.Name(), option)
// Get the new explored movies
movieIds, err := GetMediaIDs(env, "movie", movieExplorer.Name(), option)
if err != nil {
env.Log.Warnf("error while refreshing %s/%s : %s", movieExplorer.Name(), option, err)
continue
}
// Add them in a map ( to get every movies only once )
for _, id := range movieIds {
movieMap[id] = struct{}{}
}
}
}
env.Log.Info("========================================================")
// Iterate over the map of movies to refresh them
for id := range movieMap {
movie := movies.New(id, nil, nil, false, env.Config.PublicDir)
// Refresh the movie
err := movie.Refresh(env, env.Config.MovieDetailers)
if err != nil {
env.Log.Warnf("error while refreshing movie %s : %s", id, err)
}
// Refresh its torrents
err = movie.RefreshTorrents(env, env.Config.MovieTorrenters)
if err != nil {
env.Log.Warnf("error while refreshing movie torrents %s : %s", id, err)
}
}
}
// Refresh refreshes explore new shows and refresh them
func Refresh(env *web.Env) {
env.Log.Debugf("refreshing shows")
RefreshShows(env)
env.Log.Debugf("done refreshing shows")
env.Log.Debugf("refreshing movies")
RefreshMovies(env)
env.Log.Debugf("done refreshing movies")
}

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
import (
"database/sql"
"errors"
"fmt"
"net/http"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus"
polochon "github.com/odwrtw/polochon/lib"
eztvExplorer "github.com/odwrtw/polochon/modules/eztv"
traktExplorer "github.com/odwrtw/polochon/modules/trakttv"
ytsExplorer "github.com/odwrtw/polochon/modules/yts"
)
// GetMediaIDs will get some media IDs
func GetMediaIDs(env *web.Env, mediaType string, source string, category string, force bool) ([]string, error) {
// RefreshHandler refresh the explored movies
func RefreshHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
log := env.Log.WithFields(logrus.Fields{
"mediaType": mediaType,
"source": source,
"category": category,
"function": "extmedias.GetMediaIds",
"function": "extmedias.RefreshHandler",
})
log.Debugf("refreshing shows and movies")
Refresh(env)
log.Debugf("done refreshing shows and movies")
return nil
}
var err error
media, err := Get(env.Database, mediaType, source, category)
switch err {
case nil:
log.Debugf("%s medias found in database", mediaType)
if !force {
log.Debug("returning medias from db")
return media.IDs, nil
}
case sql.ErrNoRows:
log.Debugf("%s medias not found in database", mediaType)
default:
// Unexpected error
return nil, err
}
// RefreshMoviesHandler refresh the explored movies
func RefreshMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
log := env.Log.WithFields(logrus.Fields{
"function": "extmedias.RefreshMoviesHandler",
})
log.Debugf("refreshing movies")
RefreshMovies(env)
log.Debugf("done refreshing movies")
return nil
}
explorer, err := NewExplorer(env, source)
if err != nil {
return nil, err
}
var ids []string
if mediaType == "movie" {
medias, err := explorer.GetMovieList(polochon.ExploreByRate, log)
if err != nil {
return nil, err
}
for _, media := range medias {
ids = append(ids, media.ImdbID)
}
} else {
medias, err := explorer.GetShowList(polochon.ExploreByRate, log)
if err != nil {
return nil, err
}
for i, media := range medias {
if i > 5 {
break
}
ids = append(ids, media.ImdbID)
}
}
log.Debugf("got %d medias from %s", len(ids), source)
media = &Media{
Type: mediaType,
Source: source,
Category: category,
IDs: ids,
}
err = media.Upsert(env.Database)
if err != nil {
return nil, err
}
log.Debug("medias updated in database")
return ids, nil
// RefreshShowsHandler refresh the explored shows
func RefreshShowsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
log := env.Log.WithFields(logrus.Fields{
"function": "extmedias.RefreshShowsHandler",
})
log.Debugf("refreshing shows")
RefreshShows(env)
log.Debugf("done refreshing shows")
return nil
}
// GetMovies get some movies
func GetMovies(env *web.Env, user *users.User, source string, category string, force bool) ([]*movies.Movie, error) {
movieIds, err := GetMediaIDs(env, "movie", source, category, force)
func GetMovies(env *web.Env, user *users.User, source string, category string) ([]*movies.Movie, error) {
log := env.Log.WithFields(logrus.Fields{
"source": source,
"category": category,
"function": "extmedias.GetMovies",
})
log.Debugf("getting movies")
media, err := backend.Explore(env.Database, "movie", source, category)
if err != nil {
return nil, err
}
// Get the URLs from polochon, we don't really care if it fails for now
var urls map[string]string
urls, err = movies.GetPolochonMoviesURLs(user)
// Create a papi client
client, err := user.NewPapiClient()
if err != nil {
env.Log.Errorf("error while getting polochon movies url: %s", err)
return nil, err
}
// Get the user's polochon movies
pMovies, err := client.GetMovies()
if err != nil {
return nil, err
}
// Get the user's wishlisted movies
moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID)
if err != nil {
return nil, err
}
movieList := []*movies.Movie{}
for _, id := range movieIds {
movie := movies.New(id)
err := movie.GetDetails(env, user, force)
// Fill all the movies infos from the list of IDs
for _, id := range media.IDs {
pMovie, _ := pMovies.Has(id)
movie := movies.New(id, client, pMovie, moviesWishlist.IsMovieInWishlist(id), env.Config.PublicDir)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}
// Then with the default detailers
after := env.Config.MovieDetailers
err := movie.GetAndFetch(env, before, after)
if err != nil {
env.Log.Errorf("error while getting movie details : %s", err)
continue
}
err = movie.GetTorrents(env, force)
// Look only for torrents in db
torrenters := []polochon.Torrenter{env.Backend.Torrenter}
err = movie.GetTorrents(env, torrenters)
if err != nil {
env.Log.Errorf("error while getting movie torrents : %s", err)
continue
}
if urls != nil {
if url, ok := urls[id]; ok {
movie.PolochonURL = url
}
}
movieList = append(movieList, movie)
}
return movieList, nil
@ -132,31 +109,59 @@ func GetMovies(env *web.Env, user *users.User, source string, category string, f
// GetShows get some shows
func GetShows(env *web.Env, user *users.User, source string, category string, force bool) ([]*shows.Show, error) {
showIds, err := GetMediaIDs(env, "show", source, category, force)
log := env.Log.WithFields(logrus.Fields{
"source": source,
"category": category,
"function": "extmedias.GetShows",
})
log.Debugf("getting shows")
// Get the user's polochon config
media, err := backend.Explore(env.Database, "show", source, category)
if err != nil {
return nil, err
}
// Create a papi client
client, err := user.NewPapiClient()
if err != nil {
return nil, err
}
// Get the polochon's shows
pShows, err := client.GetShows()
if err != nil {
return nil, err
}
// Get the user's wishlisted shows
wShows, err := backend.GetShowWishlist(env.Database, user.ID)
if err != nil {
return nil, err
}
showList := []*shows.Show{}
for _, id := range showIds {
show := shows.New(id)
err := show.GetDetails(env, user, force)
// Fill all the shows infos from the list of IDs
for _, id := range media.IDs {
pShow, _ := pShows.Has(id)
wShow, _ := wShows.IsShowInWishlist(id)
show := shows.NewWithClient(id, client, pShow, wShow, env.Config.PublicDir)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}
// Then with the default detailers
after := env.Config.ShowDetailers
err := show.GetAndFetch(env, before, after)
if err != nil {
env.Log.Errorf("error while getting show details : %s", err)
continue
}
err = show.GetTorrents(env, force)
if err != nil {
env.Log.Errorf("error while getting show torrents : %s", err)
continue
}
showList = append(showList, show)
}
return showList, nil
}
// Explore will explore some movies
func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// ExploreMovies will explore some movies from the db
func ExploreMovies(env *web.Env, w http.ResponseWriter, r *http.Request) error {
err := r.ParseForm()
if err != nil {
return err
@ -181,7 +186,7 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
}
// Get the medias without trying to refresh them
movies, err := GetMovies(env, user, source, category, false)
movies, err := GetMovies(env, user, source, category)
if err != nil {
return env.RenderError(w, err)
}
@ -222,94 +227,3 @@ func ExploreShows(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderJSON(w, shows)
}
// MediaSources represents the implemented media sources
var MediaSources = []string{
"trakttv",
"yts",
}
// ShowMediaSources represents the implemented media sources for shows
var ShowMediaSources = []string{
"eztv",
}
// Refresh will refresh the movie list
func Refresh(env *web.Env, w http.ResponseWriter, r *http.Request) error {
env.Log.Debugf("refreshing infos ...")
source := r.FormValue("source")
if source == "" {
source = "yts"
}
category := r.FormValue("category")
if category == "" {
category = "popular"
}
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return env.RenderError(w, fmt.Errorf("invalid user type"))
}
// We'll refresh the medias for each sources
for _, source := range MediaSources {
env.Log.Debugf("refreshing %s", source)
// GetMedias and refresh them
_, err := GetMovies(env, user, source, category, true)
if err != nil {
return env.RenderError(w, err)
}
}
return env.RenderJSON(w, map[string]string{"message": "Refresh is done"})
}
// RefreshShows will refresh the movie list
func RefreshShows(env *web.Env, w http.ResponseWriter, r *http.Request) error {
env.Log.Debugf("refreshing shows ...")
source := r.FormValue("source")
if source == "" {
source = "eztv"
}
category := r.FormValue("category")
if category == "" {
category = "popular"
}
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return env.RenderError(w, fmt.Errorf("invalid user type"))
}
// We'll refresh the medias for each sources
for _, source := range ShowMediaSources {
env.Log.Debugf("refreshing %s", source)
// GetMedias and refresh them
_, err := GetShows(env, user, source, category, true)
if err != nil {
return env.RenderError(w, err)
}
}
return env.RenderJSON(w, map[string]string{"message": "Refresh is done"})
}
// NewExplorer returns a polochon.Explorer
func NewExplorer(env *web.Env, source string) (polochon.Explorer, error) {
switch source {
case "trakttv":
return traktExplorer.NewExplorer(&traktExplorer.Params{
ClientID: env.Config.TraktTVClientID,
})
case "yts":
return ytsExplorer.NewExplorer()
case "eztv":
return eztvExplorer.NewExplorer()
default:
return nil, fmt.Errorf("unknown explorer")
}
}

View File

@ -4,9 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"log"
"net/http"
"net/url"
"github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
@ -15,84 +14,28 @@ import (
"github.com/odwrtw/polochon/modules/pam"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
)
// ErrPolochonUnavailable is an error returned if the polochon server is not available
var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address")
func getPolochonMovies(user *users.User) ([]*Movie, error) {
movies := []*Movie{}
var polochonConfig config.UserPolochon
err := user.GetConfig("polochon", &polochonConfig)
if err != nil {
return movies, err
}
client, err := papi.New(polochonConfig.URL)
if err != nil {
return movies, err
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
}
pmovies, err := client.GetMovies()
if err != nil {
// Catch network error for accessing specified polochon address
if uerr, ok := err.(*url.Error); ok {
if nerr, ok := uerr.Err.(*net.OpError); ok {
if nerr.Op == "dial" {
return movies, ErrPolochonUnavailable
}
}
}
return movies, err
}
for _, pmovie := range pmovies {
movie := New(pmovie.ImdbID)
movie.PolochonURL, err = client.DownloadURL(&papi.Movie{ImdbID: movie.ImdbID})
if err != nil {
return nil, err
}
movies = append(movies, movie)
}
return movies, nil
}
// GetPolochonMoviesURLs returns the polochon urls associated with an imdb id
func GetPolochonMoviesURLs(user *users.User) (map[string]string, error) {
movies, err := getPolochonMovies(user)
if err != nil {
return nil, err
}
urls := make(map[string]string, len(movies))
for _, movie := range movies {
urls[movie.ImdbID] = movie.PolochonURL
}
return urls, nil
}
// FromPolochon will returns movies from Polochon
func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// PolochonMoviesHandler will returns movies from Polochon
func PolochonMoviesHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// Get the user from the request
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return fmt.Errorf("invalid user type")
return env.RenderError(w, fmt.Errorf("invalid user type"))
}
movies, err := getPolochonMovies(user)
// Get the polochon movies of the user
movies, err := getPolochonMovies(user, env)
if err != nil {
return env.RenderError(w, err)
}
// Create a new polochon Detailer to get infos about the movies we just got
var polochonConfig config.UserPolochon
err = user.GetConfig("polochon", &polochonConfig)
if err != nil {
@ -104,9 +47,13 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
Token: polochonConfig.Token,
})
// Get details with the polochon Detailer for each polochon.Movies we have
for _, m := range movies {
m.Detailers = []polochon.Detailer{detailer}
err := m.GetDetails(env, user, false)
// First try from the db
first := []polochon.Detailer{env.Backend.Detailer}
// Then try from the polochon detailer
detailers := []polochon.Detailer{detailer}
err := m.GetAndFetch(env, first, detailers)
if err != nil {
env.Log.Error(err)
}
@ -115,20 +62,47 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderJSON(w, movies)
}
// GetDetailsHandler retrieves details for a movie
func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// RefreshMovieHandler refreshes details for a movie
func RefreshMovieHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["id"]
// Get the user
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return fmt.Errorf("invalid user type")
}
m := New(id)
if err := m.GetDetails(env, user, true); err != nil {
return err
// Create a new papi client
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
// Get the polochon movie
pMovie, err := client.GetMovie(id)
if err != nil && err != papi.ErrResourceNotFound {
log.Println("Error getting movie ", err)
}
// Check if the movie is wishlisted
isWishlisted, err := backend.IsMovieWishlisted(env.Database, user.ID, id)
if err != nil {
log.Println("Error getting wishlisted movie ", err)
}
// Create a new movie
m := New(id, client, pMovie, isWishlisted, env.Config.PublicDir)
// Refresh the movie's infos
if err := m.Refresh(env, env.Config.MovieDetailers); err != nil {
env.Log.Error(err)
}
// Refresh the movie's torrents
if err := m.RefreshTorrents(env, env.Config.MovieTorrenters); err != nil {
env.Log.Error(err)
}
return env.RenderJSON(w, m)
@ -154,8 +128,27 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, errors.New("invalid user"))
}
// Create a new papi client
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
// Get the user's polochon movies
pMovies, err := client.GetMovies()
if err != nil {
return env.RenderError(w, err)
}
// Get the user's wishlisted movies
moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID)
if err != nil {
return env.RenderError(w, err)
}
var movies []*polochon.Movie
searchers := env.Config.MovieSearchers
// Search for the movie with all the Searchers
for _, searcher := range searchers {
result, err := searcher.SearchMovie(data.Key, env.Log)
if err != nil {
@ -165,42 +158,45 @@ func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error {
movies = append(movies, result...)
}
// Get the URLs from polochon, we don't really care if it fails for now
var urls map[string]string
urls, err = GetPolochonMoviesURLs(user)
if err != nil {
env.Log.Errorf("error while getting polochon movies url: %s", err)
}
env.Log.Debugf("got %d movies doing search %q", len(movies), data.Key)
movieList := []*Movie{}
// For each movie found, fill the details
for _, m := range movies {
movie := New(m.ImdbID)
err := movie.GetDetails(env, user, false)
pMovie, _ := pMovies.Has(m.ImdbID)
movie := New(
m.ImdbID,
client,
pMovie,
moviesWishlist.IsMovieInWishlist(m.ImdbID),
env.Config.PublicDir,
)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}
// Then with the default detailers
after := env.Config.MovieDetailers
err := movie.GetAndFetch(env, before, after)
if err != nil {
env.Log.Errorf("error while getting movie details : %s", err)
continue
}
err = movie.GetTorrents(env, false)
// Look only for torrents in db
torrenters := []polochon.Torrenter{env.Backend.Torrenter}
err = movie.GetTorrents(env, torrenters)
if err != nil {
env.Log.Errorf("error while getting movie torrents : %s", err)
continue
}
if urls != nil {
if url, ok := urls[m.ImdbID]; ok {
movie.PolochonURL = url
}
}
movieList = append(movieList, movie)
}
return env.RenderJSON(w, movieList)
}
// DeleteHandler deletes the movie from polochon
func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// PolochonDeleteHandler deletes the movie from polochon
func PolochonDeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["id"]
@ -210,27 +206,20 @@ func DeleteHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
})
log.Debugf("deleting movie")
// Get the user
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return env.RenderError(w, errors.New("invalid user type"))
}
var polochonConfig config.UserPolochon
err := user.GetConfig("polochon", &polochonConfig)
// Create a new papi client
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
client, err := papi.New(polochonConfig.URL)
if err != nil {
return env.RenderError(w, err)
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
}
// Delete the movie
return client.Delete(&papi.Movie{ImdbID: id})
}
@ -245,8 +234,7 @@ func AddToWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, errors.New("invalid user type"))
}
m := New(id)
if err := m.AddToWishlist(env, user); err != nil {
if err := backend.AddMovieToWishlist(env.Database, user.ID, id); err != nil {
return env.RenderError(w, err)
}
@ -264,8 +252,7 @@ func DeleteFromWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) er
return env.RenderError(w, errors.New("invalid user type"))
}
m := New(id)
if err := m.DeleteFromWishlist(env, user); err != nil {
if err := backend.DeleteMovieFromWishlist(env.Database, user.ID, id); err != nil {
return env.RenderError(w, err)
}
@ -280,10 +267,55 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
return env.RenderError(w, errors.New("invalid user type"))
}
movies, err := GetWishlist(env, user)
// Create a new papi client
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
return env.RenderJSON(w, movies)
// Get the user's polochon movies
pMovies, err := client.GetMovies()
if err != nil {
return env.RenderError(w, err)
}
// Get the user's wishlisted movies
moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID)
if err != nil {
return env.RenderError(w, err)
}
movieList := []*Movie{}
// For each movie found, fill the details
for _, imdbID := range moviesWishlist.List() {
pMovie, _ := pMovies.Has(imdbID)
movie := New(
imdbID,
client,
pMovie,
moviesWishlist.IsMovieInWishlist(imdbID),
env.Config.PublicDir,
)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}
// Then with the default detailers
after := env.Config.MovieDetailers
err := movie.GetAndFetch(env, before, after)
if err != nil {
env.Log.Errorf("error while getting movie details : %s", err)
continue
}
// Look only for torrents in db
torrenters := []polochon.Torrenter{env.Backend.Torrenter}
err = movie.GetTorrents(env, torrenters)
if err != nil {
env.Log.Errorf("error while getting movie torrents : %s", err)
continue
}
movieList = append(movieList, movie)
}
return env.RenderJSON(w, movieList)
}

View File

@ -2,331 +2,198 @@ package movies
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/odwrtw/papi"
"github.com/odwrtw/polochon/lib"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
)
const (
upsertMovieQuery = `
INSERT INTO movies (imdb_id, title, rating, votes, plot, tmdb_id, year,
genres, original_title, runtime, sort_title, tagline)
VALUES (:imdb_id, :title, :rating, :votes, :plot, :tmdb_id, :year, :genres,
:original_title, :runtime, :sort_title, :tagline)
ON CONFLICT (imdb_id)
DO UPDATE
SET imdb_id=:imdb_id, title=:title, rating=:rating, votes=:votes,
plot=:plot, tmdb_id=:tmdb_id, year=:year, genres=:genres,
original_title=:original_title, runtime=:runtime, sort_title=:sort_title,
tagline=:tagline
RETURNING id;`
getUserMovieQueryByImdbID = `
SELECT
movies.id,
movies.title,
movies.imdb_id,
movies.tmdb_id,
movies.votes,
movies.rating,
movies.plot,
movies.year,
movies.original_title,
movies.runtime,
movies.genres,
movies.sort_title,
movies.tagline,
movies.created_at,
movies.updated_at,
movies_tracked.user_id
FROM movies LEFT JOIN movies_tracked
ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$2
WHERE movies.imdb_id=$1;`
getUserMovieQueryByID = `
SELECT
movies.id,
movies.title,
movies.imdb_id,
movies.tmdb_id,
movies.votes,
movies.rating,
movies.plot,
movies.year,
movies.original_title,
movies.runtime,
movies.genres,
movies.sort_title,
movies.tagline,
movies.created_at,
movies.updated_at,
movies_tracked.user_id
FROM movies LEFT JOIN movies_tracked
ON movies.imdb_id=movies_tracked.imdb_id AND movies_tracked.user_id=$2
WHERE movies.id=$1;`
getMovieQueryByImdbID = `
SELECT *
FROM movies
WHERE imdb_id=$1;`
getMovieQueryByID = `
SELECT *
FROM movies
WHERE movies.id=$1;`
deleteMovieQuery = `DELETE FROM movies WHERE id=$1;`
)
// MovieDB represents the Movie in the DB
type MovieDB struct {
ID string `db:"id"`
ImdbID string `db:"imdb_id"`
TmdbID int `db:"tmdb_id"`
UserID *string `db:"user_id"`
Title string `db:"title"`
OriginalTitle string `db:"original_title"`
SortTitle string `db:"sort_title"`
Rating float32 `db:"rating"`
Votes int `db:"votes"`
Plot string `db:"plot"`
Year int `db:"year"`
Runtime int `db:"runtime"`
Tagline string `db:"tagline"`
Genres pq.StringArray `db:"genres"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// NewMovieDB returns a Movie ready to be put in DB from a
// Movie
func NewMovieDB(m *Movie) MovieDB {
genres := []string{}
if m.Genres != nil {
genres = m.Genres
}
return MovieDB{
ID: m.ID,
ImdbID: m.ImdbID,
Title: m.Title,
Rating: m.Rating,
Votes: m.Votes,
Plot: m.Plot,
TmdbID: m.TmdbID,
Year: m.Year,
OriginalTitle: m.OriginalTitle,
Runtime: m.Runtime,
SortTitle: m.SortTitle,
Tagline: m.Tagline,
Genres: genres,
Created: m.Created,
Updated: m.Updated,
}
}
// FillFromDB returns a Movie from a MovieDB extracted from the DB
func (m *Movie) FillFromDB(mDB *MovieDB) {
m.Created = mDB.Created
m.Updated = mDB.Updated
m.ID = mDB.ID
m.ImdbID = mDB.ImdbID
m.Title = mDB.Title
m.Rating = mDB.Rating
m.Votes = mDB.Votes
m.Plot = mDB.Plot
m.TmdbID = mDB.TmdbID
m.Year = mDB.Year
m.OriginalTitle = mDB.OriginalTitle
m.Runtime = mDB.Runtime
m.Genres = mDB.Genres
m.SortTitle = mDB.SortTitle
m.Tagline = mDB.Tagline
if mDB.UserID != nil {
m.Wishlisted = true
}
}
// Movie represents a movie
type Movie struct {
sqly.BaseModel
polochon.Movie
Wishlisted bool `json:"wishlisted"`
PolochonURL string `json:"polochon_url"`
PosterURL string `json:"poster_url"`
*polochon.Movie
client *papi.Client
pMovie *papi.Movie
publicDir string
Wishlisted bool `json:"wishlisted"`
}
// New returns a new Movie with an ImDB id
func New(imdbID string) *Movie {
// MarshalJSON implements the Marshal interface
func (m *Movie) MarshalJSON() ([]byte, error) {
type Alias Movie
var downloadURL string
// If the episode is present, fill the downloadURL
if m.pMovie != nil {
downloadURL, _ = m.client.DownloadURL(m.pMovie)
}
// Marshal the movie with its polochon_url
movieToMarshal := &struct {
*Alias
PolochonURL string `json:"polochon_url"`
PosterURL string `json:"poster_url"`
}{
Alias: (*Alias)(m),
PolochonURL: downloadURL,
PosterURL: m.PosterURL(),
}
return json.Marshal(movieToMarshal)
}
// New returns a new Movie with all the needed infos
func New(imdbID string, client *papi.Client, pMovie *papi.Movie, isWishlisted bool, publicDir string) *Movie {
return &Movie{
Movie: polochon.Movie{
client: client,
pMovie: pMovie,
publicDir: publicDir,
Wishlisted: isWishlisted,
Movie: &polochon.Movie{
ImdbID: imdbID,
},
}
}
// Get returns show details in database from id or imdbid or an error
func (m *Movie) Get(env *web.Env, user *users.User) error {
var mDB MovieDB
var err error
if m.ID != "" {
err = env.Database.QueryRowx(getUserMovieQueryByID, m.ID, user.ID).StructScan(&mDB)
} else if m.ImdbID != "" {
err = env.Database.QueryRowx(getUserMovieQueryByImdbID, m.ImdbID, user.ID).StructScan(&mDB)
} else {
err = fmt.Errorf("Can't get movie details, you have to specify an ID or ImdbID")
}
if err != nil {
return err
}
// Set the poster url
m.PosterURL = m.GetPosterURL(env)
m.FillFromDB(&mDB)
return nil
}
// GetMovie returns show details in database from id or imdbid or an error
func (m *Movie) GetMovie(db *sqlx.DB) error {
var mDB MovieDB
var err error
if m.ID != "" {
err = db.QueryRowx(getMovieQueryByID, m.ID).StructScan(&mDB)
} else if m.ImdbID != "" {
err = db.QueryRowx(getMovieQueryByImdbID, m.ImdbID).StructScan(&mDB)
} else {
err = fmt.Errorf("Can't get movie details, you have to specify an ID or ImdbID")
}
if err != nil {
return err
}
// Set the poster url
// m.PosterURL = m.GetPosterURL(env)
m.FillFromDB(&mDB)
return nil
}
// if not exists, use polochon.Detailer and save informations in the database
// for future use
//
// If force is used, the detailer will be used even if the movie is found in
// database
func (m *Movie) GetDetails(env *web.Env, user *users.User, force bool) error {
// GetDetails retrieves details for the movie with the given detailers
func (m *Movie) GetDetails(env *web.Env, detailers []polochon.Detailer) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.GetDetails",
})
log.Debugf("getting details")
if len(m.Detailers) == 0 {
m.Detailers = env.Config.MovieDetailers
}
var err error
err = m.Get(env, user)
switch err {
case nil:
log.Debug("movie found in database")
case sql.ErrNoRows:
log.Debug("movie not found in database")
default:
// Unexpected error
return err
}
// If force is not specified, don't go further
if !force {
// Will return ErrNoRows if the movie wasn't found
return err
}
m.Detailers = detailers
// GetDetail
err = m.Movie.GetDetails(env.Log)
err := m.Movie.GetDetails(env.Log)
if err != nil {
return err
}
log.Debug("got details from detailers")
err = m.Upsert(env.Database)
if err != nil {
log.Debug("error while doing db func")
return err
}
log.Debug("movie added in database")
// Download poster
err = web.Download(m.Thumb, m.imgFile(env))
if err != nil {
return err
}
log.Debug("poster downloaded")
// Set the poster url
m.PosterURL = m.GetPosterURL(env)
return nil
}
// GetTorrents retrieves torrents for the movie, first try to get info from db,
// if not exists, use polochon.Torrenter and save informations in the database
// for future use
//
// If force is used, the torrenter will be used even if the torrent is found in
// GetAndFetch retrieves details for the movie with the given
// detailers 'before'
// If found, return
// If not, retrives details with the detailers 'after' and update them in
// database
func (m *Movie) GetTorrents(env *web.Env, force bool) error {
func (m *Movie) GetAndFetch(env *web.Env, before []polochon.Detailer, after []polochon.Detailer) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.GetAndFetch",
})
// Try to get details with the first batch of Detailers
err := m.GetDetails(env, before)
if err == nil {
log.Debug("movie found in first try")
return nil
}
log.Debugf("movie not found in database: %s", err)
// If not found, try the second batch and upsert
return m.Refresh(env, after)
}
// Refresh retrieves details for the movie with the given detailers
// and update them in database
func (m *Movie) Refresh(env *web.Env, detailers []polochon.Detailer) error {
// Refresh
err := m.GetDetails(env, detailers)
if err != nil {
return err
}
// Download poster
err = web.Download(m.Thumb, m.imgFile())
if err != nil {
return err
}
env.Log.Debug("poster downloaded")
// If found, update in database
return backend.UpsertMovie(env.Database, m.Movie)
}
// GetTorrents retrieves torrents for the movie with the given torrenters
func (m *Movie) GetTorrents(env *web.Env, torrenters []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.GetTorrents",
})
log.Debugf("getting torrents")
if len(m.Torrenters) == 0 {
m.Torrenters = env.Config.MovieTorrenters
}
m.Torrenters = torrenters
movieTorrents, err := torrents.GetMovieTorrents(env.Database, m.ImdbID)
switch err {
case nil:
log.Debug("torrents found in database")
case sql.ErrNoRows:
log.Debug("torrent not found in database")
default:
// Unexpected error
return err
}
if !force {
log.Debugf("returning %d torrents from db", len(movieTorrents))
// Add the torrents to the movie
for _, t := range movieTorrents {
m.Torrents = append(m.Torrents, t.Torrent)
}
return nil
}
err = m.Movie.GetTorrents(env.Log)
err := m.Movie.GetTorrents(env.Log)
if err != nil {
return err
}
log.Debugf("got %d torrents from torrenters", len(m.Movie.Torrents))
return nil
}
// GetAndFetchTorrents retrieves torrents for the movie with the given
// torrenters 'before'
// If found, return
// If not, retrives torrents with the torrenters 'after' and update them in
// database
func (m *Movie) GetAndFetchTorrents(env *web.Env, before []polochon.Torrenter, after []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.GetAndFetchTorrents",
})
// Try to get torrents with the first batch of Torrenters
err := m.GetTorrents(env, before)
switch err {
case nil:
log.Debug("movie torrent's found in first try")
return nil
case sql.ErrNoRows:
log.Debug("movie's torrents not found in database")
default:
// Unexpected error
return err
}
// If not found, try the second batch and upsert
return m.RefreshTorrents(env, after)
}
// RefreshTorrents retrieves torrents for the movie with the given torrenters
// and update them in database
func (m *Movie) RefreshTorrents(env *web.Env, torrenters []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": m.ImdbID,
"function": "movies.RefreshTorrents",
})
// Get torrents with de torrenters
err := m.GetTorrents(env, torrenters)
if err != nil {
return err
}
log.Debugf("got %d torrents from torrenters", len(m.Movie.Torrents))
// Update them in database
for _, t := range m.Movie.Torrents {
torrent := torrents.NewMovie(m.ImdbID, t)
err = torrent.Upsert(env.Database)
err = backend.UpsertMovieTorrent(env.Database, &t, m.ImdbID)
if err != nil {
log.Error("error while adding torrent", err)
continue
@ -336,51 +203,59 @@ func (m *Movie) GetTorrents(env *web.Env, force bool) error {
return nil
}
// Upsert a movie in the database
func (m *Movie) Upsert(db *sqlx.DB) error {
mDB := NewMovieDB(m)
var id string
r, err := db.NamedQuery(upsertMovieQuery, mDB)
if err != nil {
return err
}
for r.Next() {
r.Scan(&id)
}
m.ID = id
return nil
}
// Delete movie from database
func (m *Movie) Delete(db *sqlx.DB) error {
r, err := db.Exec(deleteMovieQuery, m.ID)
if err != nil {
return err
}
count, _ := r.RowsAffected()
if count != 1 {
return fmt.Errorf("Unexpected number of row deleted: %d", count)
}
return nil
}
// imgURL returns the default image url
func (m *Movie) imgURL(env *web.Env) string {
func (m *Movie) imgURL() string {
return fmt.Sprintf("img/movies/%s.jpg", m.ImdbID)
}
// imgFile returns the image location on disk
func (m *Movie) imgFile(env *web.Env) string {
return filepath.Join(env.Config.PublicDir, m.imgURL(env))
func (m *Movie) imgFile() string {
return filepath.Join(m.publicDir, m.imgURL())
}
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded
func (m *Movie) GetPosterURL(env *web.Env) string {
// PosterURL returns the image URL or the default image if the poster is not yet downloaded
func (m *Movie) PosterURL() string {
// Check if the movie image exists
if _, err := os.Stat(m.imgFile(env)); os.IsNotExist(err) {
if _, err := os.Stat(m.imgFile()); os.IsNotExist(err) {
// TODO image in the config ?
return "img/noimage.png"
}
return m.imgURL(env)
return m.imgURL()
}
// getPolochonMovies returns an array of the user's polochon movies
func getPolochonMovies(user *users.User, env *web.Env) ([]*Movie, error) {
movies := []*Movie{}
// Create a papi client
client, err := user.NewPapiClient()
if err != nil {
return movies, err
}
// Retrieve the user's polochon movies
pmovies, err := client.GetMovies()
if err != nil {
return movies, err
}
// Get the user's wishlisted movies
moviesWishlist, err := backend.GetMovieWishlist(env.Database, user.ID)
if err != nil {
return movies, err
}
// Create Movies objects from the movies retrieved
for _, pmovie := range pmovies.List() {
movie := New(
pmovie.ImdbID,
client,
pmovie,
moviesWishlist.IsMovieInWishlist(pmovie.ImdbID),
env.Config.PublicDir,
)
movies = append(movies, movie)
}
return movies, nil
}

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())
// String returns n random strings
func String(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!

View File

@ -1,181 +1,140 @@
package shows
import (
"database/sql"
"time"
"encoding/json"
"github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx"
"github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
)
const (
upsertEpisodeQuery = `
INSERT INTO episodes (show_imdb_id, show_tvdb_id, title, season, episode, tvdb_id, aired, plot, runtime, rating, imdb_id)
VALUES (:show_imdb_id, :show_tvdb_id, :title, :season, :episode, :tvdb_id, :aired, :plot, :runtime, :rating, :imdb_id)
ON CONFLICT (show_imdb_id, season, episode)
DO UPDATE
SET show_imdb_id=:show_imdb_id, show_tvdb_id=:show_tvdb_id, title=:title,
season=:season, episode=:episode, tvdb_id=:tvdb_id, aired=:aired,
plot=:plot, runtime=:runtime, rating=:rating, imdb_id=:imdb_id
RETURNING id;`
getEpisodesQuery = `
SELECT *
FROM episodes WHERE show_imdb_id=$1;`
getEpisodeQuery = `
SELECT *
FROM episodes WHERE show_imdb_id=$1 AND season=$2 AND episode=$3;`
)
// Episode represents an episode
type Episode struct {
sqly.BaseModel
polochon.ShowEpisode
PolochonURL string `json:"polochon_url"`
client *papi.Client
pShow *papi.Show
*polochon.ShowEpisode
}
// EpisodeDB represents the Episode in the DB
type EpisodeDB struct {
ID string `db:"id"`
TvdbID int `db:"tvdb_id"`
ImdbID string `db:"imdb_id"`
ShowImdbID string `db:"show_imdb_id"`
ShowTvdbID int `db:"show_tvdb_id"`
Season int `db:"season"`
Episode int `db:"episode"`
Title string `db:"title"`
Rating float32 `db:"rating"`
Plot string `db:"plot"`
Thumb string `db:"thumb"`
Runtime int `db:"runtime"`
Aired string `db:"aired"`
ReleaseGroup string `db:"release_group"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
// MarshalJSON implements the Marshal interface
func (e *Episode) MarshalJSON() ([]byte, error) {
type alias Episode
var downloadURL string
// If the episode is present, fill the downloadURL
if e.pShow != nil && e.pShow.HasEpisode(e.Season, e.Episode) {
downloadURL, _ = e.client.DownloadURL(
&papi.Episode{
ShowImdbID: e.ShowImdbID,
Episode: e.Episode,
Season: e.Season,
},
)
}
// Marshal the episode with its polochon_url
episodeToMarshal := &struct {
*alias
PolochonURL string `json:"polochon_url"`
}{
alias: (*alias)(e),
PolochonURL: downloadURL,
}
return json.Marshal(episodeToMarshal)
}
// NewEpisode returns an Episode
func NewEpisode() *Episode {
return &Episode{}
}
// NewEpisodeDB returns an Episode ready to be put in DB from an
// Episode
func NewEpisodeDB(e *Episode) EpisodeDB {
return EpisodeDB{
ID: e.ID,
TvdbID: e.TvdbID,
ImdbID: e.EpisodeImdbID,
ShowImdbID: e.ShowImdbID,
ShowTvdbID: e.ShowTvdbID,
Season: e.Season,
Episode: e.Episode,
Title: e.Title,
Rating: e.Rating,
Plot: e.Plot,
Thumb: e.Thumb,
Runtime: e.Runtime,
Aired: e.Aired,
ReleaseGroup: e.ReleaseGroup,
Created: e.Created,
Updated: e.Updated,
func NewEpisode(client *papi.Client, pShow *papi.Show, imdbID string, season, episode int) *Episode {
return &Episode{
client: client,
pShow: pShow,
ShowEpisode: &polochon.ShowEpisode{
ShowImdbID: imdbID,
Season: season,
Episode: episode,
},
}
}
// FillFromDB returns a Show from a ShowDB extracted from the DB
func (e *Episode) FillFromDB(eDB *EpisodeDB) {
e.ID = eDB.ID
e.TvdbID = eDB.TvdbID
e.EpisodeImdbID = eDB.ImdbID
e.ShowImdbID = eDB.ShowImdbID
e.ShowTvdbID = eDB.ShowTvdbID
e.Season = eDB.Season
e.Episode = eDB.Episode
e.Title = eDB.Title
e.Rating = eDB.Rating
e.Plot = eDB.Plot
e.Thumb = eDB.Thumb
e.Runtime = eDB.Runtime
e.Aired = eDB.Aired
e.Created = eDB.Created
e.Updated = eDB.Updated
}
// Upsert episode to the database
func (e *Episode) Upsert(db *sqlx.DB) error {
eDB := NewEpisodeDB(e)
var id string
r, err := db.NamedQuery(upsertEpisodeQuery, eDB)
if err != nil {
return err
}
for r.Next() {
r.Scan(&id)
}
e.ID = id
return nil
}
// GetTorrents retrieves torrents for the show, first try to get info from db,
// if not exists, use polochon.Torrenter and save informations in the database
// for future use
//
// If force is used, the torrenter will be used even if the torrent is found in
// GetAndFetchTorrents retrieves torrents for the episode with the given
// torrenters 'before'
// If found, return
// If not, retrives torrents with the torrenters 'after' and update them in
// database
func (e *Episode) GetTorrents(env *web.Env, force bool) error {
func (e *Episode) GetAndFetchTorrents(env *web.Env, before []polochon.Torrenter, after []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": e.ShowEpisode.ShowImdbID,
"season": e.ShowEpisode.Season,
"episode": e.ShowEpisode.Episode,
"function": "shows.GetTorrents",
"imdb_id": e.ShowImdbID,
"season": e.Season,
"episode": e.Episode,
"function": "episodes.GetAndFetchTorrents",
})
// Try to get details with the first batch of Detailers
err := e.GetTorrents(env, before)
if err == nil {
log.Debug("episode torrents found in first try")
return nil
}
log.Debugf("episode torrent not found in database: %s", err)
// If not found, try the second batch and upsert
return e.RefreshTorrents(env, after)
}
// GetTorrents retrieves torrents for the episode with the given torrenters
func (e *Episode) GetTorrents(env *web.Env, torrenters []polochon.Torrenter) error {
return GetTorrents(env, e.ShowEpisode, torrenters)
}
// GetTorrents retrieves torrents for the episode with the given torrenters
func GetTorrents(env *web.Env, showEpisode *polochon.ShowEpisode, torrenters []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": showEpisode.ShowImdbID,
"season": showEpisode.Season,
"episode": showEpisode.Episode,
"function": "episodes.GetTorrents",
})
log.Debugf("getting torrents")
if len(e.Torrenters) == 0 {
e.Torrenters = env.Config.ShowTorrenters
}
showEpisode.Torrenters = torrenters
episodeTorrents, err := torrents.GetEpisodeTorrents(
env.Database,
e.ShowEpisode.ShowImdbID,
e.ShowEpisode.Season,
e.ShowEpisode.Episode,
)
switch err {
case nil:
log.Debug("torrents found in database")
case sql.ErrNoRows:
log.Debug("torrent not found in database")
default:
// Unexpected error
return err
}
// If force is not specified, don't go further
if !force {
log.Debugf("returning %d torrents from db", len(episodeTorrents))
// Add the torrents to the episode
for _, t := range episodeTorrents {
e.Torrents = append(e.Torrents, t.Torrent)
}
// Will return ErrNoRows if the torrent wasn't found
return err
}
err = e.ShowEpisode.GetTorrents(env.Log)
err := showEpisode.GetTorrents(env.Log)
if err != nil {
return err
}
log.Debugf("got %d torrents from torrenters", len(e.ShowEpisode.Torrents))
log.Debugf("got %d torrents from torrenters", len(showEpisode.Torrents))
for _, t := range e.ShowEpisode.Torrents {
torrent := torrents.NewEpisodeFromPolochon(e.ShowEpisode, t)
err = torrent.Upsert(env.Database)
return nil
}
// RefreshTorrents refresh the episode torrents
func (e *Episode) RefreshTorrents(env *web.Env, torrenters []polochon.Torrenter) error {
return RefreshTorrents(env, e.ShowEpisode, torrenters)
}
// RefreshTorrents refresh the episode torrents
func RefreshTorrents(env *web.Env, showEpisode *polochon.ShowEpisode, torrenters []polochon.Torrenter) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": showEpisode.ShowImdbID,
"season": showEpisode.Season,
"episode": showEpisode.Episode,
"function": "episodes.RefreshTorrents",
})
log.Debugf("refreshing torrents")
// Refresh
err := GetTorrents(env, showEpisode, torrenters)
if err != nil {
return err
}
// Upsert all the torrents we got
for _, t := range showEpisode.Torrents {
err = backend.UpsertEpisodeTorrent(env.Database, &t, showEpisode.ShowImdbID, showEpisode.Season, showEpisode.Episode)
if err != nil {
log.Errorf("error while adding torrent : %s", err)
continue
@ -185,20 +144,35 @@ func (e *Episode) GetTorrents(env *web.Env, force bool) error {
return nil
}
// Get returns an episode
func (e *Episode) Get(env *web.Env) error {
return e.GetEpisode(env.Database)
}
// GetEpisode returns an episode
func (e *Episode) GetEpisode(db *sqlx.DB) error {
var episodeDB EpisodeDB
err := db.QueryRowx(getEpisodeQuery, e.ShowImdbID, e.Season, e.Episode).StructScan(&episodeDB)
// Refresh retrieves details for the episode with the given detailers
// and update them in database
func (e *Episode) Refresh(env *web.Env, detailers []polochon.Detailer) error {
// Refresh
err := e.GetEpisodeDetails(env, detailers)
if err != nil {
return err
}
e.FillFromDB(&episodeDB)
return backend.UpsertEpisode(env.Database, e.ShowEpisode)
}
// GetEpisodeDetails retrieves details for the episode with the given detailers
func (e *Episode) GetEpisodeDetails(env *web.Env, detailers []polochon.Detailer) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": e.ShowImdbID,
"function": "episodes.GetDetails",
})
log.Debugf("getting details")
e.ShowEpisode.Detailers = detailers
// Get the details
err := e.ShowEpisode.GetDetails(env.Log)
if err != nil {
return err
}
log.Debug("got details from detailers")
return nil
}

View File

@ -4,37 +4,26 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"log"
"strconv"
"github.com/gorilla/mux"
customError "github.com/odwrtw/errors"
"github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib"
"github.com/odwrtw/polochon/modules/pam"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"net/http"
"github.com/gorilla/mux"
"github.com/odwrtw/papi"
polochon "github.com/odwrtw/polochon/lib"
)
// ErrPolochonUnavailable is an error returned if the polochon server is not available
var ErrPolochonUnavailable = fmt.Errorf("Invalid polochon address")
// GetDetailsHandler retrieves details of a show
// GetDetailsHandler handles details of a show
func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return DetailsHandler(env, w, r, false)
}
// RefreahDetailsHandler refresh details of a show
func RefreshDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return DetailsHandler(env, w, r, true)
}
// DetailsHandler handles details of an episode
func DetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force bool) error {
vars := mux.Vars(r)
id := vars["id"]
@ -44,31 +33,94 @@ func DetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force
return env.RenderError(w, errors.New("invalid user type"))
}
s := New(id)
if err := s.GetDetails(env, user, force); err != nil {
return env.RenderError(w, err)
}
if err := s.GetEpisodes(env, force); err != nil {
return env.RenderError(w, err)
}
// Get the show from the polochon of the user
pShow, err := getPolochonShow(user, s.ImdbID)
client, err := user.NewPapiClient()
if err != nil {
env.Log.Warnf("error while getting polochon show %s : %s", s.ImdbID, err)
return env.RenderError(w, err)
}
// For each of the user's polochon episodes, add a direct link to it
for _, pEpisode := range pShow.Episodes {
for _, e := range s.Episodes {
if e.Season != pEpisode.Season || e.Episode != pEpisode.Episode {
continue
}
e.PolochonURL = pEpisode.PolochonURL
pShow, err := client.GetShow(id)
if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting show ", err)
}
wShow, err := backend.IsShowWishlisted(env.Database, user.ID, id)
if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting wishlisted show ", err)
}
s := NewWithClient(id, client, pShow, wShow, env.Config.PublicDir)
// First try from the db
first := []polochon.Detailer{env.Backend.Detailer}
// Then try from the polochon detailers
detailers := env.Config.ShowDetailers
err = s.GetAndFetch(env, first, detailers)
if err != nil {
env.Log.Error(err)
return err
}
env.Log.Debug("getting episodes torrents")
for _, e := range s.Show.Episodes {
// Get torrents from the db
backend := []polochon.Torrenter{env.Backend.Torrenter}
err := GetTorrents(env, e, backend)
if err != nil {
env.Log.Error(err)
}
}
return env.RenderJSON(w, s)
}
// RefreshShowHandler refreshes details of a show + torrents
func RefreshShowHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["id"]
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return env.RenderError(w, errors.New("invalid user type"))
}
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
pShow, err := client.GetShow(id)
if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting show ", err)
}
wShow, err := backend.IsShowWishlisted(env.Database, user.ID, id)
if err != nil && err != papi.ErrResourceNotFound {
log.Println("Got error getting wishlisted show ", err)
}
s := NewWithClient(id, client, pShow, wShow, env.Config.PublicDir)
// Refresh the polochon detailers
detailers := env.Config.ShowDetailers
err = s.Refresh(env, detailers)
if err != nil {
env.Log.Error(err)
return err
}
env.Log.Debug("getting episodes torrents")
for _, e := range s.Episodes {
// Get torrents from the db
err := RefreshTorrents(env, e, env.Config.ShowTorrenters)
if err != nil {
env.Log.Error(err)
}
}
env.Log.Debug("getting polochon show")
return env.RenderJSON(w, s)
}
// SearchShow will search a show
func SearchShow(env *web.Env, w http.ResponseWriter, r *http.Request) error {
var data struct {
@ -90,23 +142,50 @@ func SearchShow(env *web.Env, w http.ResponseWriter, r *http.Request) error {
var shows []*polochon.Show
searchers := env.Config.ShowSearchers
// Iterate on all the searchers to search for the show
for _, searcher := range searchers {
result, err := searcher.SearchShow(data.Key, env.Log)
if err != nil {
env.Log.Errorf("error while searching show : %s", err)
continue
}
// Add the results to the list of results
shows = append(shows, result...)
}
env.Log.Debugf("got %d shows doing search %q", len(shows), data.Key)
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
// Get the polochon's shows
pShows, err := client.GetShows()
if err != nil {
return env.RenderError(w, err)
}
// Get the user's wishlisted shows
wShows, err := backend.GetShowWishlist(env.Database, user.ID)
if err != nil {
return env.RenderError(w, err)
}
showList := []*Show{}
// Now iterate over all the shows to get details
for _, s := range shows {
show := New(s.ImdbID)
err := show.GetDetails(env, user, false)
pShow, _ := pShows.Has(s.ImdbID)
wShow, _ := wShows.IsShowInWishlist(s.ImdbID)
show := NewWithClient(s.ImdbID, client, pShow, wShow, env.Config.PublicDir)
// First try from the db
first := []polochon.Detailer{env.Backend.Detailer}
// Then try from the polochon detailers
detailers := env.Config.ShowDetailers
err := show.GetAndFetch(env, first, detailers)
if err != nil {
env.Log.Errorf("error while getting show details : %s", err)
continue
env.Log.Error(err)
}
showList = append(showList, show)
}
@ -135,8 +214,7 @@ func AddToWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderError(w, errors.New("invalid user type"))
}
s := New(id)
if err := s.AddToWishlist(env, user, data.Season, data.Episode); err != nil {
if err := backend.AddShowToWishlist(env.Database, user.ID, id, data.Season, data.Episode); err != nil {
env.Log.Warnf("Error while adding to db : %s", err)
return env.RenderError(w, err)
}
@ -155,8 +233,7 @@ func DeleteFromWishlist(env *web.Env, w http.ResponseWriter, r *http.Request) er
return env.RenderError(w, errors.New("invalid user type"))
}
s := New(id)
if err := s.DeleteFromWishlist(env, user); err != nil {
if err := backend.DeleteShowFromWishlist(env.Database, user.ID, id); err != nil {
env.Log.Warnf("Error while deleting to db : %s", err)
return env.RenderError(w, err)
}
@ -172,111 +249,64 @@ func GetWishlistHandler(env *web.Env, w http.ResponseWriter, r *http.Request) er
return env.RenderError(w, errors.New("invalid user type"))
}
shows, err := GetWishlist(env, user)
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
return env.RenderJSON(w, shows)
// Get the polochon's shows
pShows, err := client.GetShows()
if err != nil {
return env.RenderError(w, err)
}
wShows, err := backend.GetShowWishlist(env.Database, user.ID)
if err != nil {
return env.RenderError(w, err)
}
showList := []*Show{}
for _, wishedShow := range wShows.List() {
pShow, _ := pShows.Has(wishedShow.ImdbID)
show := NewWithClient(wishedShow.ImdbID, client, pShow, wishedShow, env.Config.PublicDir)
// First check in the DB
before := []polochon.Detailer{env.Backend.Detailer}
// Then with the default detailers
after := env.Config.ShowDetailers
err := show.GetAndFetch(env, before, after)
if err != nil {
env.Log.Errorf("error while getting show details : %s", err)
continue
}
showList = append(showList, show)
}
return env.RenderJSON(w, showList)
}
// getPolochonShows returns all the Shows from the polochon of a user
func getPolochonShows(user *users.User) ([]*Show, error) {
shows := []*Show{}
var polochonConfig config.UserPolochon
err := user.GetConfig("polochon", &polochonConfig)
if err != nil {
return shows, err
}
client, err := papi.New(polochonConfig.URL)
if err != nil {
return shows, err
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
}
pshows, err := client.GetShows()
if err != nil {
// Catch network error for accessing specified polochon address
if uerr, ok := err.(*url.Error); ok {
if nerr, ok := uerr.Err.(*net.OpError); ok {
if nerr.Op == "dial" {
return shows, ErrPolochonUnavailable
}
}
}
return shows, err
}
for _, pshow := range pshows {
show := New(pshow.ImdbID)
for _, season := range pshow.Seasons {
for _, episode := range season.Episodes {
e := NewEpisode()
e.Season = episode.Season
e.Episode = episode.Episode
e.ShowImdbID = episode.ShowImdbID
e.PolochonURL, _ = client.DownloadURL(
&papi.Episode{
ShowImdbID: show.ImdbID,
Episode: e.Episode,
Season: e.Season,
},
)
show.Episodes = append(show.Episodes, e)
}
}
shows = append(shows, show)
}
return shows, nil
}
// getPolochonShow returns a Show with its epidodes from the polochon of a user
func getPolochonShow(user *users.User, imdbID string) (Show, error) {
shows, err := getPolochonShows(user)
if err != nil {
return Show{}, err
}
for _, s := range shows {
if s.ImdbID == imdbID {
return *s, nil
}
}
return Show{}, nil
}
// FromPolochon will returns shows from Polochon
func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// PolochonShowsHandler will returns shows from Polochon
func PolochonShowsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
v := auth.GetCurrentUser(r, env.Log)
user, ok := v.(*users.User)
if !ok {
return env.RenderError(w, errors.New("invalid user type"))
}
shows, err := getPolochonShows(user)
// Get the polochon's shows
shows, err := getPolochonShows(env, user)
if err != nil {
return env.RenderError(w, err)
}
var polochonConfig config.UserPolochon
err = user.GetConfig("polochon", &polochonConfig)
if err != nil {
return env.RenderError(w, err)
}
detailer, err := pam.New(&pam.Params{
Endpoint: polochonConfig.URL,
Token: polochonConfig.Token,
})
// Get details in DB for each shows
// Fetch the details if not found
for _, s := range shows {
s.Detailers = []polochon.Detailer{detailer}
err := s.GetDetails(env, user, false)
// First try from the db
first := []polochon.Detailer{env.Backend.Detailer}
// Then try from the polochon detailer
detailers := env.Config.ShowDetailers
err := s.GetAndFetch(env, first, detailers)
if err != nil {
env.Log.Error(err)
}
@ -287,11 +317,6 @@ func FromPolochon(env *web.Env, w http.ResponseWriter, r *http.Request) error {
// RefreshEpisodeHandler refresh details of an episode
func RefreshEpisodeHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return EpisodeDetailsHandler(env, w, r, true)
}
// EpisodeDetailsHandler handles details of a show
func EpisodeDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request, force bool) error {
vars := mux.Vars(r)
id := vars["id"]
@ -306,29 +331,28 @@ func EpisodeDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request,
return env.RenderError(w, errors.New("invalid user type"))
}
s := New(id)
e, err := s.GetEpisodeDetails(env, season, episode, force)
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, err)
}
err = e.GetTorrents(env, force)
if err != nil && customError.IsFatal(err) {
pShow, err := client.GetShow(id)
if err != nil && err != papi.ErrResourceNotFound {
env.Log.Warnf("Error getting show ", err)
}
e := NewEpisode(client, pShow, id, season, episode)
// Refresh the episode
err = e.Refresh(env, env.Config.ShowDetailers)
if err != nil {
env.Log.Error(err)
return env.RenderError(w, err)
}
// Get the show from the polochon of the user
pShow, err := getPolochonShow(user, id)
// Refresh the torrents
err = e.RefreshTorrents(env, env.Config.ShowTorrenters)
if err != nil {
env.Log.Warnf("error while getting polochon episode %s S%02dE%02d : %s", id, season, episode, err)
}
// Find if the user has a the episode in its polochon to add the
// DownloadURL
for _, pEpisode := range pShow.Episodes {
if e.Season == pEpisode.Season && e.Episode == pEpisode.Episode {
e.PolochonURL = pEpisode.PolochonURL
break
}
env.Log.Error(err)
}
return env.RenderJSON(w, e)

View File

@ -1,419 +1,215 @@
package shows
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"strings"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx"
"github.com/odwrtw/papi"
"github.com/odwrtw/polochon/lib"
)
const (
upsertShowQuery = `
INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired)
VALUES (:imdb_id, :title, :rating, :plot, :tvdb_id, :year, :first_aired)
ON CONFLICT (imdb_id)
DO UPDATE
SET imdb_id=:imdb_id, title=:title, rating=:rating, plot=:plot,
tvdb_id=:tvdb_id, year=:year, first_aired=:first_aired
RETURNING id;`
getShowQueryByImdbID = `
SELECT *
FROM shows WHERE imdb_id=$1;`
getShowQueryByID = `
SELECT *
FROM shows WHERE id=$1;`
deleteShowQueryByID = `DELETE FROM shows WHERE id=$1;`
getShowWithUserQueryByImdbID = `
SELECT
shows.id,
shows.imdb_id,
shows.title,
shows.rating,
shows.plot,
shows.tvdb_id,
shows.year,
shows.first_aired,
shows_tracked.season,
shows_tracked.episode,
shows.updated_at,
shows.created_at
FROM shows LEFT JOIN shows_tracked
ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$2
WHERE shows.imdb_id=$1;`
getShowWithUserQueryByID = `
SELECT
shows.id,
shows.imdb_id,
shows.title,
shows.rating,
shows.plot,
shows.tvdb_id,
shows.year,
shows.first_aired,
shows_tracked.season,
shows_tracked.episode,
shows.updated_at,
shows.created_at
FROM shows LEFT JOIN shows_tracked ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$2
WHERE shows.id=$1;`
)
var (
// ErrNotFound error returned when show not found in database
ErrNotFound = fmt.Errorf("Not found")
)
// Show represents a show
type Show struct {
sqly.BaseModel
polochon.Show
Episodes []*Episode `json:"episodes"`
TrackedSeason *int `json:"tracked_season"`
TrackedEpisode *int `json:"tracked_episode"`
BannerURL string `json:"banner_url"`
FanartURL string `json:"fanart_url"`
PosterURL string `json:"poster_url"`
client *papi.Client
pShow *papi.Show
*polochon.Show
TrackedSeason *int `json:"tracked_season"`
TrackedEpisode *int `json:"tracked_episode"`
publicDir string
}
// ShowDB represents the Show in the DB
type ShowDB struct {
ID string `db:"id"`
ImdbID string `db:"imdb_id"`
TvdbID int `db:"tvdb_id"`
TrackedSeason *int `db:"season"`
TrackedEpisode *int `db:"episode"`
Title string `db:"title"`
Rating float32 `db:"rating"`
Plot string `db:"plot"`
Year int `db:"year"`
FirstAired time.Time `db:"first_aired"`
Created time.Time `db:"created_at"`
Updated time.Time `db:"updated_at"`
}
// NewShowDB returns a Show ready to be put in DB from a
// Show
func NewShowDB(s *Show) ShowDB {
sDB := ShowDB{
ID: s.ID,
ImdbID: s.ImdbID,
Title: s.Title,
Rating: s.Rating,
Plot: s.Plot,
TvdbID: s.TvdbID,
Year: s.Year,
Created: s.Created,
Updated: s.Updated,
// MarshalJSON implements the Marshal interface
func (s *Show) MarshalJSON() ([]byte, error) {
type alias Show
// Create the structure that we want to marshal
showToMarshal := &struct {
*alias
Episodes []Episode `json:"episodes"`
BannerURL string `json:"banner_url"`
FanartURL string `json:"fanart_url"`
PosterURL string `json:"poster_url"`
}{
alias: (*alias)(s),
BannerURL: s.GetImageURL("banner"),
FanartURL: s.GetImageURL("fanart"),
PosterURL: s.GetImageURL("poster"),
}
if s.FirstAired != nil {
sDB.FirstAired = *s.FirstAired
}
return sDB
}
// FillFromDB returns a Show from a ShowDB extracted from the DB
func (s *Show) FillFromDB(sDB *ShowDB) {
s.ID = sDB.ID
s.ImdbID = sDB.ImdbID
s.Title = sDB.Title
s.Rating = sDB.Rating
s.Plot = sDB.Plot
s.TvdbID = sDB.TvdbID
s.Year = sDB.Year
s.FirstAired = &sDB.FirstAired
s.Created = sDB.Created
s.Updated = sDB.Updated
s.TrackedSeason = sDB.TrackedSeason
s.TrackedEpisode = sDB.TrackedEpisode
// Create Episode obj from polochon.Episodes and add them to the object to
// marshal
for _, e := range s.Show.Episodes {
showToMarshal.Episodes = append(showToMarshal.Episodes, Episode{
ShowEpisode: e,
client: s.client,
pShow: s.pShow,
})
}
return json.Marshal(showToMarshal)
}
// New returns a new Show with a polochon ShowConfig
func New(imdbID string) *Show {
return &Show{
Show: polochon.Show{
Show: &polochon.Show{
ImdbID: imdbID,
},
}
}
// Get returns a show with user info like tracked
func (s *Show) Get(env *web.Env, user *users.User) error {
var err error
var sDB ShowDB
if s.ID != "" {
err = env.Database.QueryRowx(getShowWithUserQueryByID, s.ID, user.ID).StructScan(&sDB)
} else if s.ImdbID != "" {
err = env.Database.QueryRowx(getShowWithUserQueryByImdbID, s.ImdbID, user.ID).StructScan(&sDB)
} else {
err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID")
// NewWithClient returns a new Show with a polochon ShowConfig
func NewWithClient(imdbID string, client *papi.Client, pShow *papi.Show, wShow *backend.WishedShow, publicDir string) *Show {
s := &Show{
Show: &polochon.Show{
ImdbID: imdbID,
},
client: client,
pShow: pShow,
publicDir: publicDir,
}
if err != nil {
return err
if wShow != nil {
s.TrackedSeason = &wShow.Season
s.TrackedEpisode = &wShow.Episode
}
// Set the poster url
s.PosterURL = s.GetPosterURL(env)
s.FillFromDB(&sDB)
return nil
return s
}
// GetShow returns a show with user info like tracked
func (s *Show) GetShow(db *sqlx.DB) error {
var err error
var sDB ShowDB
if s.ID != "" {
err = db.QueryRowx(getShowQueryByID, s.ID).StructScan(&sDB)
} else if s.ImdbID != "" {
err = db.QueryRowx(getShowQueryByImdbID, s.ImdbID).StructScan(&sDB)
} else {
err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID")
}
if err != nil {
return err
}
// Set the poster url
// s.PosterURL = s.GetPosterURL(env)
s.FillFromDB(&sDB)
return nil
}
// GetDetails retrieves details for the show, first try to
// get info from db, if not exists, use polochon.Detailer
// and save informations in the database for future use
func (s *Show) GetDetails(env *web.Env, user *users.User, force bool) error {
// GetDetails retrieves details for the show with the given detailers
func (s *Show) GetDetails(env *web.Env, detailers []polochon.Detailer) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": s.ImdbID,
"function": "shows.GetDetails",
})
log.Debugf("getting details")
if len(s.Detailers) == 0 {
s.Detailers = env.Config.ShowDetailers
var detailersName []string
for _, d := range detailers {
detailersName = append(detailersName, d.Name())
}
log.Debugf("getting details with %s", strings.Join(detailersName, ", "))
var err error
err = s.Get(env, user)
switch err {
case nil:
log.Debug("show found in database")
case sql.ErrNoRows:
log.Debug("show not found in database")
default:
// Unexpected error
return err
}
// If force is not specified, don't go further
if !force {
// Will return ErrNoRows if the show wasn't found
return err
}
s.Detailers = detailers
// GetDetail
err = s.Show.GetDetails(env.Log)
// Get the details
err := s.Show.GetDetails(env.Log)
if err != nil {
return err
}
log.Debug("got details from detailers")
s.Episodes = []*Episode{}
for _, pe := range s.Show.Episodes {
s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe})
}
err = s.Upsert(env.Database)
if err != nil {
log.Debug("error while doing show upsert func", err)
return err
}
log.Debug("show added in database")
// Download show images
s.downloadImages(env)
log.Debug("images downloaded")
// Set the poster url
s.PosterURL = s.GetPosterURL(env)
log.Debugf("got details from detailers ")
return nil
}
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded
func (s *Show) GetPosterURL(env *web.Env) string {
// GetAndFetch retrieves details for the show with the given
// detailers 'before'
// If found, return
// If not, retrives details with the detailers 'after' and update them in
// database
func (s *Show) GetAndFetch(env *web.Env, before []polochon.Detailer, after []polochon.Detailer) error {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": s.ImdbID,
"function": "shows.GetAndFetch",
})
// Try to get details with the first batch of Detailers
err := s.GetDetails(env, before)
if err == nil {
log.Debug("show found in first try")
return nil
}
log.Debugf("show not found in database: %s", err)
// If not found, try the second batch and upsert
return s.Refresh(env, after)
}
// Refresh retrieves details for the show with the given detailers
// and update them in database
func (s *Show) Refresh(env *web.Env, detailers []polochon.Detailer) error {
// Refresh
err := s.GetDetails(env, detailers)
if err != nil {
return err
}
// Download show images
s.downloadImages(env)
env.Log.Debug("images downloaded")
// If found, update in database
return backend.UpsertShow(env.Database, s.Show)
}
// GetImageURL returns the image URL or the default image if the poster is not yet downloaded
func (s *Show) GetImageURL(imgType string) string {
// Check if the show image exists
if _, err := os.Stat(s.imgFile(env, "poster")); os.IsNotExist(err) {
if _, err := os.Stat(s.imgFile(imgType)); os.IsNotExist(err) {
// TODO image in the config ?
return "img/noimage.png"
}
return s.imgURL(env, "poster")
return s.imgURL(imgType)
}
// downloadImages will download the show images
func (s *Show) downloadImages(env *web.Env) {
// Download the banner
err := web.Download(s.Show.Banner, s.imgFile(env, "banner"))
err := web.Download(s.Show.Banner, s.imgFile("banner"))
if err != nil {
env.Log.Errorf("failed to dowload banner: %s", err)
}
err = web.Download(s.Show.Fanart, s.imgFile(env, "fanart"))
err = web.Download(s.Show.Fanart, s.imgFile("fanart"))
if err != nil {
env.Log.Errorf("failed to dowload fanart: %s", err)
}
err = web.Download(s.Show.Poster, s.imgFile(env, "poster"))
err = web.Download(s.Show.Poster, s.imgFile("poster"))
if err != nil {
env.Log.Errorf("failed to dowload poster: %s", err)
}
}
// imgFile returns the image location on disk
func (s *Show) imgFile(env *web.Env, imgType string) string {
func (s *Show) imgFile(imgType string) string {
fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType)
return filepath.Join(env.Config.PublicDir, fileURL)
return filepath.Join(s.publicDir, fileURL)
}
// imgURL returns the default image url
func (s *Show) imgURL(env *web.Env, imgType string) string {
func (s *Show) imgURL(imgType string) string {
return fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType)
}
// Upsert a show in the database
func (s *Show) Upsert(db *sqlx.DB) error {
sDB := NewShowDB(s)
var id string
r, err := db.NamedQuery(upsertShowQuery, sDB)
// getPolochonShows returns all the Shows from the polochon of a user
func getPolochonShows(env *web.Env, user *users.User) ([]*Show, error) {
shows := []*Show{}
client, err := user.NewPapiClient()
if err != nil {
return err
return shows, err
}
for r.Next() {
r.Scan(&id)
}
s.ID = id
for _, e := range s.Episodes {
e.ShowImdbID = s.ImdbID
err = e.Upsert(db)
if err != nil {
return err
}
}
return nil
}
// Delete show from database
func (s *Show) Delete(db *sqlx.DB) error {
r, err := db.Exec(deleteShowQueryByID, s.ID)
// Get the polochon's shows
pshows, err := client.GetShows()
if err != nil {
return err
return shows, err
}
count, _ := r.RowsAffected()
if count != 1 {
return fmt.Errorf("Unexpected number of row deleted: %d", count)
}
return nil
}
// GetEpisodes from database
func (s *Show) GetEpisodes(env *web.Env, force bool) error {
// We retrive episode's info from database populate the s.Episodes member
var episodesDB = []*EpisodeDB{}
err := env.Database.Select(&episodesDB, getEpisodesQuery, s.ImdbID)
wShows, err := backend.GetShowWishlist(env.Database, user.ID)
if err != nil {
return err
}
if len(episodesDB) == 0 {
return nil
return shows, err
}
for _, episodeDB := range episodesDB {
episode := NewEpisode()
episode.FillFromDB(episodeDB)
s.Episodes = append(s.Episodes, episode)
err = episode.GetTorrents(env, force)
if err != nil {
env.Log.Debugf("error while getting episode torrent: %q", err)
}
// Create Shows objects from the shows retrieved
for _, pShow := range pshows.List() {
wShow, _ := wShows.IsShowInWishlist(pShow.ImdbID)
show := NewWithClient(pShow.ImdbID, client, pShow, wShow, env.Config.PublicDir)
shows = append(shows, show)
}
return nil
}
// GetEpisode from database
func (s *Show) GetEpisodeDetails(env *web.Env, seasonNb, episodeNb int, force bool) (*Episode, error) {
log := env.Log.WithFields(logrus.Fields{
"imdb_id": s.ImdbID,
"season": seasonNb,
"episode": episodeNb,
"function": "show.GetEpisodeDetails",
})
log.Debugf("getting episode details")
e := NewEpisode()
e.ShowImdbID = s.ImdbID
e.Season = seasonNb
e.Episode = episodeNb
if len(e.Detailers) == 0 {
e.Detailers = env.Config.ShowDetailers
}
var err error
err = e.Get(env)
switch err {
case nil:
log.Debug("episode found in database")
case sql.ErrNoRows:
log.Debug("episode not found in database")
default:
// Unexpected error
return nil, err
}
// If force is not specified, don't go further
if !force {
// Will return ErrNoRows if the episode wasn't found
return e, err
}
// GetDetail of the episode
err = e.ShowEpisode.GetDetails(env.Log)
if err != nil {
return nil, err
}
// Upsert the episode
err = e.Upsert(env.Database)
if err != nil {
log.Debug("error while doing episode upsert func", err)
return nil, err
}
log.Debug("episode inserted/updated in database")
return e, nil
}
// GetTorrents from the database or fetch them if needed
func (s *Show) GetTorrents(env *web.Env, force bool) error {
for _, e := range s.Episodes {
err := e.GetTorrents(env, force)
if err != nil {
env.Log.Errorf("error while getting episode torrent: %s", err)
}
}
return nil
return shows, nil
}

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"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/odwrtw/papi"
)
// DownloadHandler downloads a movie via polochon
@ -33,19 +30,9 @@ func DownloadHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error
return env.RenderError(w, errors.New("invalid user type"))
}
var polochonConfig config.UserPolochon
err = user.GetConfig("polochon", &polochonConfig)
client, err := user.NewPapiClient()
if err != nil {
return env.RenderError(w, errors.New("problem when getting user config"))
}
client, err := papi.New(polochonConfig.URL)
if err != nil {
return env.RenderError(w, errors.New("problem when getting papi client"))
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
return env.RenderError(w, err)
}
err = client.AddTorrent(data.URL)

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"
)
// SignupPOSTHandler handles the user's Signup
func SignupPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
var data struct {
Username string `json:"username"`

View File

@ -2,10 +2,14 @@ package users
import (
"encoding/json"
"errors"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/types"
"github.com/odwrtw/papi"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/random"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly"
)
@ -23,7 +27,9 @@ const (
)
const (
UserRole = "user"
// UserRole represents the user's role
UserRole = "user"
// AdminRole represents the admin's role
AdminRole = "admin"
)
@ -89,6 +95,25 @@ func (u *User) NewConfig() error {
return u.RawConfig.UnmarshalJSON(b)
}
// NewPapiClient creates a new papi client for the given user
func (u *User) NewPapiClient() (*papi.Client, error) {
var polochonConfig config.UserPolochon
err := u.GetConfig("polochon", &polochonConfig)
if err != nil {
return nil, errors.New("missing polochon config")
}
client, err := papi.New(polochonConfig.URL)
if err != nil {
return nil, errors.New("error getting papi client")
}
if polochonConfig.Token != "" {
client.SetToken(polochonConfig.Token)
}
return client, nil
}
// Token represents a token
type Token struct {
sqly.BaseModel
@ -201,6 +226,7 @@ func (u *User) GetName() string {
return u.Name
}
// HasRole checks if a user as a role
func (u *User) HasRole(role string) bool {
if role == AdminRole && !u.Admin {
return false
@ -208,6 +234,7 @@ func (u *User) HasRole(role string) bool {
return true
}
// IsAdmin checks if a user is admin
func (u *User) IsAdmin() bool {
return u.HasRole(AdminRole)
}

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

View File

@ -7,17 +7,14 @@ import (
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/backend"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/torrents"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/users"
extmedias "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/external_medias"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/phyber/negroni-gzip/gzip"
"github.com/robfig/cron"
"github.com/urfave/negroni"
)
@ -61,43 +58,38 @@ func main() {
Auth: authorizer,
Log: log,
Config: cf,
Backend: web.DetailerTorrenter{
Torrenter: backend,
Detailer: backend,
},
})
authMiddleware := auth.NewMiddleware(env.Auth, log)
env.Handle("/users/login", users.LoginPOSTHandler).Methods("POST")
env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST")
env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole).Methods("GET")
env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole).Methods("POST")
// Setup the routes
setupRoutes(env)
env.Handle("/movies/polochon", movies.FromPolochon).WithRole(users.UserRole).Methods("GET")
env.Handle("/movies/{id:tt[0-9]+}/refresh", movies.GetDetailsHandler).WithRole(users.UserRole).Methods("POST")
env.Handle("/movies/{id:tt[0-9]+}", movies.DeleteHandler).WithRole(users.AdminRole).Methods("DELETE")
env.Handle("/movies/explore", extmedias.Explore).WithRole(users.UserRole).Methods("GET")
env.Handle("/movies/refresh", extmedias.Refresh).WithRole(users.UserRole).Methods("POST")
env.Handle("/movies/search", movies.SearchMovie).WithRole(users.UserRole).Methods("POST")
// Create the cron object
c := cron.New()
env.Handle("/shows/polochon", shows.FromPolochon).WithRole(users.UserRole).Methods("GET")
env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler).WithRole(users.UserRole).Methods("GET")
env.Handle("/shows/{id:tt[0-9]+}/seasons/{season:[0-9]+}/episodes/{episode:[0-9]+}", shows.RefreshEpisodeHandler).WithRole(users.UserRole).Methods("POST")
env.Handle("/shows/{id:tt[0-9]+}/refresh", shows.RefreshDetailsHandler).WithRole(users.UserRole).Methods("POST")
env.Handle("/shows/refresh", extmedias.RefreshShows).WithRole(users.UserRole).Methods("POST")
env.Handle("/shows/explore", extmedias.ExploreShows).WithRole(users.UserRole).Methods("GET")
env.Handle("/shows/search", shows.SearchShow).WithRole(users.UserRole).Methods("POST")
// Refresh the library every 6h
c.AddFunc("@every 6h", func() {
env.Log.Infof("Running refresh cron!")
extmedias.Refresh(env)
})
env.Handle("/torrents", torrents.DownloadHandler).WithRole(users.UserRole).Methods("POST")
// Start the cron
c.Start()
env.Handle("/wishlist/shows", shows.GetWishlistHandler).WithRole(users.UserRole).Methods("GET")
env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.AddToWishlist).WithRole(users.UserRole).Methods("POST")
env.Handle("/wishlist/shows/{id:tt[0-9]+}", shows.DeleteFromWishlist).WithRole(users.UserRole).Methods("DELETE")
env.Handle("/wishlist/movies", movies.GetWishlistHandler).WithRole(users.UserRole).Methods("GET")
env.Handle("/wishlist/movies/{id:tt[0-9]+}", movies.AddToWishlist).WithRole(users.UserRole).Methods("POST")
env.Handle("/wishlist/movies/{id:tt[0-9]+}", movies.DeleteFromWishlist).WithRole(users.UserRole).Methods("DELETE")
// Stop the cron
defer c.Stop()
n := negroni.Classic()
// Middleware for authentication
n.Use(authMiddleware)
// Serve static files
n.Use(negroni.NewStatic(http.Dir(cf.PublicDir)))
// Compress responses
n.Use(gzip.Gzip(gzip.DefaultCompression))
n.UseHandler(env.Router)
n.Run(":" + cf.Port)

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