Add shows to the party

This commit is contained in:
Lucas BEE 2016-12-16 18:35:05 +00:00
parent cd6a6f26fd
commit 828c34830e
12 changed files with 733 additions and 110 deletions

View File

@ -42,13 +42,14 @@ CREATE TABLE shows (
first_aired timestamp with time zone, first_aired timestamp with time zone,
LIKE base INCLUDING DEFAULTS 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 TRIGGER update_shows_updated_at BEFORE UPDATE ON shows FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TABLE episodes ( CREATE TABLE episodes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
imdb_id text NOT NULL, 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, title text NOT NULL,
season smallint NOT NULL, season smallint NOT NULL,
episode smallint NOT NULL, episode smallint NOT NULL,
@ -59,17 +60,18 @@ CREATE TABLE episodes (
rating real 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 TRIGGER update_episodes_updated_at BEFORE UPDATE ON episodes FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
CREATE TABLE shows_tracked ( 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, user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
season smallint NOT NULL, season smallint NOT NULL,
episode 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 INDEX ON shows_tracked (user_id);
CREATE TABLE movies ( CREATE TABLE movies (

View File

@ -1,6 +1,6 @@
CREATE TYPE media_type AS ENUM ('movie', 'show'); CREATE TYPE media_type AS ENUM ('movie', 'show');
CREATE TYPE media_category AS ENUM ('trending', 'popular', 'anticipated', 'box_office'); 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 ( CREATE TABLE external_medias (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id uuid PRIMARY KEY DEFAULT gen_random_uuid(),

View File

@ -23,7 +23,7 @@ INHERITS (torrents_abstract);
CREATE INDEX ON movie_torrents (imdb_id); CREATE INDEX ON movie_torrents (imdb_id);
CREATE UNIQUE INDEX ON movie_torrents (imdb_id, source, quality); CREATE UNIQUE INDEX ON movie_torrents (imdb_id, source, quality);
CREATE INDEX ON episode_torrents (imdb_id); 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 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(); CREATE TRIGGER update_movie_torrents_updated_at BEFORE UPDATE ON movie_torrents FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();

View File

@ -5,7 +5,9 @@ import (
"os" "os"
polochon "github.com/odwrtw/polochon/lib" polochon "github.com/odwrtw/polochon/lib"
"github.com/odwrtw/polochon/modules/eztv"
"github.com/odwrtw/polochon/modules/tmdb" "github.com/odwrtw/polochon/modules/tmdb"
"github.com/odwrtw/polochon/modules/tvdb"
"github.com/odwrtw/polochon/modules/yts" "github.com/odwrtw/polochon/modules/yts"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -22,6 +24,8 @@ type Config struct {
TmdbAPIKey string `yaml:"tmdb_api_key"` TmdbAPIKey string `yaml:"tmdb_api_key"`
MovieDetailers []polochon.Detailer MovieDetailers []polochon.Detailer
MovieTorrenters []polochon.Torrenter MovieTorrenters []polochon.Torrenter
ShowDetailers []polochon.Detailer
ShowTorrenters []polochon.Torrenter
} }
type AuthorizerConfig struct { type AuthorizerConfig struct {
@ -67,5 +71,21 @@ func Load(path string) (*Config, error) {
} }
cf.MovieTorrenters = append(cf.MovieTorrenters, d) cf.MovieTorrenters = append(cf.MovieTorrenters, d)
// 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 return cf, nil
} }

View File

@ -6,11 +6,13 @@ import (
"net/http" "net/http"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
polochon "github.com/odwrtw/polochon/lib" polochon "github.com/odwrtw/polochon/lib"
eztvExplorer "github.com/odwrtw/polochon/modules/eztv"
traktExplorer "github.com/odwrtw/polochon/modules/trakttv" traktExplorer "github.com/odwrtw/polochon/modules/trakttv"
ytsExplorer "github.com/odwrtw/polochon/modules/yts" 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) media, err := Get(env.Database, mediaType, source, category)
switch err { switch err {
case nil: case nil:
log.Debug("medias found in database") log.Debugf("%s medias found in database", mediaType)
if !force { if !force {
log.Debug("returning medias from db") log.Debug("returning medias from db")
return media.IDs, nil return media.IDs, nil
} }
case sql.ErrNoRows: case sql.ErrNoRows:
log.Debug("medias not found in database") log.Debugf("%s medias not found in database", mediaType)
default: default:
// Unexpected error // Unexpected error
return nil, err return nil, err
@ -46,9 +48,25 @@ func GetMediaIDs(env *web.Env, mediaType string, source string, category string,
} }
var ids []string var ids []string
movies, err := explorer.GetMovieList(polochon.ExploreByRate, log) if mediaType == "movie" {
for _, movie := range movies { medias, err := explorer.GetMovieList(polochon.ExploreByRate, log)
ids = append(ids, movie.ImdbID) 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) log.Debugf("got %d medias from %s", len(ids), source)
@ -71,8 +89,8 @@ func GetMediaIDs(env *web.Env, mediaType string, source string, category string,
return ids, nil return ids, nil
} }
// GetMedias get some movies // GetMovies get some movies
func GetMedias(env *web.Env, source string, category string, force bool) ([]*movies.Movie, error) { func GetMovies(env *web.Env, source string, category string, force bool) ([]*movies.Movie, error) {
movieIds, err := GetMediaIDs(env, "movie", source, category, force) movieIds, err := GetMediaIDs(env, "movie", source, category, force)
if err != nil { if err != nil {
return nil, err return nil, err
@ -83,12 +101,12 @@ func GetMedias(env *web.Env, source string, category string, force bool) ([]*mov
movie := movies.New(id) movie := movies.New(id)
err := movie.GetDetails(env, force) err := movie.GetDetails(env, force)
if err != nil { if err != nil {
env.Log.Error(err) env.Log.Errorf("error while getting movie details : %s", err)
continue continue
} }
err = movie.GetTorrents(env, force) err = movie.GetTorrents(env, force)
if err != nil { if err != nil {
env.Log.Error(err) env.Log.Errorf("error while getting movie torrents : %s", err)
continue continue
} }
movieList = append(movieList, movie) movieList = append(movieList, movie)
@ -96,6 +114,31 @@ func GetMedias(env *web.Env, source string, category string, force bool) ([]*mov
return movieList, nil 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 // Explore will explore some movies
func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error { func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
err := r.ParseForm() err := r.ParseForm()
@ -116,7 +159,7 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
} }
// Get the medias without trying to refresh them // Get the medias without trying to refresh them
movies, err := GetMedias(env, source, category, false) movies, err := GetMovies(env, source, category, false)
if err != nil { if err != nil {
return err return err
} }
@ -124,12 +167,45 @@ func Explore(env *web.Env, w http.ResponseWriter, r *http.Request) error {
return env.RenderJSON(w, movies) 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 // MediaSources represents the implemented media sources
var MediaSources = []string{ var MediaSources = []string{
"trakttv", "trakttv",
"yts", "yts",
} }
// ShowMediaSources represents the implemented media sources for shows
var ShowMediaSources = []string{
"eztv",
}
// Refresh will refresh the movie list // Refresh will refresh the movie list
func Refresh(env *web.Env, w http.ResponseWriter, r *http.Request) error { func Refresh(env *web.Env, w http.ResponseWriter, r *http.Request) error {
env.Log.Debugf("refreshing infos ...") env.Log.Debugf("refreshing infos ...")
@ -147,7 +223,33 @@ func Refresh(env *web.Env, w http.ResponseWriter, r *http.Request) error {
for _, source := range MediaSources { for _, source := range MediaSources {
env.Log.Debugf("refreshing %s", source) env.Log.Debugf("refreshing %s", source)
// GetMedias and refresh them // 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 { if err != nil {
return err return err
} }
@ -165,6 +267,8 @@ func NewExplorer(env *web.Env, source string) (polochon.Explorer, error) {
}) })
case "yts": case "yts":
return ytsExplorer.NewExplorer() return ytsExplorer.NewExplorer()
case "eztv":
return eztvExplorer.NewExplorer()
default: default:
return nil, fmt.Errorf("unknown explorer") return nil, fmt.Errorf("unknown explorer")
} }

View File

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

View File

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

View File

@ -1,10 +1,15 @@
package shows package shows
import ( import (
"database/sql"
"fmt" "fmt"
"os"
"path/filepath"
"time"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/sqly" "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/users"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/web"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -12,70 +17,59 @@ import (
) )
const ( const (
addShowQuery = ` upsertShowQuery = `
INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired) 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 = ` getShowQueryByImdbID = `
SELECT SELECT *
id, imdb_id AS imdbid,
title, rating, plot,
tvdb_id AS tvdbid,
year, first_aired AS firstaired,
created_at, updated_at
FROM shows WHERE imdb_id=$1;` FROM shows WHERE imdb_id=$1;`
getShowQueryByID = ` getShowQueryByID = `
SELECT SELECT *
id, imdb_id AS imdbid,
title, rating, plot,
tvdb_id AS tvdbid,
year, first_aired AS firstaired,
created_at, updated_at
FROM shows WHERE id=$1;` FROM shows WHERE id=$1;`
deleteShowQuery = `DELETE FROM shows WHERE id=$1;` deleteShowQueryByID = `DELETE FROM shows WHERE id=$1;`
addEpisodeQuery = ` deleteShowQueryByImdbID = `DELETE FROM shows WHERE imdb_id=$1;`
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;`
getShowWithUserQueryByImdbID = ` getShowWithUserQueryByImdbID = `
SELECT SELECT
shows.id, shows.id,
shows.imdb_id AS imdbid, shows.imdb_id,
shows.title, shows.title,
shows.rating, shows.rating,
shows.plot, shows.plot,
shows.tvdb_id AS tvdbid, shows.tvdb_id,
shows.year, shows.year,
shows.first_aired AS firstaired, shows.first_aired,
shows.created_at, shows.created_at,
shows.updated_at, shows.updated_at,
COALESCE(shows_tracked.season,0) AS trackedseason, COALESCE(shows_tracked.season,0),
COALESCE(shows_tracked.episode,0) AS trackedepisode 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;` WHERE shows.imdb_id=$1;`
getShowWithUserQueryByID = ` getShowWithUserQueryByID = `
SELECT SELECT
shows.id, shows.id,
shows.imdb_id AS imdbid, shows.imdb_id,
shows.title, shows.title,
shows.rating, shows.rating,
shows.plot, shows.plot,
shows.tvdb_id AS tvdbid, shows.tvdb_id,
shows.year, shows.year,
shows.first_aired AS firstaired, shows.first_aired,
shows.created_at, shows.created_at,
shows.updated_at, shows.updated_at,
COALESCE(shows_tracked.season,0) AS trackedseason, COALESCE(shows_tracked.season,0),
COALESCE(shows_tracked.episode,0) AS trackedepisode COALESCE(shows_tracked.episode,0)
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.id=$1;` WHERE shows.id=$1;`
) )
@ -91,29 +85,85 @@ type Show struct {
Episodes []*Episode Episodes []*Episode
TrackedSeason int TrackedSeason int
TrackedEpisode 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 // New returns a new Show with a polochon ShowConfig
func New(conf polochon.ShowConfig) *Show { func New(imdbID string) *Show {
return &Show{Show: polochon.Show{ShowConfig: conf}} return &Show{
Show: polochon.Show{
ImdbID: imdbID,
},
}
} }
// Get returns show details in database from id or imdbid or an error // 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 var err error
if s.ID != "" { if s.ID != "" {
err = db.QueryRowx(getShowQueryByID, s.ID).StructScan(s) err = env.Database.QueryRowx(getShowQueryByID, s.ID).StructScan(&sDB)
} else if s.ImdbID != "" { } else if s.ImdbID != "" {
err = db.QueryRowx(getShowQueryByImdbID, s.ImdbID).StructScan(s) err = env.Database.QueryRowx(getShowQueryByImdbID, s.ImdbID).StructScan(&sDB)
} else { } else {
err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID") err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID")
} }
if err != nil { if err != nil {
if err.Error() == "sql: no rows in result set" {
return ErrNotFound
}
return err return err
} }
// Set the poster url
s.PosterURL = s.GetPosterURL(env)
s.FillFromDB(&sDB)
return nil return nil
} }
@ -139,36 +189,97 @@ func (s *Show) GetAsUser(db *sqlx.DB, user *users.User) error {
// GetDetails retrieves details for the show, first try to // GetDetails retrieves details for the show, first try to
// get info from db, if not exists, use polochon.Detailer // get info from db, if not exists, use polochon.Detailer
// and save informations in the database for future use // and save informations in the database for future use
func (s *Show) GetDetails(db *sqlx.DB, log *logrus.Entry) error { 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
}
var err error var err error
err = s.Get(db) err = s.Get(env)
if err == nil { switch err {
// found ok case nil:
log.Debug("show found in database")
if !force {
log.Debug("returning show from db")
return nil return nil
} }
if err != ErrNotFound { case sql.ErrNoRows:
log.Debug("show not found in database")
default:
// Unexpected error // Unexpected error
return err return err
} }
// so we got ErrNotFound so GetDetails from a detailer // GetDetail
err = s.Show.GetDetails(log) err = s.Show.GetDetails(env.Log)
if err != nil { if err != nil {
return err return err
} }
log.Debug("got details from detailers")
s.Episodes = []*Episode{} s.Episodes = []*Episode{}
for _, pe := range s.Show.Episodes { for _, pe := range s.Show.Episodes {
s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe}) s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe})
} }
err = s.Add(db) err = s.Upsert(env.Database)
if err != nil { if err != nil {
log.Debug("error while doing show upsert func", err)
return err return err
} }
log.Debug("show added in database")
// Download show images
s.downloadImages(env)
log.Debug("images downloaded")
// Set the poster url
s.PosterURL = s.GetPosterURL(env)
return nil return nil
} }
// GetPosterURL returns the image URL or the default image if the poster is not yet downloaded
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 // GetDetailsAsUser like GetDetails but with User context
func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry) error { func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry) error {
var err error var err error
@ -191,7 +302,7 @@ func (s *Show) GetDetailsAsUser(db *sqlx.DB, user *users.User, log *logrus.Entry
s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe}) s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe})
} }
err = s.Add(db) err = s.Upsert(db)
if err != nil { if err != nil {
return err return err
} }
@ -207,10 +318,11 @@ func (s *Show) IsTracked() bool {
return false return false
} }
// Add a show in the database // Upsert a show in the database
func (s *Show) Add(db *sqlx.DB) error { func (s *Show) Upsert(db *sqlx.DB) error {
sDB := NewShowDB(s)
var id string var id string
r, err := db.NamedQuery(addShowQuery, s) r, err := db.NamedQuery(upsertShowQuery, sDB)
if err != nil { if err != nil {
return err return err
} }
@ -220,8 +332,8 @@ func (s *Show) Add(db *sqlx.DB) error {
s.ID = id s.ID = id
for _, e := range s.Episodes { for _, e := range s.Episodes {
e.ShowID = s.ID e.ShowImdbID = s.ImdbID
err = e.Add(db) err = e.Upsert(db)
if err != nil { if err != nil {
return err return err
} }
@ -231,7 +343,7 @@ func (s *Show) Add(db *sqlx.DB) error {
// Delete show from database // Delete show from database
func (s *Show) Delete(db *sqlx.DB) error { 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 { if err != nil {
return err return err
} }
@ -242,36 +354,60 @@ func (s *Show) Delete(db *sqlx.DB) error {
return nil 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
// }
// GetEpisodes from database // GetEpisodes from database
func (s *Show) GetEpisodes(db *sqlx.DB) error { func (s *Show) GetEpisodes(env *web.Env) error {
// When retrive episode's info from database populate the s.Episodes member // We retrive episode's info from database populate the s.Episodes member
// and not s.Show.Episodes var episodesDB = []*EpisodeDB{}
s.Episodes = []*Episode{} err := env.Database.Select(&episodesDB, getEpisodesQuery, s.ImdbID)
err := db.Select(&s.Episodes, getEpisodesQuery, s.ID)
if err != nil { if err != nil {
return err 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 return nil
} }
// Episode represents an episode // GetTorrents from the database or fetch them if needed
type Episode struct { func (s *Show) GetTorrents(env *web.Env, force bool) error {
sqly.BaseModel for _, e := range s.Episodes {
polochon.ShowEpisode err := e.GetTorrents(env, force)
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 { if err != nil {
return err env.Log.Errorf("error while getting episode torrent: %s", err)
} }
for r.Next() {
r.Scan(&id)
} }
e.ID = id
return nil return nil
} }

View File

@ -140,7 +140,7 @@ func TestTrackedShow(t *testing.T) {
}, },
} }
err := show.Add(db) err := show.Upsert(db)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

@ -54,15 +54,6 @@ type MovieTorrentDB struct {
Updated time.Time `db:"updated_at"` 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 // NewMovieFromImDB returns a new MovieTorrent with an ImDB id
func NewMovieFromImDB(imdbID string) *MovieTorrent { func NewMovieFromImDB(imdbID string) *MovieTorrent {
return &MovieTorrent{ return &MovieTorrent{

View File

@ -5,6 +5,7 @@ import (
"os" "os"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/movies"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/shows"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/auth"
"gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config" "gitlab.quimbo.fr/odwrtw/canape-sql/src/internal/config"
@ -80,6 +81,11 @@ func main() {
env.Handle("/movies/explore", extmedias.Explore) env.Handle("/movies/explore", extmedias.Explore)
env.Handle("/movies/refresh", extmedias.Refresh) env.Handle("/movies/refresh", extmedias.Refresh)
// 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 := negroni.Classic()
n.Use(authMiddleware) n.Use(authMiddleware)
n.Use(negroni.NewStatic(http.Dir(cf.PublicDir))) n.Use(negroni.NewStatic(http.Dir(cf.PublicDir)))