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" "github.com/odwrtw/polochon/lib" ) const ( upsertShowQuery = ` INSERT INTO shows (imdb_id, title, rating, plot, tvdb_id, year, first_aired) VALUES (:imdb_id, :title, :rating, :plot, :tvdb_id, :year, :first_aired) ON CONFLICT (imdb_id) DO UPDATE SET imdb_id=:imdb_id, title=:title, rating=:rating, plot=:plot, tvdb_id=:tvdb_id, year=:year, first_aired=:first_aired RETURNING id;` getShowQueryByImdbID = ` SELECT * FROM shows WHERE imdb_id=$1;` getShowQueryByID = ` SELECT * FROM shows WHERE id=$1;` deleteShowQueryByID = `DELETE FROM shows WHERE id=$1;` getShowWithUserQueryByImdbID = ` SELECT shows.id, shows.imdb_id, shows.title, shows.rating, shows.plot, shows.tvdb_id, shows.year, shows.first_aired, shows_tracked.season, shows_tracked.episode, shows.updated_at, shows.created_at FROM shows LEFT JOIN shows_tracked ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$2 WHERE shows.imdb_id=$1;` getShowWithUserQueryByID = ` SELECT shows.id, shows.imdb_id, shows.title, shows.rating, shows.plot, shows.tvdb_id, shows.year, shows.first_aired, shows_tracked.season, shows_tracked.episode, shows.updated_at, shows.created_at FROM shows LEFT JOIN shows_tracked ON shows.imdb_id=shows_tracked.imdb_id AND shows_tracked.user_id=$2 WHERE shows.id=$1;` ) var ( // ErrNotFound error returned when show not found in database ErrNotFound = fmt.Errorf("Not found") ) // Show represents a show type Show struct { sqly.BaseModel polochon.Show Episodes []*Episode `json:"episodes"` TrackedSeason *int `json:"tracked_season"` TrackedEpisode *int `json:"tracked_episode"` BannerURL string `json:"banner_url"` FanartURL string `json:"fanart_url"` PosterURL string `json:"poster_url"` } // ShowDB represents the Show in the DB type ShowDB struct { ID string `db:"id"` ImdbID string `db:"imdb_id"` TvdbID int `db:"tvdb_id"` TrackedSeason *int `db:"season"` TrackedEpisode *int `db:"episode"` Title string `db:"title"` Rating float32 `db:"rating"` Plot string `db:"plot"` Year int `db:"year"` FirstAired time.Time `db:"first_aired"` Created time.Time `db:"created_at"` Updated time.Time `db:"updated_at"` } // NewShowDB returns a Show ready to be put in DB from a // Show func NewShowDB(s *Show) ShowDB { sDB := ShowDB{ ID: s.ID, ImdbID: s.ImdbID, Title: s.Title, Rating: s.Rating, Plot: s.Plot, TvdbID: s.TvdbID, Year: s.Year, Created: s.Created, Updated: s.Updated, } if s.FirstAired != nil { sDB.FirstAired = *s.FirstAired } return sDB } // FillFromDB returns a Show from a ShowDB extracted from the DB func (s *Show) FillFromDB(sDB *ShowDB) { s.ID = sDB.ID s.ImdbID = sDB.ImdbID s.Title = sDB.Title s.Rating = sDB.Rating s.Plot = sDB.Plot s.TvdbID = sDB.TvdbID s.Year = sDB.Year s.FirstAired = &sDB.FirstAired s.Created = sDB.Created s.Updated = sDB.Updated s.TrackedSeason = sDB.TrackedSeason s.TrackedEpisode = sDB.TrackedEpisode } // New returns a new Show with a polochon ShowConfig func New(imdbID string) *Show { return &Show{ Show: polochon.Show{ ImdbID: imdbID, }, } } // Get returns a show with user info like tracked func (s *Show) Get(env *web.Env, user *users.User) error { var err error var sDB ShowDB if s.ID != "" { err = env.Database.QueryRowx(getShowWithUserQueryByID, s.ID, user.ID).StructScan(&sDB) } else if s.ImdbID != "" { err = env.Database.QueryRowx(getShowWithUserQueryByImdbID, s.ImdbID, user.ID).StructScan(&sDB) } else { err = fmt.Errorf("Can't get show details, you have to specify an ID or ImdbID") } if err != nil { return err } // Set the poster url s.PosterURL = s.GetPosterURL(env) s.FillFromDB(&sDB) return nil } // GetDetails retrieves details for the show, first try to // get info from db, if not exists, use polochon.Detailer // and save informations in the database for future use func (s *Show) GetDetails(env *web.Env, user *users.User, force bool) error { 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 err = s.Get(env, user) switch err { case nil: log.Debug("show found in database") case sql.ErrNoRows: log.Debug("show not found in database") default: // Unexpected error return err } // If force is not specified, don't go further if !force { // Will return ErrNoRows if the show wasn't found return err } // GetDetail err = s.Show.GetDetails(env.Log) if err != nil { return err } log.Debug("got details from detailers") s.Episodes = []*Episode{} for _, pe := range s.Show.Episodes { s.Episodes = append(s.Episodes, &Episode{ShowEpisode: *pe}) } err = s.Upsert(env.Database) if err != nil { log.Debug("error while doing show upsert func", err) return err } log.Debug("show added in database") // Download show images s.downloadImages(env) log.Debug("images downloaded") // Set the poster url s.PosterURL = s.GetPosterURL(env) 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 show image exists if _, err := os.Stat(s.imgFile(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.imgFile(env, "banner")) if err != nil { env.Log.Errorf("failed to dowload banner: %s", err) } err = web.Download(s.Show.Fanart, s.imgFile(env, "fanart")) if err != nil { env.Log.Errorf("failed to dowload fanart: %s", err) } err = web.Download(s.Show.Poster, s.imgFile(env, "poster")) if err != nil { env.Log.Errorf("failed to dowload poster: %s", err) } } // imgFile returns the image location on disk func (s *Show) imgFile(env *web.Env, imgType string) string { fileURL := fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) return filepath.Join(env.Config.PublicDir, fileURL) } // imgURL returns the default image url func (s *Show) imgURL(env *web.Env, imgType string) string { return fmt.Sprintf("img/shows/%s-%s.jpg", s.ImdbID, imgType) } // Upsert a show in the database func (s *Show) Upsert(db *sqlx.DB) error { sDB := NewShowDB(s) var id string r, err := db.NamedQuery(upsertShowQuery, sDB) if err != nil { return err } for r.Next() { r.Scan(&id) } s.ID = id for _, e := range s.Episodes { e.ShowImdbID = s.ImdbID err = e.Upsert(db) if err != nil { return err } } return nil } // Delete show from database func (s *Show) Delete(db *sqlx.DB) error { r, err := db.Exec(deleteShowQueryByID, s.ID) if err != nil { return err } count, _ := r.RowsAffected() if count != 1 { return fmt.Errorf("Unexpected number of row deleted: %d", count) } return nil } // GetEpisodes from database func (s *Show) GetEpisodes(env *web.Env, force bool) error { // We retrive episode's info from database populate the s.Episodes member var episodesDB = []*EpisodeDB{} err := env.Database.Select(&episodesDB, getEpisodesQuery, s.ImdbID) if err != nil { return err } if len(episodesDB) == 0 { return nil } for _, episodeDB := range episodesDB { episode := NewEpisode() episode.FillFromDB(episodeDB) s.Episodes = append(s.Episodes, episode) err = episode.GetTorrents(env, force) if err != nil { env.Log.Debugf("error while getting episode torrent: %q", err) } } return nil } // GetEpisode from database func (s *Show) GetEpisodeDetails(env *web.Env, seasonNb, episodeNb int, force bool) (*Episode, error) { log := env.Log.WithFields(logrus.Fields{ "imdb_id": s.ImdbID, "season": seasonNb, "episode": episodeNb, "function": "show.GetEpisodeDetails", }) log.Debugf("getting episode details") e := NewEpisode() e.ShowImdbID = s.ImdbID e.Season = seasonNb e.Episode = episodeNb if len(e.Detailers) == 0 { e.Detailers = env.Config.ShowDetailers } var err error err = e.Get(env) switch err { case nil: log.Debug("episode found in database") case sql.ErrNoRows: log.Debug("episode not found in database") default: // Unexpected error return nil, err } // If force is not specified, don't go further if !force { // Will return ErrNoRows if the episode wasn't found return e, err } // GetDetail of the episode err = e.ShowEpisode.GetDetails(env.Log) if err != nil { return nil, err } // Upsert the episode err = e.Upsert(env.Database) if err != nil { log.Debug("error while doing episode upsert func", err) return nil, err } log.Debug("episode inserted/updated in database") return e, nil } // GetTorrents from the database or fetch them if needed func (s *Show) GetTorrents(env *web.Env, force bool) error { for _, e := range s.Episodes { err := e.GetTorrents(env, force) if err != nil { env.Log.Errorf("error while getting episode torrent: %s", err) } } return nil }