diff --git a/sql/migration/0001_initial.up.sql b/sql/migration/0001_initial.up.sql index 4866e47..c9d5d62 100644 --- a/sql/migration/0001_initial.up.sql +++ b/sql/migration/0001_initial.up.sql @@ -42,13 +42,14 @@ CREATE TABLE shows ( first_aired timestamp with time zone, LIKE base INCLUDING DEFAULTS ); -CREATE INDEX ON shows (imdb_id); +CREATE UNIQUE INDEX ON shows (imdb_id); CREATE TRIGGER update_shows_updated_at BEFORE UPDATE ON shows FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); CREATE TABLE episodes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), imdb_id text NOT NULL, - show_id uuid REFERENCES shows (id) ON DELETE CASCADE, + show_imdb_id text REFERENCES shows (imdb_id) ON DELETE CASCADE, + show_tvdb_id text NOT NULL, title text NOT NULL, season smallint NOT NULL, episode smallint NOT NULL, @@ -57,19 +58,20 @@ CREATE TABLE episodes ( plot text NOT NULL, runtime smallint NOT NULL, rating real NOT NULL, - LIKE base INCLUDING DEFAULTS + LIKE base INCLUDING DEFAULTS ); -CREATE INDEX ON episodes (show_id, season); -CREATE INDEX ON episodes (show_id, season, episode); + +CREATE INDEX ON episodes (show_imdb_id, season); +CREATE UNIQUE INDEX ON episodes (show_imdb_id, season, episode); CREATE TRIGGER update_episodes_updated_at BEFORE UPDATE ON episodes FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); CREATE TABLE shows_tracked ( - show_id uuid NOT NULL REFERENCES shows (id) ON DELETE CASCADE, + show_imdb_id text NOT NULL REFERENCES shows (imdb_id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE, season smallint NOT NULL, episode smallint NOT NULL ); -CREATE INDEX ON shows_tracked (show_id, user_id); +CREATE INDEX ON shows_tracked (show_imdb_id, user_id); CREATE INDEX ON shows_tracked (user_id); CREATE TABLE movies ( diff --git a/sql/migration/0002_external_medias.up.sql b/sql/migration/0002_external_medias.up.sql index 86a138f..71bfcc4 100644 --- a/sql/migration/0002_external_medias.up.sql +++ b/sql/migration/0002_external_medias.up.sql @@ -1,6 +1,6 @@ CREATE TYPE media_type AS ENUM ('movie', 'show'); CREATE TYPE media_category AS ENUM ('trending', 'popular', 'anticipated', 'box_office'); -CREATE TYPE media_source AS ENUM ('trakttv', 'yts'); +CREATE TYPE media_source AS ENUM ('trakttv', 'yts', 'eztv'); CREATE TABLE external_medias ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), diff --git a/sql/migration/0003_torrents.up.sql b/sql/migration/0003_torrents.up.sql index 70fef4f..e458ef9 100644 --- a/sql/migration/0003_torrents.up.sql +++ b/sql/migration/0003_torrents.up.sql @@ -23,7 +23,7 @@ INHERITS (torrents_abstract); CREATE INDEX ON movie_torrents (imdb_id); CREATE UNIQUE INDEX ON movie_torrents (imdb_id, source, quality); CREATE INDEX ON episode_torrents (imdb_id); -CREATE INDEX ON episode_torrents (imdb_id, season, episode); +CREATE UNIQUE INDEX ON episode_torrents (imdb_id, season, episode, source, quality); CREATE INDEX ON torrents_abstract (imdb_id); CREATE TRIGGER update_movie_torrents_updated_at BEFORE UPDATE ON movie_torrents FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); diff --git a/src/internal/config/canape.go b/src/internal/config/canape.go index 8a76564..505d3b9 100644 --- a/src/internal/config/canape.go +++ b/src/internal/config/canape.go @@ -5,7 +5,9 @@ import ( "os" polochon "github.com/odwrtw/polochon/lib" + "github.com/odwrtw/polochon/modules/eztv" "github.com/odwrtw/polochon/modules/tmdb" + "github.com/odwrtw/polochon/modules/tvdb" "github.com/odwrtw/polochon/modules/yts" "gopkg.in/yaml.v2" @@ -22,6 +24,9 @@ type Config struct { TmdbAPIKey string `yaml:"tmdb_api_key"` MovieDetailers []polochon.Detailer MovieTorrenters []polochon.Torrenter + MovieSearchers []polochon.Searcher + ShowDetailers []polochon.Detailer + ShowTorrenters []polochon.Torrenter } type AuthorizerConfig struct { @@ -67,5 +72,29 @@ func Load(path string) (*Config, error) { } cf.MovieTorrenters = append(cf.MovieTorrenters, d) + // Default searchers + cf.MovieSearchers = []polochon.Searcher{} + s, err := yts.NewSearcher() + if err != nil { + return nil, err + } + cf.MovieSearchers = append(cf.MovieSearchers, s) + + // Default detailers + cf.ShowDetailers = []polochon.Detailer{} + showDetailer, err := tvdb.NewDetailer() + if err != nil { + return nil, err + } + cf.ShowDetailers = append(cf.ShowDetailers, showDetailer) + + // Default torrenters + cf.ShowTorrenters = []polochon.Torrenter{} + showTorrenter, err := eztv.New() + if err != nil { + return nil, err + } + cf.ShowTorrenters = append(cf.ShowTorrenters, showTorrenter) + return cf, nil } diff --git a/src/internal/external_medias/external_medias.go b/src/internal/external_medias/external_medias.go index ba94b0a..338b078 100644 --- a/src/internal/external_medias/external_medias.go +++ b/src/internal/external_medias/external_medias.go @@ -7,7 +7,6 @@ import ( ) const ( - // addExternalMediaQuery = `INSERT INTO external_medias (type, source, category, ids) VALUES ($1, $2, $3, $4) RETURNING id;` upsertExternalMediaQuery = ` INSERT INTO external_medias (type, source, category, ids) VALUES (:type, :source, :category, :ids) diff --git a/src/internal/external_medias/handlers.go b/src/internal/external_medias/handlers.go index bbe6c0a..c60f603 100644 --- a/src/internal/external_medias/handlers.go +++ b/src/internal/external_medias/handlers.go @@ -6,11 +6,13 @@ import ( "net/http" "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" + eztvExplorer "github.com/odwrtw/polochon/modules/eztv" traktExplorer "github.com/odwrtw/polochon/modules/trakttv" ytsExplorer "github.com/odwrtw/polochon/modules/yts" ) @@ -28,13 +30,13 @@ func GetMediaIDs(env *web.Env, mediaType string, source string, category string, media, err := Get(env.Database, mediaType, source, category) switch err { case nil: - log.Debug("medias found in database") + log.Debugf("%s medias found in database", mediaType) if !force { log.Debug("returning medias from db") return media.IDs, nil } case sql.ErrNoRows: - log.Debug("medias not found in database") + log.Debugf("%s medias not found in database", mediaType) default: // Unexpected error return nil, err @@ -46,9 +48,25 @@ func GetMediaIDs(env *web.Env, mediaType string, source string, category string, } var ids []string - movies, err := explorer.GetMovieList(polochon.ExploreByRate, log) - for _, movie := range movies { - ids = append(ids, movie.ImdbID) + 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) @@ -60,7 +78,6 @@ func GetMediaIDs(env *web.Env, mediaType string, source string, category string, IDs: ids, } - log.Debugf("Inserting int to DB %+v", media) err = media.Upsert(env.Database) if err != nil { return nil, err @@ -71,8 +88,8 @@ func GetMediaIDs(env *web.Env, mediaType string, source string, category string, return ids, nil } -// GetMedias get some movies -func GetMedias(env *web.Env, source string, category string, force bool) ([]*movies.Movie, error) { +// GetMovies get some movies +func GetMovies(env *web.Env, source string, category string, force bool) ([]*movies.Movie, error) { movieIds, err := GetMediaIDs(env, "movie", source, category, force) if err != nil { return nil, err @@ -83,12 +100,12 @@ func GetMedias(env *web.Env, source string, category string, force bool) ([]*mov movie := movies.New(id) err := movie.GetDetails(env, force) if err != nil { - env.Log.Error(err) + env.Log.Errorf("error while getting movie details : %s", err) continue } err = movie.GetTorrents(env, force) if err != nil { - env.Log.Error(err) + env.Log.Errorf("error while getting movie torrents : %s", err) continue } movieList = append(movieList, movie) @@ -96,6 +113,31 @@ func GetMedias(env *web.Env, source string, category string, force bool) ([]*mov return movieList, nil } +// GetShows get some shows +func GetShows(env *web.Env, source string, category string, force bool) ([]*shows.Show, error) { + showIds, err := GetMediaIDs(env, "show", source, category, force) + if err != nil { + return nil, err + } + + showList := []*shows.Show{} + for _, id := range showIds { + show := shows.New(id) + err := show.GetDetails(env, force) + 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 { err := r.ParseForm() @@ -116,7 +158,7 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error { } // Get the medias without trying to refresh them - movies, err := GetMedias(env, source, category, false) + movies, err := GetMovies(env, source, category, false) if err != nil { return err } @@ -124,12 +166,45 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error { return env.RenderJSON(w, movies) } +// ExploreShows will explore some shows +func ExploreShows(env *web.Env, w http.ResponseWriter, r *http.Request) error { + err := r.ParseForm() + if err != nil { + return err + } + + source := r.FormValue("source") + // Default source + if source == "" { + source = "eztv" + } + + category := r.FormValue("category") + // Default category + if category == "" { + category = "popular" + } + + // Get the medias without trying to refresh them + shows, err := GetShows(env, source, category, false) + if err != nil { + return err + } + + 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 ...") @@ -147,7 +222,33 @@ func Refresh(env *web.Env, w http.ResponseWriter, r *http.Request) error { for _, source := range MediaSources { env.Log.Debugf("refreshing %s", source) // GetMedias and refresh them - _, err := GetMedias(env, source, category, true) + _, err := GetMovies(env, source, category, true) + if err != nil { + return 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" + } + + // 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, source, category, true) if err != nil { return err } @@ -165,6 +266,8 @@ func NewExplorer(env *web.Env, source string) (polochon.Explorer, error) { }) case "yts": return ytsExplorer.NewExplorer() + case "eztv": + return eztvExplorer.NewExplorer() default: return nil, fmt.Errorf("unknown explorer") } diff --git a/src/internal/movies/handlers.go b/src/internal/movies/handlers.go index be12419..70e0e76 100644 --- a/src/internal/movies/handlers.go +++ b/src/internal/movies/handlers.go @@ -1,6 +1,7 @@ package movies import ( + "errors" "fmt" "net" "net/http" @@ -109,3 +110,41 @@ func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) err return env.RenderJSON(w, m) } + +// SearchMovie will search movie +func SearchMovie(env *web.Env, w http.ResponseWriter, r *http.Request) error { + key := r.FormValue("key") + if key == "" { + return env.RenderError(w, errors.New("no given key")) + } + + var movies []*polochon.Movie + searchers := env.Config.MovieSearchers + for _, searcher := range searchers { + result, err := searcher.SearchMovie(key, env.Log) + if err != nil { + env.Log.Errorf("error while searching movie : %s", err) + continue + } + movies = append(movies, result...) + } + + env.Log.Debugf("got %d movies doing search %q", len(movies), key) + movieList := []*Movie{} + for _, m := range movies { + movie := New(m.ImdbID) + err := movie.GetDetails(env, false) + if err != nil { + env.Log.Errorf("error while getting movie details : %s", err) + continue + } + err = movie.GetTorrents(env, false) + if err != nil { + env.Log.Errorf("error while getting movie torrents : %s", err) + continue + } + movieList = append(movieList, movie) + } + + return env.RenderJSON(w, movieList) +} diff --git a/src/internal/shows/episodes.go b/src/internal/shows/episodes.go new file mode 100644 index 0000000..f02e223 --- /dev/null +++ b/src/internal/shows/episodes.go @@ -0,0 +1,180 @@ +package shows + +import ( + "database/sql" + "time" + + "github.com/Sirupsen/logrus" + "github.com/jmoiron/sqlx" + 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/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;` +) + +// Episode represents an episode +type Episode struct { + sqly.BaseModel + 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"` +} + +// 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, + } +} + +// 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 +// database +func (e *Episode) GetTorrents(env *web.Env, force bool) error { + log := env.Log.WithFields(logrus.Fields{ + "imdb_id": e.ShowEpisode.ShowImdbID, + "season": e.ShowEpisode.Season, + "episode": e.ShowEpisode.Episode, + "function": "shows.GetTorrents", + }) + log.Debugf("getting torrents") + + if len(e.Torrenters) == 0 { + e.Torrenters = env.Config.ShowTorrenters + } + + 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") + // We'll need to GetTorrents from torrenters + default: + // Unexpected error + return err + } + 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) + } + return nil + } + + err = e.ShowEpisode.GetTorrents(env.Log) + if err != nil { + return err + } + + log.Debugf("got %d torrents from torrenters", len(e.ShowEpisode.Torrents)) + + for _, t := range e.ShowEpisode.Torrents { + torrent := torrents.NewEpisodeFromPolochon(e.ShowEpisode, t) + err = torrent.Upsert(env.Database) + if err != nil { + log.Errorf("error while adding torrent : %s", err) + continue + } + } + + return nil +} diff --git a/src/internal/shows/handlers.go b/src/internal/shows/handlers.go new file mode 100644 index 0000000..488d42a --- /dev/null +++ b/src/internal/shows/handlers.go @@ -0,0 +1,24 @@ +package shows + +import ( + "net/http" + + "github.com/gorilla/mux" + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" +) + +// GetDetailsHandler retrieves details for a movie +func GetDetailsHandler(env *web.Env, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + id := vars["id"] + + s := New(id) + if err := s.GetDetails(env, false); err != nil { + return err + } + if err := s.GetEpisodes(env); err != nil { + return err + } + + return env.RenderJSON(w, s) +} diff --git a/src/internal/shows/shows.go b/src/internal/shows/shows.go index 7ca0dce..cd5475c 100644 --- a/src/internal/shows/shows.go +++ b/src/internal/shows/shows.go @@ -1,10 +1,15 @@ package shows import ( + "database/sql" "fmt" + "os" + "path/filepath" + "time" "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" "github.com/Sirupsen/logrus" "github.com/jmoiron/sqlx" @@ -12,70 +17,57 @@ import ( ) const ( - addShowQuery = ` + upsertShowQuery = ` INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired) - VALUES (:imdbid, :title, :rating, :plot, :tvdbid, :year, :firstaired) RETURNING id;` + 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 - id, imdb_id AS imdbid, - title, rating, plot, - tvdb_id AS tvdbid, - year, first_aired AS firstaired, - created_at, updated_at + SELECT * FROM shows WHERE imdb_id=$1;` getShowQueryByID = ` - SELECT - id, imdb_id AS imdbid, - title, rating, plot, - tvdb_id AS tvdbid, - year, first_aired AS firstaired, - created_at, updated_at + SELECT * FROM shows WHERE id=$1;` - deleteShowQuery = `DELETE FROM shows WHERE id=$1;` - - addEpisodeQuery = ` - INSERT INTO episodes (show_id, title, season, episode, tvdb_id, aired, plot, runtime, rating, imdb_id) - VALUES (:showid, :title, :season, :episode, :tvdbid, :aired, :plot, :runtime, :rating, :episodeimdbid) RETURNING id;` - - getEpisodesQuery = ` - SELECT title, season, episode, tvdb_id AS tvdbid, aired, plot, runtime, rating, imdb_id AS episodeimdbid, show_id AS showid - FROM episodes WHERE show_id=$1;` + deleteShowQueryByID = `DELETE FROM shows WHERE id=$1;` getShowWithUserQueryByImdbID = ` SELECT shows.id, - shows.imdb_id AS imdbid, + shows.imdb_id, shows.title, shows.rating, shows.plot, - shows.tvdb_id AS tvdbid, + shows.tvdb_id, shows.year, - shows.first_aired AS firstaired, + shows.first_aired, shows.created_at, shows.updated_at, - COALESCE(shows_tracked.season,0) AS trackedseason, + COALESCE(shows_tracked.season,0), COALESCE(shows_tracked.episode,0) AS trackedepisode - FROM shows LEFT JOIN shows_tracked ON shows.id=shows_tracked.show_id AND shows_tracked.user_id=$2 + FROM shows LEFT JOIN shows_tracked ON shows.id=shows_tracked.show_imdb_id AND shows_tracked.user_id=$2 WHERE shows.imdb_id=$1;` getShowWithUserQueryByID = ` SELECT shows.id, - shows.imdb_id AS imdbid, + shows.imdb_id, shows.title, shows.rating, shows.plot, - shows.tvdb_id AS tvdbid, + shows.tvdb_id, shows.year, - shows.first_aired AS firstaired, + shows.first_aired, shows.created_at, shows.updated_at, - COALESCE(shows_tracked.season,0) AS trackedseason, - COALESCE(shows_tracked.episode,0) AS trackedepisode - FROM shows LEFT JOIN shows_tracked ON shows.id=shows_tracked.show_id AND shows_tracked.user_id=$2 + COALESCE(shows_tracked.season,0), + COALESCE(shows_tracked.episode,0) + FROM shows LEFT JOIN shows_tracked ON shows.id=shows_tracked.show_imdb_id AND shows_tracked.user_id=$2 WHERE shows.id=$1;` ) @@ -91,29 +83,85 @@ type Show struct { Episodes []*Episode TrackedSeason int TrackedEpisode int + BannerURL string `json:"banner_url"` + FanartURL string `json:"fanart_url"` + PosterURL string `json:"poster_url"` +} + +// ShowDB represents the Show in the DB +type ShowDB struct { + ID string `db:"id"` + ImdbID string `db:"imdb_id"` + TvdbID int `db:"tvdb_id"` + 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"` + // URL string `json:"-"` +} + +// NewShowDB returns a Show ready to be put in DB from a +// Show +func NewShowDB(s *Show) ShowDB { + return ShowDB{ + ID: s.ID, + ImdbID: s.ImdbID, + Title: s.Title, + Rating: s.Rating, + Plot: s.Plot, + TvdbID: s.TvdbID, + Year: s.Year, + FirstAired: *s.FirstAired, + Created: s.Created, + Updated: s.Updated, + } +} + +// 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 } // New returns a new Show with a polochon ShowConfig -func New(conf polochon.ShowConfig) *Show { - return &Show{Show: polochon.Show{ShowConfig: conf}} +func New(imdbID string) *Show { + return &Show{ + Show: polochon.Show{ + ImdbID: imdbID, + }, + } } // Get returns show details in database from id or imdbid or an error -func (s *Show) Get(db *sqlx.DB) error { +func (s *Show) Get(env *web.Env) error { + var sDB ShowDB var err error if s.ID != "" { - err = db.QueryRowx(getShowQueryByID, s.ID).StructScan(s) + err = env.Database.QueryRowx(getShowQueryByID, s.ID).StructScan(&sDB) } else if s.ImdbID != "" { - err = db.QueryRowx(getShowQueryByImdbID, s.ImdbID).StructScan(s) + err = env.Database.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 { - if err.Error() == "sql: no rows in result set" { - return ErrNotFound - } return err } + + // Set the poster url + s.PosterURL = s.GetPosterURL(env) + + s.FillFromDB(&sDB) return nil } @@ -139,36 +187,97 @@ func (s *Show) GetAsUser(db *sqlx.DB, user *users.User) error { // 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(db *sqlx.DB, log *logrus.Entry) error { - var err error - err = s.Get(db) - if err == nil { - // found ok - return nil +func (s *Show) GetDetails(env *web.Env, force bool) 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 } - if err != ErrNotFound { + + var err error + err = s.Get(env) + switch err { + case nil: + log.Debug("show found in database") + if !force { + log.Debug("returning show from db") + return nil + } + case sql.ErrNoRows: + log.Debug("show not found in database") + default: // Unexpected error return err } - // so we got ErrNotFound so GetDetails from a detailer - err = s.Show.GetDetails(log) + // GetDetail + 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.Add(db) + err = s.Upsert(env.Database) if err != nil { + log.Debug("error while doing show upsert func", err) return err } + + log.Debug("show added in database") + + // Download show images + s.downloadImages(env) + + log.Debug("images downloaded") + + // Set the poster url + s.PosterURL = s.GetPosterURL(env) + return nil } +// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded +func (s *Show) GetPosterURL(env *web.Env) string { + // Check if the movie image exists + if _, err := os.Stat(s.imgURL(env, "poster")); os.IsNotExist(err) { + // TODO image in the config ? + return "img/noimage.png" + } + return s.imgURL(env, "poster") +} + +// downloadImages will download the show images +func (s *Show) downloadImages(env *web.Env) { + // Download the banner + err := web.Download(s.Show.Banner, s.imgURL(env, "banner")) + if err != nil { + env.Log.Errorf("failed to dowload banner: %s", err) + } + err = web.Download(s.Show.Fanart, s.imgURL(env, "fanart")) + if err != nil { + env.Log.Errorf("failed to dowload fanart: %s", err) + } + err = web.Download(s.Show.Poster, s.imgURL(env, "poster")) + if err != nil { + env.Log.Errorf("failed to dowload poster: %s", err) + } +} + +// imgFile returns the image location on disk +func (s *Show) imgURL(env *web.Env, imgType string) string { + fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) + return filepath.Join(env.Config.PublicDir, fileURL) +} + // GetDetailsAsUser like GetDetails but with User context func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry) error { var err error @@ -191,7 +300,7 @@ func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe}) } - err = s.Add(db) + err = s.Upsert(db) if err != nil { return err } @@ -207,10 +316,11 @@ func (s *Show) IsTracked() bool { return false } -// Add a show in the database -func (s *Show) Add(db *sqlx.DB) error { +// Upsert a show in the database +func (s *Show) Upsert(db *sqlx.DB) error { + sDB := NewShowDB(s) var id string - r, err := db.NamedQuery(addShowQuery, s) + r, err := db.NamedQuery(upsertShowQuery, sDB) if err != nil { return err } @@ -220,8 +330,8 @@ func (s *Show) Add(db *sqlx.DB) error { s.ID = id for _, e := range s.Episodes { - e.ShowID = s.ID - err = e.Add(db) + e.ShowImdbID = s.ImdbID + err = e.Upsert(db) if err != nil { return err } @@ -231,7 +341,7 @@ func (s *Show) Add(db *sqlx.DB) error { // Delete show from database func (s *Show) Delete(db *sqlx.DB) error { - r, err := db.Exec(deleteShowQuery, s.ID) + r, err := db.Exec(deleteShowQueryByID, s.ID) if err != nil { return err } @@ -243,35 +353,37 @@ func (s *Show) Delete(db *sqlx.DB) error { } // GetEpisodes from database -func (s *Show) GetEpisodes(db *sqlx.DB) error { - // When retrive episode's info from database populate the s.Episodes member - // and not s.Show.Episodes - s.Episodes = []*Episode{} - err := db.Select(&s.Episodes, getEpisodesQuery, s.ID) +func (s *Show) GetEpisodes(env *web.Env) error { + // We retrive episode's info from database populate the s.Episodes member + var episodesDB = []*EpisodeDB{} + err := env.Database.Select(&episodesDB, getEpisodesQuery, s.ImdbID) if err != nil { return err } + if len(episodesDB) == 0 { + return nil + } + + for _, episodeDB := range episodesDB { + episode := NewEpisode() + episode.FillFromDB(episodeDB) + s.Episodes = append(s.Episodes, episode) + err = episode.GetTorrents(env, false) + if err != nil { + env.Log.Debugf("error while getting episode torrent: %q", err) + } + } return nil } -// Episode represents an episode -type Episode struct { - sqly.BaseModel - polochon.ShowEpisode - ShowID string -} - -// Add episode to the database -func (e *Episode) Add(db *sqlx.DB) error { - var id string - r, err := db.NamedQuery(addEpisodeQuery, e) - if err != nil { - return err +// 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) + } } - for r.Next() { - r.Scan(&id) - } - e.ID = id return nil } diff --git a/src/internal/shows/shows_test.go b/src/internal/shows/shows_test.go index 7c411dd..e3e9e71 100644 --- a/src/internal/shows/shows_test.go +++ b/src/internal/shows/shows_test.go @@ -140,7 +140,7 @@ func TestTrackedShow(t *testing.T) { }, } - err := show.Add(db) + err := show.Upsert(db) if err != nil { t.Fatal(err) } diff --git a/src/internal/torrents/episode_torrents.go b/src/internal/torrents/episode_torrents.go new file mode 100644 index 0000000..486ac02 --- /dev/null +++ b/src/internal/torrents/episode_torrents.go @@ -0,0 +1,160 @@ +package torrents + +import ( + "database/sql" + "fmt" + "time" + + "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" + + "github.com/jmoiron/sqlx" + polochon "github.com/odwrtw/polochon/lib" +) + +const ( + upsertEpisodeTorrentQuery = ` + INSERT INTO episode_torrents (imdb_id, url, source, quality, upload_user, + season, episode, seeders, leechers) + VALUES (:imdb_id, :url, :source, :quality, :upload_user, :season, :episode, + :seeders, :leechers) + ON CONFLICT (imdb_id, season, episode, quality, source) + DO UPDATE SET imdb_id=:imdb_id, url=:url, source=:source, quality=:quality, + upload_user=:upload_user, season=:season, episode=:episode, + seeders=:seeders, leechers=:leechers + RETURNING id;` + + getEpisodeTorrentQuery = ` + SELECT * + FROM episode_torrents WHERE imdb_id=$1 AND season=$2 AND episode=$3;` + + getEpisodeTorrentQueryByID = ` + SELECT * + FROM episode_torrents WHERE id=$1;` + + deleteEpisodeTorrentQuery = `DELETE FROM movie_torrents WHERE id=$1;` +) + +// EpisodeTorrent represents an episode torrent +type EpisodeTorrent struct { + sqly.BaseModel + polochon.Torrent + ShowImdbID string `json:"show_imdb_id"` + Season int `json:"season"` + Episode int `json:"episode"` +} + +// EpisodeTorrentDB represents the EpisodeTorrent in the DB +type EpisodeTorrentDB struct { + ID string `db:"id"` + ImdbID string `db:"imdb_id"` + URL string `db:"url"` + Source string `db:"source"` + Quality string `db:"quality"` + UploadUser string `db:"upload_user"` + Season int `db:"season"` + Episode int `db:"episode"` + Seeders int `db:"seeders"` + Leechers int `db:"leechers"` + Created time.Time `db:"created_at"` + Updated time.Time `db:"updated_at"` +} + +// NewEpisodeFromPolochon returns a new EpisodeTorrent from a polochon.ShowEpisode +func NewEpisodeFromPolochon(se polochon.ShowEpisode, poTo polochon.Torrent) *EpisodeTorrent { + return &EpisodeTorrent{ + ShowImdbID: se.ShowImdbID, + Season: se.Season, + Episode: se.Episode, + Torrent: poTo, + } +} + +// NewEpisode returns a new EpisodeTorrent +func NewEpisode() *EpisodeTorrent { + return &EpisodeTorrent{} +} + +// GetEpisodeTorrents returns show details in database from id or imdbid or an error +func GetEpisodeTorrents(db *sqlx.DB, imdbID string, season, episode int) ([]*EpisodeTorrent, error) { + var torrentsDB = []*EpisodeTorrentDB{} + err := db.Select(&torrentsDB, getEpisodeTorrentQuery, imdbID, season, episode) + if err != nil { + return nil, err + } + + if len(torrentsDB) == 0 { + return nil, sql.ErrNoRows + } + + var torrents []*EpisodeTorrent + for _, torrentDB := range torrentsDB { + episode := NewEpisode() + episode.FillFromDB(torrentDB) + torrents = append(torrents, episode) + } + + return torrents, nil +} + +// Delete episode from database +func (e *EpisodeTorrent) Delete(db *sqlx.DB) error { + r, err := db.Exec(deleteEpisodeTorrentQuery, e.ID) + if err != nil { + return err + } + count, _ := r.RowsAffected() + if count != 1 { + return fmt.Errorf("Unexpected number of row deleted: %d", count) + } + return nil +} + +// FillFromDB will fill a MovieTorrent from a MovieTorrentDB extracted from the DB +func (e *EpisodeTorrent) FillFromDB(eDB *EpisodeTorrentDB) { + q, _ := polochon.StringToQuality(eDB.Quality) + e.ShowImdbID = eDB.ImdbID + e.Created = eDB.Created + e.Updated = eDB.Updated + e.ID = eDB.ID + e.URL = eDB.URL + e.Source = eDB.Source + e.Quality = *q + e.UploadUser = eDB.UploadUser + e.Seeders = eDB.Seeders + e.Leechers = eDB.Leechers +} + +// Upsert an episode torrent in the database +func (e *EpisodeTorrent) Upsert(db *sqlx.DB) error { + eDB := NewEpisodeTorrentDB(e) + var id string + r, err := db.NamedQuery(upsertEpisodeTorrentQuery, eDB) + if err != nil { + return err + } + for r.Next() { + r.Scan(&id) + } + e.ID = id + + return nil +} + +// NewEpisodeTorrentDB returns an EpisodeTorrent ready to be put in DB from a +// EspisodeTorrent +func NewEpisodeTorrentDB(e *EpisodeTorrent) EpisodeTorrentDB { + return EpisodeTorrentDB{ + ID: e.ID, + ImdbID: e.ShowImdbID, + Season: e.Season, + Episode: e.Episode, + URL: e.URL, + Source: e.Source, + Quality: string(e.Quality), + UploadUser: e.UploadUser, + Seeders: e.Seeders, + Leechers: e.Leechers, + Created: e.Created, + Updated: e.Updated, + } +} diff --git a/src/internal/torrents/torrents.go b/src/internal/torrents/movie_torrents.go similarity index 94% rename from src/internal/torrents/torrents.go rename to src/internal/torrents/movie_torrents.go index 548c430..a872ff1 100644 --- a/src/internal/torrents/torrents.go +++ b/src/internal/torrents/movie_torrents.go @@ -54,15 +54,6 @@ type MovieTorrentDB struct { Updated time.Time `db:"updated_at"` } -// 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"` -} - // NewMovieFromImDB returns a new MovieTorrent with an ImDB id func NewMovieFromImDB(imdbID string) *MovieTorrent { return &MovieTorrent{ diff --git a/src/main.go b/src/main.go index 544635d..e55496c 100644 --- a/src/main.go +++ b/src/main.go @@ -5,6 +5,7 @@ import ( "os" "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/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" @@ -79,6 +80,12 @@ func main() { env.Handle("/movies/{id:tt[0-9]+}/get_details", movies.GetDetailsHandler).WithRole(users.UserRole) env.Handle("/movies/explore", extmedias.Explore) env.Handle("/movies/refresh", extmedias.Refresh) + env.Handle("/movies/search", movies.SearchMovie) + + // env.Handle("/shows/polochon", shows.FromPolochon).WithRole(users.UserRole) + env.Handle("/shows/{id:tt[0-9]+}", shows.GetDetailsHandler) + env.Handle("/shows/refresh", extmedias.RefreshShows) + env.Handle("/shows/explore", extmedias.ExploreShows) n := negroni.Classic() n.Use(authMiddleware)