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,
@ -57,19 +58,20 @@ CREATE TABLE episodes (
plot text NOT NULL, plot text NOT NULL,
runtime smallint NOT NULL, runtime smallint NOT NULL,
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 {
var err error log := env.Log.WithFields(logrus.Fields{
err = s.Get(db) "imdb_id": s.ImdbID,
if err == nil { "function": "shows.GetDetails",
// found ok })
return nil 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 // 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 if err != nil {
} env.Log.Errorf("error while getting episode torrent: %s", err)
}
// 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
} }
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)))