mirror of
https://github.com/simon-ding/polaris.git
synced 2026-03-17 08:50:49 +08:00
refactor: core client to engine
This commit is contained in:
@@ -1,192 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/downloadclients"
|
||||
"polaris/log"
|
||||
"polaris/pkg"
|
||||
"polaris/pkg/buildin"
|
||||
"polaris/pkg/qbittorrent"
|
||||
"polaris/pkg/tmdb"
|
||||
"polaris/pkg/transmission"
|
||||
"polaris/pkg/utils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
func NewClient(db *db.Client, language string) *Client {
|
||||
return &Client{
|
||||
db: db,
|
||||
cron: cron.New(),
|
||||
tasks: make(map[int]*Task, 0),
|
||||
language: language,
|
||||
}
|
||||
}
|
||||
|
||||
type scheduler struct {
|
||||
cron string
|
||||
f func() error
|
||||
}
|
||||
type Client struct {
|
||||
db *db.Client
|
||||
cron *cron.Cron
|
||||
tasks map[int]*Task
|
||||
language string
|
||||
schedulers utils.Map[string, scheduler]
|
||||
}
|
||||
|
||||
func (c *Client) registerCronJob(name string, cron string, f func() error) {
|
||||
c.schedulers.Store(name, scheduler{
|
||||
cron: cron,
|
||||
f: f,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) Init() {
|
||||
go c.reloadTasks()
|
||||
c.addSysCron()
|
||||
go c.checkW500PosterOnStartup()
|
||||
}
|
||||
|
||||
func (c *Client) reloadTasks() {
|
||||
allTasks := c.db.GetRunningHistories()
|
||||
for _, t := range allTasks {
|
||||
dl, err := c.db.GetDownloadClient(t.DownloadClientID)
|
||||
if err != nil {
|
||||
log.Warnf("no download client related: %v", t.SourceTitle)
|
||||
continue
|
||||
}
|
||||
|
||||
if dl.Implementation == downloadclients.ImplementationTransmission {
|
||||
if t.Hash != "" { //优先使用hash
|
||||
to, err := transmission.NewTorrentHash(transmission.Config{
|
||||
URL: dl.URL,
|
||||
User: dl.User,
|
||||
Password: dl.Password,
|
||||
}, t.Hash)
|
||||
if err != nil {
|
||||
log.Warnf("get task error: %v", err)
|
||||
continue
|
||||
}
|
||||
c.tasks[t.ID] = &Task{Torrent: to}
|
||||
} else if t.Link != "" {
|
||||
to, err := transmission.NewTorrent(transmission.Config{
|
||||
URL: dl.URL,
|
||||
User: dl.User,
|
||||
Password: dl.Password,
|
||||
}, t.Link)
|
||||
if err != nil {
|
||||
log.Warnf("get task error: %v", err)
|
||||
continue
|
||||
}
|
||||
c.tasks[t.ID] = &Task{Torrent: to}
|
||||
}
|
||||
} else if dl.Implementation == downloadclients.ImplementationQbittorrent {
|
||||
if t.Hash != "" {
|
||||
to, err := qbittorrent.NewTorrentHash(qbittorrent.Info{
|
||||
URL: dl.URL,
|
||||
User: dl.User,
|
||||
Password: dl.Password,
|
||||
}, t.Hash)
|
||||
if err != nil {
|
||||
log.Warnf("get task error: %v", err)
|
||||
continue
|
||||
}
|
||||
c.tasks[t.ID] = &Task{Torrent: to}
|
||||
|
||||
} else if t.Link != "" {
|
||||
to, err := qbittorrent.NewTorrent(qbittorrent.Info{
|
||||
URL: dl.URL,
|
||||
User: dl.User,
|
||||
Password: dl.Password,
|
||||
}, t.Link)
|
||||
if err != nil {
|
||||
log.Warnf("get task error: %v", err)
|
||||
continue
|
||||
}
|
||||
c.tasks[t.ID] = &Task{Torrent: to}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
log.Infof("------ task reloading done ------")
|
||||
}
|
||||
|
||||
func (c *Client) buildInDownloader() (pkg.Downloader, error) {
|
||||
dir := c.db.GetDownloadDir()
|
||||
return buildin.NewDownloader(dir)
|
||||
}
|
||||
|
||||
func (c *Client) GetDownloadClient() (pkg.Downloader, *ent.DownloadClients, error) {
|
||||
downloaders := c.db.GetAllDonloadClients()
|
||||
for _, d := range downloaders {
|
||||
if !d.Enable {
|
||||
continue
|
||||
}
|
||||
if d.Implementation == downloadclients.ImplementationTransmission {
|
||||
trc, err := transmission.NewClient(transmission.Config{
|
||||
URL: d.URL,
|
||||
User: d.User,
|
||||
Password: d.Password,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("connect to download client error: %v", d.URL)
|
||||
continue
|
||||
}
|
||||
return trc, d, nil
|
||||
|
||||
} else if d.Implementation == downloadclients.ImplementationQbittorrent {
|
||||
qbt, err := qbittorrent.NewClient(d.URL, d.User, d.Password)
|
||||
if err != nil {
|
||||
log.Warnf("connect to download client error: %v", d.URL)
|
||||
continue
|
||||
}
|
||||
return qbt, d, nil
|
||||
} else if d.Implementation == downloadclients.ImplementationBuildin {
|
||||
bin, err := c.buildInDownloader()
|
||||
if err != nil {
|
||||
log.Warnf("connect to download client error: %v", d.URL)
|
||||
continue
|
||||
}
|
||||
return bin, d, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, errors.Errorf("no available download client")
|
||||
}
|
||||
|
||||
func (c *Client) TMDB() (*tmdb.Client, error) {
|
||||
api := c.db.GetTmdbApiKey()
|
||||
if api == "" {
|
||||
return nil, errors.New("TMDB apiKey not set")
|
||||
}
|
||||
proxy := c.db.GetSetting(db.SettingProxy)
|
||||
adult := c.db.GetSetting(db.SettingEnableTmdbAdultContent)
|
||||
return tmdb.NewClient(api, proxy, adult == "true")
|
||||
}
|
||||
|
||||
func (c *Client) MustTMDB() *tmdb.Client {
|
||||
t, err := c.TMDB()
|
||||
if err != nil {
|
||||
log.Panicf("get tmdb: %v", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *Client) RemoveTaskAndTorrent(id int) error {
|
||||
torrent := c.tasks[id]
|
||||
if torrent != nil {
|
||||
if err := torrent.Remove(); err != nil {
|
||||
return errors.Wrap(err, "remove torrent")
|
||||
}
|
||||
delete(c.tasks, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetTasks() map[int]*Task {
|
||||
return c.tasks
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package core
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/importlist"
|
||||
"polaris/ent/media"
|
||||
"polaris/ent/schema"
|
||||
"polaris/log"
|
||||
"polaris/pkg/importlist/plexwatchlist"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
tmdb "github.com/cyruzin/golang-tmdb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) periodicallyUpdateImportlist() error {
|
||||
log.Infof("begin check import list")
|
||||
lists, err := c.db.GetAllImportLists()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get from db")
|
||||
}
|
||||
for _, l := range lists {
|
||||
log.Infof("check import list content for %v", l.Name)
|
||||
if l.Type == importlist.TypePlex {
|
||||
res, err := plexwatchlist.ParsePlexWatchlist(l.URL)
|
||||
if err != nil {
|
||||
log.Errorf("parse plex watchlist: %v", err)
|
||||
continue
|
||||
}
|
||||
for _, item := range res.Items {
|
||||
var tmdbRes *tmdb.FindByID
|
||||
if item.ImdbID != "" {
|
||||
tmdbRes1, err := c.MustTMDB().GetByImdbId(item.ImdbID, c.language)
|
||||
if err != nil {
|
||||
log.Errorf("get by imdb id error: %v", err)
|
||||
continue
|
||||
}
|
||||
tmdbRes = tmdbRes1
|
||||
} else if item.TvdbID != "" {
|
||||
tmdbRes1, err := c.MustTMDB().GetByTvdbId(item.TvdbID, c.language)
|
||||
if err != nil {
|
||||
log.Errorf("get by imdb id error: %v", err)
|
||||
continue
|
||||
}
|
||||
tmdbRes = tmdbRes1
|
||||
}
|
||||
if tmdbRes == nil {
|
||||
log.Errorf("can not find media for : %+v", item)
|
||||
continue
|
||||
}
|
||||
if len(tmdbRes.MovieResults) > 0 {
|
||||
d := tmdbRes.MovieResults[0]
|
||||
name, err := c.SuggestedMovieFolderName(int(d.ID))
|
||||
if err != nil {
|
||||
log.Errorf("suggesting name error: %v", err)
|
||||
continue
|
||||
}
|
||||
_, err = c.AddMovie2Watchlist(AddWatchlistIn{
|
||||
TmdbID: int(d.ID),
|
||||
StorageID: l.StorageID,
|
||||
Resolution: l.Qulity,
|
||||
Folder: name,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("[update_import_lists] add movie to watchlist error: %v", err)
|
||||
} else {
|
||||
c.sendMsg(fmt.Sprintf("成功监控电影:%v", d.Title))
|
||||
log.Infof("[update_import_lists] add movie to watchlist success")
|
||||
}
|
||||
} else if len(tmdbRes.TvResults) > 0 {
|
||||
d := tmdbRes.TvResults[0]
|
||||
name, err := c.SuggestedSeriesFolderName(int(d.ID))
|
||||
if err != nil {
|
||||
log.Errorf("suggesting name error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = c.AddTv2Watchlist(AddWatchlistIn{
|
||||
TmdbID: int(d.ID),
|
||||
StorageID: l.StorageID,
|
||||
Resolution: l.Qulity,
|
||||
Folder: name,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("[update_import_lists] add tv to watchlist error: %v", err)
|
||||
} else {
|
||||
c.sendMsg(fmt.Sprintf("成功监控电视剧:%v", d.Name))
|
||||
log.Infof("[update_import_lists] add tv to watchlist success")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddWatchlistIn struct {
|
||||
TmdbID int `json:"tmdb_id" binding:"required"`
|
||||
StorageID int `json:"storage_id" `
|
||||
Resolution string `json:"resolution" binding:"required"`
|
||||
Folder string `json:"folder" binding:"required"`
|
||||
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
|
||||
SizeMin int64 `json:"size_min"`
|
||||
SizeMax int64 `json:"size_max"`
|
||||
PreferSize int64 `json:"prefer_size"`
|
||||
}
|
||||
|
||||
func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
log.Debugf("add tv watchlist input %+v", in)
|
||||
if in.Folder == "" {
|
||||
return nil, errors.New("folder should be provided")
|
||||
}
|
||||
detailCn, err := c.MustTMDB().GetTvDetails(in.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get tv detail")
|
||||
}
|
||||
var nameCn = detailCn.Name
|
||||
|
||||
detailEn, _ := c.MustTMDB().GetTvDetails(in.TmdbID, db.LanguageEN)
|
||||
var nameEn = detailEn.Name
|
||||
var detail *tmdb.TVDetails
|
||||
if c.language == "" || c.language == db.LanguageCN {
|
||||
detail = detailCn
|
||||
} else {
|
||||
detail = detailEn
|
||||
}
|
||||
log.Infof("find detail for tv id %d: %+v", in.TmdbID, detail)
|
||||
|
||||
lastSeason := 0
|
||||
for _, season := range detail.Seasons {
|
||||
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
|
||||
lastSeason = season.SeasonNumber
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("latest season is %v", lastSeason)
|
||||
|
||||
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get alter titles")
|
||||
}
|
||||
|
||||
var epIds []int
|
||||
for _, season := range detail.Seasons {
|
||||
seasonId := season.SeasonNumber
|
||||
se, err := c.MustTMDB().GetSeasonDetails(int(detail.ID), seasonId, c.language)
|
||||
if err != nil {
|
||||
log.Errorf("get season detail (%s) error: %v", detail.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
shouldMonitor := seasonId >= lastSeason //监控最新的一季
|
||||
|
||||
for _, ep := range se.Episodes {
|
||||
|
||||
// //如果设置下载往期剧集,则监控所有剧集。如果没有则监控未上映的剧集,考虑时差等问题留24h余量
|
||||
// if in.DownloadHistoryEpisodes {
|
||||
// shouldMonitor = true
|
||||
// } else {
|
||||
// t, err := time.Parse("2006-01-02", ep.AirDate)
|
||||
// if err != nil {
|
||||
// log.Error("air date not known, will monitor: %v", ep.AirDate)
|
||||
// shouldMonitor = true
|
||||
|
||||
// } else {
|
||||
// if time.Since(t) < 24*time.Hour { //monitor episode air 24h before now
|
||||
// shouldMonitor = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
ep := ent.Episode{
|
||||
SeasonNumber: seasonId,
|
||||
EpisodeNumber: ep.EpisodeNumber,
|
||||
Title: ep.Name,
|
||||
Overview: ep.Overview,
|
||||
AirDate: ep.AirDate,
|
||||
Monitored: shouldMonitor,
|
||||
}
|
||||
epid, err := c.db.SaveEposideDetail(&ep)
|
||||
if err != nil {
|
||||
log.Errorf("save episode info error: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debugf("success save episode %+v", ep)
|
||||
epIds = append(epIds, epid)
|
||||
}
|
||||
}
|
||||
|
||||
m := &ent.Media{
|
||||
TmdbID: int(detail.ID),
|
||||
ImdbID: detail.IMDbID,
|
||||
MediaType: media.MediaTypeTv,
|
||||
NameCn: nameCn,
|
||||
NameEn: nameEn,
|
||||
OriginalName: detail.OriginalName,
|
||||
Overview: detail.Overview,
|
||||
AirDate: detail.FirstAirDate,
|
||||
Resolution: media.Resolution(in.Resolution),
|
||||
StorageID: in.StorageID,
|
||||
TargetDir: in.Folder,
|
||||
DownloadHistoryEpisodes: in.DownloadHistoryEpisodes,
|
||||
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
|
||||
Extras: schema.MediaExtras{
|
||||
OriginalLanguage: detail.OriginalLanguage,
|
||||
Genres: detail.Genres,
|
||||
},
|
||||
AlternativeTitles: alterTitles,
|
||||
}
|
||||
|
||||
r, err := c.db.AddMediaWatchlist(m, epIds)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "add to list")
|
||||
}
|
||||
go func() {
|
||||
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download w500 poster error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.CheckDownloadedSeriesFiles(r); err != nil {
|
||||
log.Errorf("check downloaded files error: %v", err)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
log.Infof("add tv %s to watchlist success", detail.Name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Client) getAlterTitles(tmdbId int, mediaType media.MediaType) ([]schema.AlternativeTilte, error){
|
||||
var titles []schema.AlternativeTilte
|
||||
|
||||
if mediaType == media.MediaTypeTv {
|
||||
alterTitles, err := c.MustTMDB().GetTVAlternativeTitles(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdb")
|
||||
}
|
||||
|
||||
for _, t := range alterTitles.Results {
|
||||
titles = append(titles, schema.AlternativeTilte{
|
||||
Iso3166_1: t.Iso3166_1,
|
||||
Title: t.Title,
|
||||
Type: t.Type,
|
||||
})
|
||||
}
|
||||
|
||||
} else if mediaType == media.MediaTypeMovie {
|
||||
alterTitles, err := c.MustTMDB().GetMovieAlternativeTitles(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdb")
|
||||
}
|
||||
|
||||
for _, t := range alterTitles.Titles {
|
||||
titles = append(titles, schema.AlternativeTilte{
|
||||
Iso3166_1: t.Iso3166_1,
|
||||
Title: t.Title,
|
||||
Type: t.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
log.Debugf("get alternative titles: %+v", titles)
|
||||
|
||||
return titles, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
log.Infof("add movie watchlist input: %+v", in)
|
||||
detailCn, err := c.MustTMDB().GetMovieDetails(in.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get movie detail")
|
||||
}
|
||||
var nameCn = detailCn.Title
|
||||
|
||||
detailEn, _ := c.MustTMDB().GetMovieDetails(in.TmdbID, db.LanguageEN)
|
||||
var nameEn = detailEn.Title
|
||||
var detail *tmdb.MovieDetails
|
||||
if c.language == "" || c.language == db.LanguageCN {
|
||||
detail = detailCn
|
||||
} else {
|
||||
detail = detailEn
|
||||
}
|
||||
log.Infof("find detail for movie id %d: %v", in.TmdbID, detail)
|
||||
|
||||
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get alter titles")
|
||||
}
|
||||
|
||||
|
||||
epid, err := c.db.SaveEposideDetail(&ent.Episode{
|
||||
SeasonNumber: 1,
|
||||
EpisodeNumber: 1,
|
||||
Title: "dummy episode for movies",
|
||||
Overview: "dummy episode for movies",
|
||||
AirDate: detail.ReleaseDate,
|
||||
Monitored: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "add dummy episode")
|
||||
}
|
||||
log.Infof("added dummy episode for movie: %v", nameEn)
|
||||
|
||||
movie := ent.Media{
|
||||
TmdbID: int(detail.ID),
|
||||
ImdbID: detail.IMDbID,
|
||||
MediaType: media.MediaTypeMovie,
|
||||
NameCn: nameCn,
|
||||
NameEn: nameEn,
|
||||
OriginalName: detail.OriginalTitle,
|
||||
Overview: detail.Overview,
|
||||
AirDate: detail.ReleaseDate,
|
||||
Resolution: media.Resolution(in.Resolution),
|
||||
StorageID: in.StorageID,
|
||||
TargetDir: in.Folder,
|
||||
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
|
||||
AlternativeTitles: alterTitles,
|
||||
}
|
||||
|
||||
extras := schema.MediaExtras{
|
||||
IsAdultMovie: detail.Adult,
|
||||
OriginalLanguage: detail.OriginalLanguage,
|
||||
Genres: detail.Genres,
|
||||
}
|
||||
if IsJav(detail) {
|
||||
javid := c.GetJavid(in.TmdbID)
|
||||
extras.JavId = javid
|
||||
}
|
||||
|
||||
movie.Extras = extras
|
||||
r, err := c.db.AddMediaWatchlist(&movie, []int{epid})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "add to list")
|
||||
}
|
||||
go func() {
|
||||
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download w500 poster error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||
log.Errorf("download backdrop error: %v", err)
|
||||
}
|
||||
if err := c.checkMovieFolder(r); err != nil {
|
||||
log.Warnf("check movie folder error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("add movie %s to watchlist success", detail.Title)
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) checkMovieFolder(m *ent.Media) error {
|
||||
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := storageImpl.ReadDir(m.TargetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ep, err := c.db.GetMovieDummyEpisode(m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() || f.Size() < 100*1000*1000 /* 100M */ { //忽略路径和小于100M的文件
|
||||
continue
|
||||
}
|
||||
meta := metadata.ParseMovie(f.Name())
|
||||
if meta.IsAcceptable(m.NameCn) || meta.IsAcceptable(m.NameEn) {
|
||||
log.Infof("found already downloaded movie: %v", f.Name())
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloaded)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsJav(detail *tmdb.MovieDetails) bool {
|
||||
if detail.Adult && len(detail.ProductionCountries) > 0 && strings.ToUpper(detail.ProductionCountries[0].Iso3166_1) == "JP" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) GetJavid(id int) string {
|
||||
alters, err := c.MustTMDB().GetMovieAlternativeTitles(id, c.language)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, t := range alters.Titles {
|
||||
if t.Iso3166_1 == "JP" && t.Type == "" {
|
||||
return t.Title
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Client) downloadBackdrop(path string, mediaID int) error {
|
||||
url := "https://image.tmdb.org/t/p/original" + path
|
||||
return c.downloadImage(url, mediaID, "backdrop.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadPoster(path string, mediaID int) error {
|
||||
var url = "https://image.tmdb.org/t/p/original" + path
|
||||
|
||||
return c.downloadImage(url, mediaID, "poster.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadW500Poster(path string, mediaID int) error {
|
||||
url := "https://image.tmdb.org/t/p/w500" + path
|
||||
return c.downloadImage(url, mediaID, "poster_w500.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadImage(url string, mediaID int, name string) error {
|
||||
|
||||
log.Infof("try to download image: %v", url)
|
||||
var resp, err = http.Get(url)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "http get")
|
||||
}
|
||||
targetDir := fmt.Sprintf("%v/%d", db.ImgPath, mediaID)
|
||||
os.MkdirAll(targetDir, 0777)
|
||||
//ext := filepath.Ext(path)
|
||||
targetFile := filepath.Join(targetDir, name)
|
||||
f, err := os.Create(targetFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "new file")
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "copy http response")
|
||||
}
|
||||
log.Infof("image successfully downlaoded: %v", targetFile)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) checkW500PosterOnStartup() {
|
||||
log.Infof("check all w500 posters")
|
||||
all := c.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
movies := c.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
all = append(all, movies...)
|
||||
for _, e := range all {
|
||||
targetFile := filepath.Join(fmt.Sprintf("%v/%d", db.ImgPath, e.ID), "poster_w500.jpg")
|
||||
if _, err := os.Stat(targetFile); err != nil {
|
||||
log.Infof("poster_w500.jpg not exist for %s, will download it", e.NameEn)
|
||||
|
||||
if e.MediaType ==media.MediaTypeTv {
|
||||
detail, err := c.MustTMDB().GetTvDetails(e.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
|
||||
log.Warnf("download w500 poster error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
} else {
|
||||
detail, err := c.MustTMDB().GetMovieDetails(e.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
|
||||
log.Warnf("download w500 poster error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SuggestedMovieFolderName(tmdbId int) (string, error) {
|
||||
|
||||
d1, err := c.MustTMDB().GetMovieDetails(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "get movie details")
|
||||
}
|
||||
name := d1.Title
|
||||
|
||||
if IsJav(d1) {
|
||||
javid := c.GetJavid(tmdbId)
|
||||
if javid != "" {
|
||||
return javid, nil
|
||||
}
|
||||
}
|
||||
info := db.NamingInfo{TmdbID: tmdbId}
|
||||
if utils.IsASCII(name) {
|
||||
info.NameEN = stripExtraCharacters(name)
|
||||
} else {
|
||||
info.NameCN = stripExtraCharacters(name)
|
||||
en, err := c.MustTMDB().GetMovieDetails(tmdbId, db.LanguageEN)
|
||||
if err != nil {
|
||||
log.Errorf("get en tv detail error: %v", err)
|
||||
} else {
|
||||
info.NameEN = stripExtraCharacters(en.Title)
|
||||
}
|
||||
}
|
||||
year := strings.Split(d1.ReleaseDate, "-")[0]
|
||||
info.Year = year
|
||||
movieNamingFormat := c.db.GetMovingNamingFormat()
|
||||
|
||||
tmpl, err := template.New("test").Parse(movieNamingFormat)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "naming format")
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
err = tmpl.Execute(buff, info)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "tmpl exec")
|
||||
}
|
||||
res := strings.TrimSpace(buff.String())
|
||||
|
||||
log.Infof("tv series of tmdb id %v suggestting name is %v", tmdbId, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) SuggestedSeriesFolderName(tmdbId int) (string, error) {
|
||||
|
||||
d, err := c.MustTMDB().GetTvDetails(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "get tv details")
|
||||
}
|
||||
|
||||
name := d.Name
|
||||
|
||||
info := db.NamingInfo{TmdbID: tmdbId}
|
||||
if utils.IsASCII(name) {
|
||||
info.NameEN = stripExtraCharacters(name)
|
||||
} else {
|
||||
info.NameCN = stripExtraCharacters(name)
|
||||
en, err := c.MustTMDB().GetTvDetails(tmdbId, db.LanguageEN)
|
||||
if err != nil {
|
||||
log.Errorf("get en tv detail error: %v", err)
|
||||
} else {
|
||||
if en.Name != name { //sometimes en name is in chinese
|
||||
info.NameEN = stripExtraCharacters(en.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
year := strings.Split(d.FirstAirDate, "-")[0]
|
||||
info.Year = year
|
||||
|
||||
tvNamingFormat := c.db.GetTvNamingFormat()
|
||||
|
||||
tmpl, err := template.New("test").Parse(tvNamingFormat)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "naming format")
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
err = tmpl.Execute(buff, info)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "tmpl exec")
|
||||
}
|
||||
res := strings.TrimSpace(buff.String())
|
||||
|
||||
log.Infof("tv series of tmdb id %v suggestting name is %v", tmdbId, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func stripExtraCharacters(s string) string {
|
||||
re := regexp.MustCompile(`[^\p{L}\w\s]`)
|
||||
s = re.ReplaceAllString(s, " ")
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"polaris/ent"
|
||||
"polaris/log"
|
||||
"polaris/pkg/prowlarr"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const prowlarrPrefix = "Prowlarr_"
|
||||
|
||||
func (c *Client) SyncProwlarrIndexers(apiKey, url string) error {
|
||||
client := prowlarr.New(apiKey, url)
|
||||
if ins, err := client.GetIndexers(); err != nil {
|
||||
return errors.Wrap(err, "connect to prowlarr error")
|
||||
} else {
|
||||
var prowlarrNames = make(map[string]bool, len(ins))
|
||||
for _, in := range ins {
|
||||
prowlarrNames[in.Name] = true
|
||||
}
|
||||
all := c.db.GetAllIndexers()
|
||||
for _, index := range all {
|
||||
|
||||
if index.Synced {
|
||||
if !prowlarrNames[strings.TrimPrefix(index.Name, prowlarrPrefix)] {
|
||||
c.db.DeleteIndexer(index.ID) //remove deleted indexers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, indexer := range ins {
|
||||
if err := c.db.SaveIndexer(&ent.Indexers{
|
||||
Disabled: indexer.Disabled,
|
||||
Name: prowlarrPrefix + indexer.Name,
|
||||
Priority: indexer.Priority,
|
||||
SeedRatio: indexer.SeedRatio,
|
||||
//Settings: indexer.Settings,
|
||||
Implementation: "torznab",
|
||||
APIKey: indexer.APIKey,
|
||||
URL: indexer.URL,
|
||||
TvSearch: indexer.TvSearch,
|
||||
MovieSearch: indexer.MovieSearch,
|
||||
Synced: true,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "save prowlarr indexers")
|
||||
}
|
||||
log.Debugf("synced prowlarr indexer to db: %v", indexer.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) syncProwlarr() error {
|
||||
p, err := c.db.GetProwlarrSetting()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "db")
|
||||
}
|
||||
if p.Disabled {
|
||||
return nil
|
||||
}
|
||||
if err := c.SyncProwlarrIndexers(p.ApiKey, p.URL); err != nil {
|
||||
return errors.Wrap(err, "sync prowlarr indexers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) DeleteAllProwlarrIndexers() error {
|
||||
all := c.db.GetAllIndexers()
|
||||
for _, index := range all {
|
||||
if index.Synced {
|
||||
c.db.DeleteIndexer(index.ID)
|
||||
log.Debugf("success delete prowlarr indexer: %s", index.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"polaris/db"
|
||||
"polaris/ent/media"
|
||||
storage1 "polaris/ent/storage"
|
||||
"polaris/log"
|
||||
"polaris/pkg/alist"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/notifier"
|
||||
"polaris/pkg/storage"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) writeNfoFile(historyId int) error {
|
||||
if !c.nfoSupportEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
his := c.db.GetHistory(historyId)
|
||||
|
||||
md, err := c.db.GetMedia(his.MediaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if md.MediaType == media.MediaTypeTv { //tvshow.nfo
|
||||
st, err := c.GetStorage(md.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
|
||||
nfoPath := filepath.Join(md.TargetDir, "tvshow.nfo")
|
||||
_, err = st.ReadFile(nfoPath)
|
||||
if err != nil {
|
||||
log.Infof("tvshow.nfo file missing, create new one, tv series name: %s", md.NameEn)
|
||||
show := Tvshow{
|
||||
Title: md.NameCn,
|
||||
Originaltitle: md.OriginalName,
|
||||
Showtitle: md.NameCn,
|
||||
Plot: md.Overview,
|
||||
ID: strconv.Itoa(md.TmdbID),
|
||||
Uniqueid: []UniqueId{
|
||||
{
|
||||
Text: strconv.Itoa(md.TmdbID),
|
||||
Type: "tmdb",
|
||||
Default: "true",
|
||||
},
|
||||
{
|
||||
Text: md.ImdbID,
|
||||
Type: "imdb",
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := xml.MarshalIndent(&show, " ", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "xml marshal")
|
||||
}
|
||||
return st.WriteFile(nfoPath, []byte(xml.Header+string(data)))
|
||||
}
|
||||
|
||||
} else if md.MediaType == media.MediaTypeMovie { //movie.nfo
|
||||
st, err := c.GetStorage(md.StorageID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
|
||||
nfoPath := filepath.Join(md.TargetDir, "movie.nfo")
|
||||
_, err = st.ReadFile(nfoPath)
|
||||
if err != nil {
|
||||
log.Infof("movie.nfo file missing, create new one, tv series name: %s", md.NameEn)
|
||||
nfoData := Movie{
|
||||
Title: md.NameCn,
|
||||
Originaltitle: md.OriginalName,
|
||||
Sorttitle: md.NameCn,
|
||||
Plot: md.Overview,
|
||||
ID: strconv.Itoa(md.TmdbID),
|
||||
Uniqueid: []UniqueId{
|
||||
{
|
||||
Text: strconv.Itoa(md.TmdbID),
|
||||
Type: "tmdb",
|
||||
Default: "true",
|
||||
},
|
||||
{
|
||||
Text: md.ImdbID,
|
||||
Type: "imdb",
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := xml.MarshalIndent(&nfoData, " ", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "xml marshal")
|
||||
}
|
||||
return st.WriteFile(nfoPath, []byte(xml.Header+string(data)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) writePlexmatch(historyId int) error {
|
||||
|
||||
if !c.plexmatchEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
his := c.db.GetHistory(historyId)
|
||||
|
||||
series, err := c.db.GetMedia(his.MediaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if series.MediaType != media.MediaTypeTv { //.plexmatch only support tv series
|
||||
return nil
|
||||
}
|
||||
st, err := c.GetStorage(series.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
|
||||
//series plexmatch file
|
||||
_, err = st.ReadFile(filepath.Join(series.TargetDir, ".plexmatch"))
|
||||
if err != nil {
|
||||
//create new
|
||||
buff := bytes.Buffer{}
|
||||
if series.ImdbID != "" {
|
||||
buff.WriteString(fmt.Sprintf("imdbid: %s\n", series.ImdbID))
|
||||
}
|
||||
buff.WriteString(fmt.Sprintf("tmdbid: %d\n", series.TmdbID))
|
||||
log.Warnf(".plexmatch file not found, create new one: %s", series.NameEn)
|
||||
if err := st.WriteFile(filepath.Join(series.TargetDir, ".plexmatch"), buff.Bytes()); err != nil {
|
||||
return errors.Wrap(err, "series plexmatch")
|
||||
}
|
||||
}
|
||||
|
||||
buff := bytes.Buffer{}
|
||||
seasonPlex := filepath.Join(his.TargetDir, ".plexmatch")
|
||||
data, err := st.ReadFile(seasonPlex)
|
||||
if err != nil {
|
||||
log.Infof("read season plexmatch: %v", err)
|
||||
} else {
|
||||
buff.Write(data)
|
||||
}
|
||||
episodesIds := c.GetEpisodeIds(his)
|
||||
|
||||
for _, id := range episodesIds {
|
||||
ep, err := c.db.GetEpisodeByID(id)
|
||||
if err != nil {
|
||||
log.Warnf("query episode: %v", err)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(buff.String(), ep.TargetFile) {
|
||||
log.Debugf("already write plex episode line: %v", ep.TargetFile)
|
||||
return nil
|
||||
}
|
||||
buff.WriteString(fmt.Sprintf("\nep: %d: %s\n", ep.EpisodeNumber, ep.TargetFile))
|
||||
|
||||
}
|
||||
|
||||
log.Infof("write season plexmatch file content: %s", buff.String())
|
||||
return st.WriteFile(seasonPlex, buff.Bytes())
|
||||
}
|
||||
|
||||
func (c *Client) plexmatchEnabled() bool {
|
||||
return c.db.GetSetting(db.SettingPlexMatchEnabled) == "true"
|
||||
}
|
||||
|
||||
func (c *Client) nfoSupportEnabled() bool {
|
||||
return c.db.GetSetting(db.SettingNfoSupportEnabled) == "true"
|
||||
}
|
||||
|
||||
func (c *Client) GetStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
|
||||
st := c.db.GetStorage(storageId)
|
||||
targetPath := st.TvPath
|
||||
if mediaType == media.MediaTypeMovie {
|
||||
targetPath = st.MoviePath
|
||||
}
|
||||
videoFormats, err := c.db.GetAcceptedVideoFormats()
|
||||
if err != nil {
|
||||
log.Warnf("get accepted video format error: %v", err)
|
||||
}
|
||||
subtitleFormats, err := c.db.GetAcceptedSubtitleFormats()
|
||||
if err != nil {
|
||||
log.Warnf("get accepted subtitle format error: %v", err)
|
||||
}
|
||||
|
||||
switch st.Implementation {
|
||||
case storage1.ImplementationLocal:
|
||||
|
||||
storageImpl1, err := storage.NewLocalStorage(targetPath, videoFormats, subtitleFormats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new local")
|
||||
}
|
||||
return storageImpl1, nil
|
||||
|
||||
case storage1.ImplementationWebdav:
|
||||
ws := st.ToWebDavSetting()
|
||||
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true", videoFormats, subtitleFormats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new webdav")
|
||||
}
|
||||
return storageImpl1, nil
|
||||
case storage1.ImplementationAlist:
|
||||
cfg := st.ToWebDavSetting()
|
||||
storageImpl1, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, targetPath, videoFormats, subtitleFormats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "alist")
|
||||
}
|
||||
return storageImpl1, nil
|
||||
}
|
||||
return nil, errors.New("no storage found")
|
||||
}
|
||||
|
||||
func (c *Client) sendMsg(msg string) {
|
||||
clients, err := c.db.GetAllNotificationClients2()
|
||||
if err != nil {
|
||||
log.Errorf("query notification clients: %v", err)
|
||||
return
|
||||
}
|
||||
for _, cl := range clients {
|
||||
if !cl.Enabled {
|
||||
continue
|
||||
}
|
||||
handler, ok := notifier.Gethandler(cl.Service)
|
||||
if !ok {
|
||||
log.Errorf("no notification implementation of service %s", cl.Service)
|
||||
continue
|
||||
}
|
||||
noCl, err := handler(cl.Settings)
|
||||
if err != nil {
|
||||
log.Errorf("handle setting for name %s error: %v", cl.Name, err)
|
||||
continue
|
||||
}
|
||||
err = noCl.SendMsg(msg)
|
||||
if err != nil {
|
||||
log.Errorf("send message error: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debugf("send message to %s success, msg is %s", cl.Name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) findEpisodeFilesPreMoving(historyId int) error {
|
||||
his := c.db.GetHistory(historyId)
|
||||
|
||||
episodeIds := c.GetEpisodeIds(his)
|
||||
|
||||
task := c.tasks[historyId]
|
||||
|
||||
ff, err := c.db.GetAcceptedVideoFormats()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range episodeIds {
|
||||
ep, _ := c.db.GetEpisode(his.MediaID, his.SeasonNum, id)
|
||||
task.WalkFunc()(func(path string, info fs.FileInfo) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := filepath.Ext(info.Name())
|
||||
if slices.Contains(ff, ext) {
|
||||
return nil
|
||||
}
|
||||
meta := metadata.ParseTv(info.Name())
|
||||
if meta.StartEpisode == meta.EndEpisode && meta.StartEpisode == ep.EpisodeNumber {
|
||||
if err := c.db.UpdateEpisodeTargetFile(id, info.Name()); err != nil {
|
||||
log.Errorf("writing downloaded file name to db error: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package core
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type Tvshow struct {
|
||||
XMLName xml.Name `xml:"tvshow"`
|
||||
Text string `xml:",chardata"`
|
||||
Title string `xml:"title"`
|
||||
Originaltitle string `xml:"originaltitle"`
|
||||
Showtitle string `xml:"showtitle"`
|
||||
Ratings struct {
|
||||
Text string `xml:",chardata"`
|
||||
Rating []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name,attr"`
|
||||
Max string `xml:"max,attr"`
|
||||
Default string `xml:"default,attr"`
|
||||
Value string `xml:"value"`
|
||||
Votes string `xml:"votes"`
|
||||
} `xml:"rating"`
|
||||
} `xml:"ratings"`
|
||||
Userrating string `xml:"userrating"`
|
||||
Top250 string `xml:"top250"`
|
||||
Season string `xml:"season"`
|
||||
Episode string `xml:"episode"`
|
||||
Displayseason string `xml:"displayseason"`
|
||||
Displayepisode string `xml:"displayepisode"`
|
||||
Outline string `xml:"outline"`
|
||||
Plot string `xml:"plot"`
|
||||
Tagline string `xml:"tagline"`
|
||||
Runtime string `xml:"runtime"`
|
||||
Thumb []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Spoof string `xml:"spoof,attr"`
|
||||
Cache string `xml:"cache,attr"`
|
||||
Aspect string `xml:"aspect,attr"`
|
||||
Preview string `xml:"preview,attr"`
|
||||
Season string `xml:"season,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"thumb"`
|
||||
Fanart struct {
|
||||
Text string `xml:",chardata"`
|
||||
Thumb []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Colors string `xml:"colors,attr"`
|
||||
Preview string `xml:"preview,attr"`
|
||||
} `xml:"thumb"`
|
||||
} `xml:"fanart"`
|
||||
Mpaa string `xml:"mpaa"`
|
||||
Playcount string `xml:"playcount"`
|
||||
Lastplayed string `xml:"lastplayed"`
|
||||
ID string `xml:"id"`
|
||||
Uniqueid []UniqueId `xml:"uniqueid"`
|
||||
Genre string `xml:"genre"`
|
||||
Premiered string `xml:"premiered"`
|
||||
Year string `xml:"year"`
|
||||
Status string `xml:"status"`
|
||||
Code string `xml:"code"`
|
||||
Aired string `xml:"aired"`
|
||||
Studio string `xml:"studio"`
|
||||
Trailer string `xml:"trailer"`
|
||||
Actor []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name"`
|
||||
Role string `xml:"role"`
|
||||
Order string `xml:"order"`
|
||||
Thumb string `xml:"thumb"`
|
||||
} `xml:"actor"`
|
||||
Namedseason []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Number string `xml:"number,attr"`
|
||||
} `xml:"namedseason"`
|
||||
Resume struct {
|
||||
Text string `xml:",chardata"`
|
||||
Position string `xml:"position"`
|
||||
Total string `xml:"total"`
|
||||
} `xml:"resume"`
|
||||
Dateadded string `xml:"dateadded"`
|
||||
}
|
||||
|
||||
type UniqueId struct {
|
||||
Text string `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
Default string `xml:"default,attr"`
|
||||
}
|
||||
|
||||
type Episodedetails struct {
|
||||
XMLName xml.Name `xml:"episodedetails"`
|
||||
Text string `xml:",chardata"`
|
||||
Title string `xml:"title"`
|
||||
Showtitle string `xml:"showtitle"`
|
||||
Ratings struct {
|
||||
Text string `xml:",chardata"`
|
||||
Rating []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name,attr"`
|
||||
Max string `xml:"max,attr"`
|
||||
Default string `xml:"default,attr"`
|
||||
Value string `xml:"value"`
|
||||
Votes string `xml:"votes"`
|
||||
} `xml:"rating"`
|
||||
} `xml:"ratings"`
|
||||
Userrating string `xml:"userrating"`
|
||||
Top250 string `xml:"top250"`
|
||||
Season string `xml:"season"`
|
||||
Episode string `xml:"episode"`
|
||||
Displayseason string `xml:"displayseason"`
|
||||
Displayepisode string `xml:"displayepisode"`
|
||||
Outline string `xml:"outline"`
|
||||
Plot string `xml:"plot"`
|
||||
Tagline string `xml:"tagline"`
|
||||
Runtime string `xml:"runtime"`
|
||||
Thumb []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Spoof string `xml:"spoof,attr"`
|
||||
Cache string `xml:"cache,attr"`
|
||||
Aspect string `xml:"aspect,attr"`
|
||||
Preview string `xml:"preview,attr"`
|
||||
} `xml:"thumb"`
|
||||
Mpaa string `xml:"mpaa"`
|
||||
Playcount string `xml:"playcount"`
|
||||
Lastplayed string `xml:"lastplayed"`
|
||||
ID string `xml:"id"`
|
||||
Uniqueid []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
Default string `xml:"default,attr"`
|
||||
} `xml:"uniqueid"`
|
||||
Genre string `xml:"genre"`
|
||||
Credits []string `xml:"credits"`
|
||||
Director string `xml:"director"`
|
||||
Premiered string `xml:"premiered"`
|
||||
Year string `xml:"year"`
|
||||
Status string `xml:"status"`
|
||||
Code string `xml:"code"`
|
||||
Aired string `xml:"aired"`
|
||||
Studio string `xml:"studio"`
|
||||
Trailer string `xml:"trailer"`
|
||||
Actor []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name"`
|
||||
Role string `xml:"role"`
|
||||
Order string `xml:"order"`
|
||||
Thumb string `xml:"thumb"`
|
||||
} `xml:"actor"`
|
||||
Resume struct {
|
||||
Text string `xml:",chardata"`
|
||||
Position string `xml:"position"`
|
||||
Total string `xml:"total"`
|
||||
} `xml:"resume"`
|
||||
Dateadded string `xml:"dateadded"`
|
||||
}
|
||||
|
||||
type Movie struct {
|
||||
XMLName xml.Name `xml:"movie"`
|
||||
Text string `xml:",chardata"`
|
||||
Title string `xml:"title"`
|
||||
Originaltitle string `xml:"originaltitle"`
|
||||
Sorttitle string `xml:"sorttitle"`
|
||||
Ratings struct {
|
||||
Text string `xml:",chardata"`
|
||||
Rating []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name,attr"`
|
||||
Max string `xml:"max,attr"`
|
||||
Default string `xml:"default,attr"`
|
||||
Value string `xml:"value"`
|
||||
Votes string `xml:"votes"`
|
||||
} `xml:"rating"`
|
||||
} `xml:"ratings"`
|
||||
Userrating string `xml:"userrating"`
|
||||
Top250 string `xml:"top250"`
|
||||
Outline string `xml:"outline"`
|
||||
Plot string `xml:"plot"`
|
||||
Tagline string `xml:"tagline"`
|
||||
Runtime string `xml:"runtime"`
|
||||
Thumb []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Spoof string `xml:"spoof,attr"`
|
||||
Cache string `xml:"cache,attr"`
|
||||
Aspect string `xml:"aspect,attr"`
|
||||
Preview string `xml:"preview,attr"`
|
||||
} `xml:"thumb"`
|
||||
Fanart struct {
|
||||
Text string `xml:",chardata"`
|
||||
Thumb struct {
|
||||
Text string `xml:",chardata"`
|
||||
Colors string `xml:"colors,attr"`
|
||||
Preview string `xml:"preview,attr"`
|
||||
} `xml:"thumb"`
|
||||
} `xml:"fanart"`
|
||||
Mpaa string `xml:"mpaa"`
|
||||
Playcount string `xml:"playcount"`
|
||||
Lastplayed string `xml:"lastplayed"`
|
||||
ID string `xml:"id"`
|
||||
Uniqueid []UniqueId `xml:"uniqueid"`
|
||||
Genre string `xml:"genre"`
|
||||
Country []string `xml:"country"`
|
||||
Set struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name"`
|
||||
Overview string `xml:"overview"`
|
||||
} `xml:"set"`
|
||||
Tag []string `xml:"tag"`
|
||||
Videoassettitle string `xml:"videoassettitle"`
|
||||
Videoassetid string `xml:"videoassetid"`
|
||||
Videoassettype string `xml:"videoassettype"`
|
||||
Hasvideoversions string `xml:"hasvideoversions"`
|
||||
Hasvideoextras string `xml:"hasvideoextras"`
|
||||
Isdefaultvideoversion string `xml:"isdefaultvideoversion"`
|
||||
Credits []string `xml:"credits"`
|
||||
Director string `xml:"director"`
|
||||
Premiered string `xml:"premiered"`
|
||||
Year string `xml:"year"`
|
||||
Status string `xml:"status"`
|
||||
Code string `xml:"code"`
|
||||
Aired string `xml:"aired"`
|
||||
Studio string `xml:"studio"`
|
||||
Trailer string `xml:"trailer"`
|
||||
Fileinfo struct {
|
||||
Text string `xml:",chardata"`
|
||||
Streamdetails struct {
|
||||
Text string `xml:",chardata"`
|
||||
Video struct {
|
||||
Text string `xml:",chardata"`
|
||||
Codec string `xml:"codec"`
|
||||
Aspect string `xml:"aspect"`
|
||||
Width string `xml:"width"`
|
||||
Height string `xml:"height"`
|
||||
Durationinseconds string `xml:"durationinseconds"`
|
||||
Stereomode string `xml:"stereomode"`
|
||||
Hdrtype string `xml:"hdrtype"`
|
||||
} `xml:"video"`
|
||||
Audio struct {
|
||||
Text string `xml:",chardata"`
|
||||
Codec string `xml:"codec"`
|
||||
Language string `xml:"language"`
|
||||
Channels string `xml:"channels"`
|
||||
} `xml:"audio"`
|
||||
Subtitle struct {
|
||||
Text string `xml:",chardata"`
|
||||
Language string `xml:"language"`
|
||||
} `xml:"subtitle"`
|
||||
} `xml:"streamdetails"`
|
||||
} `xml:"fileinfo"`
|
||||
Actor []struct {
|
||||
Text string `xml:",chardata"`
|
||||
Name string `xml:"name"`
|
||||
Role string `xml:"role"`
|
||||
Order string `xml:"order"`
|
||||
Thumb string `xml:"thumb"`
|
||||
} `xml:"actor"`
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/notifier/message"
|
||||
"polaris/pkg/torznab"
|
||||
"polaris/pkg/utils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum int, episodeNums ...int) (*string, error) {
|
||||
|
||||
series, err := c.db.GetMedia(seriesId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no tv series of id %v", seriesId)
|
||||
}
|
||||
|
||||
return c.downloadTorrent(series, r1, seasonNum, episodeNums...)
|
||||
}
|
||||
|
||||
/*
|
||||
tmdb 校验获取的资源名,如果用资源名在tmdb搜索出来的结果能匹配上想要的资源,则认为资源有效,否则无效
|
||||
解决名称过于简单的影视会匹配过多资源的问题, 例如:梦魇绝镇 FROM
|
||||
*/
|
||||
func (c *Client) checkBtReourceWithTmdb(r *torznab.Result, seriesId int) bool {
|
||||
m := metadata.ParseTv(r.Name)
|
||||
se, err := c.MustTMDB().SearchMedia(m.NameEn, "", 1)
|
||||
if err != nil {
|
||||
log.Warnf("tmdb search error, consider this torrent ok: %v", err)
|
||||
return true
|
||||
} else {
|
||||
if len(se.Results) == 0 {
|
||||
log.Debugf("tmdb search no result, consider this torrent ok: %s", r.Name) //because tv name parse is not accurate
|
||||
return true
|
||||
}
|
||||
series, err := c.db.GetMediaDetails(seriesId)
|
||||
if err != nil {
|
||||
log.Warnf("get media details error: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
se0 := se.Results[0]
|
||||
if se0.ID != int64(series.TmdbID) {
|
||||
log.Warnf("bt reosurce name not match tmdb id: %s", r.Name)
|
||||
return false
|
||||
} else { //resource tmdb id match
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SearchAndDownload(seriesId, seasonNum int, episodeNums ...int) ([]string, error) {
|
||||
|
||||
res, err := SearchTvSeries(c.db, &SearchParam{
|
||||
MediaId: seriesId,
|
||||
SeasonNum: seasonNum,
|
||||
Episodes: episodeNums,
|
||||
CheckFileSize: true,
|
||||
CheckResolution: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wanted := make(map[int]bool, len(episodeNums))
|
||||
for _, ep := range episodeNums {
|
||||
wanted[ep] = true
|
||||
}
|
||||
var torrentNames []string
|
||||
lo:
|
||||
for _, r := range res {
|
||||
if !c.checkBtReourceWithTmdb(&r, seriesId) {
|
||||
continue
|
||||
}
|
||||
m := metadata.ParseTv(r.Name)
|
||||
m.ParseExtraDescription(r.Description)
|
||||
if len(episodeNums) == 0 { //want season pack
|
||||
if m.IsSeasonPack {
|
||||
name, err := c.DownloadEpisodeTorrent(r, seriesId, seasonNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
torrentNames = append(torrentNames, *name)
|
||||
break lo
|
||||
}
|
||||
} else {
|
||||
torrentEpisodes := make([]int, 0)
|
||||
for i := m.StartEpisode; i <= m.EndEpisode; i++ {
|
||||
if !wanted[i] { //torrent has episode not wanted
|
||||
continue lo
|
||||
}
|
||||
torrentEpisodes = append(torrentEpisodes, i)
|
||||
}
|
||||
name, err := c.DownloadEpisodeTorrent(r, seriesId, seasonNum, torrentEpisodes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
torrentNames = append(torrentNames, *name)
|
||||
|
||||
for _, num := range torrentEpisodes {
|
||||
delete(wanted, num) //delete downloaded episode from wanted
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(wanted) > 0 {
|
||||
log.Warnf("still wanted but not downloaded episodes: %v", wanted)
|
||||
}
|
||||
return torrentNames, nil
|
||||
}
|
||||
|
||||
func (c *Client) DownloadMovie(m *ent.Media, r1 torznab.Result) (*string, error) {
|
||||
return c.downloadTorrent(m, r1, 0)
|
||||
}
|
||||
|
||||
func (c *Client) downloadTorrent(m *ent.Media, r1 torznab.Result, seasonNum int, episodeNums ...int) (*string, error) {
|
||||
trc, dlc, err := c.GetDownloadClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
}
|
||||
|
||||
//check space available
|
||||
downloadDir := c.db.GetDownloadDir()
|
||||
size := utils.AvailableSpace(downloadDir)
|
||||
if size < uint64(r1.Size) {
|
||||
log.Errorf("space available %v, space needed %v", size, r1.Size)
|
||||
return nil, errors.New("no enough space")
|
||||
}
|
||||
|
||||
var name = r1.Name
|
||||
var targetDir = m.TargetDir
|
||||
if m.MediaType == media.MediaTypeTv { //tv download
|
||||
targetDir = fmt.Sprintf("%s/Season %02d/", m.TargetDir, seasonNum)
|
||||
|
||||
if len(episodeNums) > 0 {
|
||||
for _, epNum := range episodeNums {
|
||||
ep, err := c.db.GetEpisode(m.ID, seasonNum, epNum)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("no episode of season %d episode %d", seasonNum, epNum)
|
||||
|
||||
}
|
||||
if ep.Status == episode.StatusMissing {
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
}
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
for i, ep := range episodeNums {
|
||||
if i != 0 {
|
||||
buff.WriteString(",")
|
||||
|
||||
}
|
||||
buff.WriteString(fmt.Sprint(ep))
|
||||
}
|
||||
name = fmt.Sprintf("第%s集 (%s)", buff.String(), name)
|
||||
|
||||
} else { //season package download
|
||||
name = fmt.Sprintf("全集 (%s)", name)
|
||||
c.db.SetSeasonAllEpisodeStatus(m.ID, seasonNum, episode.StatusDownloading)
|
||||
}
|
||||
|
||||
} else {
|
||||
ep, _ := c.db.GetMovieDummyEpisode(m.ID)
|
||||
if ep.Status == episode.StatusMissing {
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
}
|
||||
|
||||
}
|
||||
hash, err := utils.Link2Hash(r1.Link)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get hash")
|
||||
}
|
||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: m.ID,
|
||||
EpisodeNums: episodeNums,
|
||||
SeasonNum: seasonNum,
|
||||
SourceTitle: r1.Name,
|
||||
TargetDir: targetDir,
|
||||
Status: history.StatusRunning,
|
||||
Size: int(r1.Size),
|
||||
//Saved: torrent.Save(),
|
||||
Link: r1.Link,
|
||||
Hash: hash,
|
||||
DownloadClientID: dlc.ID,
|
||||
IndexerID: r1.IndexerId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "save record")
|
||||
}
|
||||
|
||||
torrent, err := trc.Download(r1.Link, hash, downloadDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "downloading")
|
||||
}
|
||||
torrent.Start()
|
||||
|
||||
c.tasks[history.ID] = &Task{Torrent: torrent}
|
||||
|
||||
c.sendMsg(fmt.Sprintf(message.BeginDownload, name))
|
||||
|
||||
log.Infof("success add %s to download task", r1.Name)
|
||||
|
||||
return &r1.Name, nil
|
||||
}
|
||||
@@ -1,559 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg"
|
||||
"polaris/pkg/notifier/message"
|
||||
"polaris/pkg/utils"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) addSysCron() {
|
||||
c.registerCronJob("check_running_tasks", "@every 1m", c.checkTasks)
|
||||
c.registerCronJob("check_available_medias_to_download", "0 0 * * * *", func() error {
|
||||
v := os.Getenv("POLARIS_NO_AUTO_DOWNLOAD")
|
||||
if v == "true" {
|
||||
return nil
|
||||
}
|
||||
if err := c.syncProwlarr(); err != nil {
|
||||
log.Warnf("sync prowlarr error: %v", err)
|
||||
}
|
||||
c.downloadAllTvSeries()
|
||||
c.downloadAllMovies()
|
||||
return nil
|
||||
})
|
||||
c.registerCronJob("check_series_new_release", "0 0 */12 * * *", c.checkAllSeriesNewSeason)
|
||||
c.registerCronJob("update_import_lists", "0 30 * * * *", c.periodicallyUpdateImportlist)
|
||||
|
||||
c.schedulers.Range(func(key string, value scheduler) bool {
|
||||
log.Debugf("add cron job: %v", key)
|
||||
c.mustAddCron(value.cron, func() {
|
||||
if err := value.f(); err != nil {
|
||||
log.Errorf("exexuting cron job %s error: %v", key, err)
|
||||
}
|
||||
})
|
||||
return true
|
||||
})
|
||||
c.cron.Start()
|
||||
log.Infof("--------- add cron jobs done --------")
|
||||
}
|
||||
|
||||
func (c *Client) mustAddCron(spec string, cmd func()) {
|
||||
if err := c.cron.AddFunc(spec, cmd); err != nil {
|
||||
log.Errorf("add func error: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) TriggerCronJob(name string) error {
|
||||
job, ok := c.schedulers.Load(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("job name not exists: %s", name)
|
||||
}
|
||||
return job.f()
|
||||
}
|
||||
|
||||
func (c *Client) checkTasks() error {
|
||||
log.Debug("begin check tasks...")
|
||||
for id, t := range c.tasks {
|
||||
r := c.db.GetHistory(id)
|
||||
if !t.Exists() {
|
||||
log.Infof("task no longer exists: %v", id)
|
||||
|
||||
delete(c.tasks, id)
|
||||
continue
|
||||
}
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get name")
|
||||
}
|
||||
|
||||
progress, err := t.Progress()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get progress")
|
||||
}
|
||||
log.Infof("task (%s) percentage done: %d%%", name, progress)
|
||||
if progress == 100 {
|
||||
|
||||
if r.Status == history.StatusSeeding {
|
||||
//task already success, check seed ratio
|
||||
torrent := c.tasks[id]
|
||||
ratio, ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||
if ok {
|
||||
log.Infof("torrent file seed ratio reached, remove: %v, current seed ratio: %v", name, ratio)
|
||||
torrent.Remove()
|
||||
delete(c.tasks, id)
|
||||
c.setHistoryStatus(id, history.StatusSuccess)
|
||||
} else {
|
||||
log.Infof("torrent file still sedding: %v, current seed ratio: %v", name, ratio)
|
||||
}
|
||||
continue
|
||||
} else if r.Status == history.StatusRunning {
|
||||
log.Infof("task is done: %v", name)
|
||||
c.sendMsg(fmt.Sprintf(message.DownloadComplete, name))
|
||||
go c.postTaskProcessing(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
episode 状态有3种:missing、downloading、downloaded
|
||||
|
||||
history状态有5种:running, success, fail, uploading, seeding
|
||||
|
||||
没有下载的剧集状态都是missing,已下载完成的都是downloaded,正在下载的是downloading
|
||||
|
||||
对应的history状态,下载任务创建成功,正常跑着是running,出了问题失败了,就是fail,下载完成的任务会先进入uploading状态进一步处理,
|
||||
uploading状态下会传输到对应的存储里面,uploading成功如果需要做种会进入seeding状态,如果不做种进入success状态,失败了会进入fail状态
|
||||
|
||||
seeding状态中,会定时检查做种状态,达到指定分享率,会置为success
|
||||
|
||||
任务创建成功,episode状态会由missing置为downloading,如果任务失败重新置为missing,如果任务成功进入success或seeding,episode状态应置为downloaded
|
||||
|
||||
*/
|
||||
|
||||
func (c *Client) setHistoryStatus(id int, status history.Status) {
|
||||
r := c.db.GetHistory(id)
|
||||
|
||||
episodeIds := c.GetEpisodeIds(r)
|
||||
|
||||
switch status {
|
||||
case history.StatusRunning:
|
||||
c.db.SetHistoryStatus(id, history.StatusRunning)
|
||||
c.setEpsideoStatus(episodeIds, episode.StatusDownloading)
|
||||
case history.StatusSuccess:
|
||||
c.db.SetHistoryStatus(id, history.StatusSuccess)
|
||||
c.setEpsideoStatus(episodeIds, episode.StatusDownloaded)
|
||||
|
||||
case history.StatusUploading:
|
||||
c.db.SetHistoryStatus(id, history.StatusUploading)
|
||||
|
||||
case history.StatusSeeding:
|
||||
c.db.SetHistoryStatus(id, history.StatusSeeding)
|
||||
c.setEpsideoStatus(episodeIds, episode.StatusDownloaded)
|
||||
|
||||
case history.StatusFail:
|
||||
c.db.SetHistoryStatus(id, history.StatusFail)
|
||||
c.setEpsideoStatus(episodeIds, episode.StatusMissing)
|
||||
default:
|
||||
panic(fmt.Sprintf("unkown status %v", status))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) setEpsideoStatus(episodeIds []int, status episode.Status) error {
|
||||
for _, id := range episodeIds {
|
||||
ep, err := c.db.GetEpisodeByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ep.Status == episode.StatusDownloaded {
|
||||
//已经下载完成的任务,不再重新设置状态
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.db.SetEpisodeStatus(id, status); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) postTaskProcessing(id int) {
|
||||
if err := c.findEpisodeFilesPreMoving(id); err != nil {
|
||||
log.Errorf("finding all episode file error: %v", err)
|
||||
} else {
|
||||
if err := c.writePlexmatch(id); err != nil {
|
||||
log.Errorf("write plexmatch file error: %v", err)
|
||||
}
|
||||
if err := c.writeNfoFile(id); err != nil {
|
||||
log.Errorf("write nfo file error: %v", err)
|
||||
}
|
||||
}
|
||||
if err := c.moveCompletedTask(id); err != nil {
|
||||
log.Infof("post tasks for id %v fail: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getSeasonNum(h *ent.History) int {
|
||||
if h.SeasonNum != 0 {
|
||||
return h.SeasonNum
|
||||
}
|
||||
seasonNum, err := utils.SeasonId(h.TargetDir)
|
||||
if err != nil {
|
||||
log.Errorf("no season id: %v", h.TargetDir)
|
||||
seasonNum = -1
|
||||
}
|
||||
return seasonNum
|
||||
}
|
||||
|
||||
func (c *Client) GetEpisodeIds(r *ent.History) []int {
|
||||
var episodeIds []int
|
||||
seasonNum := getSeasonNum(r)
|
||||
|
||||
// if r.EpisodeID > 0 {
|
||||
// episodeIds = append(episodeIds, r.EpisodeID)
|
||||
// }
|
||||
series, err := c.db.GetMediaDetails(r.MediaID)
|
||||
if err != nil {
|
||||
log.Errorf("get media details error: %v", err)
|
||||
return []int{}
|
||||
}
|
||||
|
||||
if len(r.EpisodeNums) > 0 {
|
||||
for _, epNum := range r.EpisodeNums {
|
||||
for _, ep := range series.Episodes {
|
||||
if ep.SeasonNumber == seasonNum && ep.EpisodeNumber == epNum {
|
||||
episodeIds = append(episodeIds, ep.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, ep := range series.Episodes {
|
||||
if ep.SeasonNumber == seasonNum {
|
||||
episodeIds = append(episodeIds, ep.ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return episodeIds
|
||||
}
|
||||
|
||||
func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
torrent := c.tasks[id]
|
||||
r := c.db.GetHistory(id)
|
||||
// if r.Status == history.StatusUploading {
|
||||
// log.Infof("task %d is already uploading, skip", id)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
c.setHistoryStatus(r.ID, history.StatusUploading)
|
||||
|
||||
downloadclient, err := c.db.GetDownloadClient(r.DownloadClientID)
|
||||
if err != nil {
|
||||
log.Errorf("get task download client error: %v, use default one", err)
|
||||
downloadclient = &ent.DownloadClients{RemoveCompletedDownloads: true, RemoveFailedDownloads: true}
|
||||
}
|
||||
torrentName, err := torrent.Name()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
||||
if err1 != nil {
|
||||
c.setHistoryStatus(r.ID, history.StatusFail)
|
||||
c.sendMsg(fmt.Sprintf(message.ProcessingFailed, err1))
|
||||
if downloadclient.RemoveFailedDownloads {
|
||||
log.Debugf("task failed, remove failed torrent and files related")
|
||||
delete(c.tasks, r.ID)
|
||||
torrent.Remove()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
series, err := c.db.GetMediaDetails(r.MediaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st := c.db.GetStorage(series.StorageID)
|
||||
log.Infof("move task files to target dir: %v", r.TargetDir)
|
||||
stImpl, err := c.GetStorage(st.ID, series.MediaType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//如果种子是路径,则会把路径展开,只移动文件,类似 move dir/* dir2/, 如果种子是文件,则会直接移动文件,类似 move file dir/
|
||||
if err := stImpl.Copy(filepath.Join(c.db.GetDownloadDir(), torrentName), r.TargetDir, torrent.WalkFunc()); err != nil {
|
||||
return errors.Wrap(err, "move file")
|
||||
}
|
||||
torrent.UploadProgresser = stImpl.UploadProgress
|
||||
|
||||
c.sendMsg(fmt.Sprintf(message.ProcessingComplete, torrentName))
|
||||
|
||||
//判断是否需要删除本地文件, TODO prowlarr has no indexer id
|
||||
r1, ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||
if downloadclient.RemoveCompletedDownloads && ok {
|
||||
log.Debugf("download complete,remove torrent and files related, torrent: %v, seed ratio: %v", torrentName, r1)
|
||||
c.setHistoryStatus(r.ID, history.StatusSuccess)
|
||||
delete(c.tasks, r.ID)
|
||||
torrent.Remove()
|
||||
} else {
|
||||
log.Infof("task complete but still needs seeding: %v", torrentName)
|
||||
c.setHistoryStatus(r.ID, history.StatusSeeding)
|
||||
}
|
||||
|
||||
log.Infof("move downloaded files to target dir success, file: %v, target dir: %v", torrentName, r.TargetDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
|
||||
if m.MediaType != media.MediaTypeTv {
|
||||
return nil
|
||||
}
|
||||
log.Infof("check files in directory: %s", m.TargetDir)
|
||||
|
||||
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := storageImpl.ReadDir(m.TargetDir)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "read dir %s", m.TargetDir)
|
||||
}
|
||||
|
||||
for _, in := range files {
|
||||
if !in.IsDir() { //season dir, ignore file
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(m.TargetDir, in.Name())
|
||||
epFiles, err := storageImpl.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Errorf("read dir %s error: %v", dir, err)
|
||||
continue
|
||||
}
|
||||
for _, ep := range epFiles {
|
||||
log.Infof("found file: %v", ep.Name())
|
||||
seNum, epNum, err := utils.FindSeasonEpisodeNum(ep.Name())
|
||||
if err != nil {
|
||||
log.Errorf("find season episode num error: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Infof("found match, season num %d, episode num %d", seNum, epNum)
|
||||
ep, err := c.db.GetEpisode(m.ID, seNum, epNum)
|
||||
if err != nil {
|
||||
log.Error("update episode: %v", err)
|
||||
continue
|
||||
}
|
||||
err = c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloaded)
|
||||
if err != nil {
|
||||
log.Error("update episode: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
//Processing bool
|
||||
pkg.Torrent
|
||||
UploadProgresser func() float64
|
||||
}
|
||||
|
||||
func (c *Client) DownloadSeriesAllEpisodes(id int) []string {
|
||||
tvDetail, err := c.db.GetMediaDetails(id)
|
||||
if err != nil {
|
||||
log.Errorf("get media details error: %v", err)
|
||||
return nil
|
||||
}
|
||||
m := make(map[int][]*ent.Episode)
|
||||
for _, ep := range tvDetail.Episodes {
|
||||
m[ep.SeasonNumber] = append(m[ep.SeasonNumber], ep)
|
||||
}
|
||||
var allNames []string
|
||||
for seasonNum, epsides := range m {
|
||||
if seasonNum == 0 {
|
||||
continue
|
||||
}
|
||||
wantedSeasonPack := true
|
||||
seasonEpisodesWanted := make(map[int][]int, 0)
|
||||
for _, ep := range epsides {
|
||||
if !ep.Monitored {
|
||||
wantedSeasonPack = false
|
||||
continue
|
||||
}
|
||||
if ep.Status != episode.StatusMissing {
|
||||
wantedSeasonPack = false
|
||||
continue
|
||||
}
|
||||
if ep.AirDate != "" {
|
||||
t, err := time.Parse("2006-01-02", ep.AirDate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
/*
|
||||
-------- now ------ t -----
|
||||
t - 1day < now 要检测的剧集
|
||||
提前一天开始检测
|
||||
*/
|
||||
if time.Now().Before(t.Add(-24 * time.Hour)) { //not aired
|
||||
wantedSeasonPack = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
seasonEpisodesWanted[ep.SeasonNumber] = append(seasonEpisodesWanted[ep.SeasonNumber], ep.EpisodeNumber)
|
||||
}
|
||||
if wantedSeasonPack {
|
||||
names, err := c.SearchAndDownload(id, seasonNum)
|
||||
if err == nil {
|
||||
allNames = append(allNames, names...)
|
||||
log.Infof("begin download torrent resource: %v", names)
|
||||
} else {
|
||||
log.Warnf("finding season pack error: %v", err)
|
||||
wantedSeasonPack = false
|
||||
}
|
||||
}
|
||||
if !wantedSeasonPack {
|
||||
|
||||
for se, eps := range seasonEpisodesWanted {
|
||||
names, err := c.SearchAndDownload(id, se, eps...)
|
||||
if err != nil {
|
||||
log.Warnf("finding resoruces of season %d episode %v error: %v", se, eps, err)
|
||||
continue
|
||||
} else {
|
||||
allNames = append(allNames, names...)
|
||||
log.Infof("begin download torrent resource: %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return allNames
|
||||
}
|
||||
|
||||
func (c *Client) downloadAllTvSeries() {
|
||||
log.Infof("begin check all tv series resources")
|
||||
allSeries := c.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
for _, series := range allSeries {
|
||||
c.DownloadSeriesAllEpisodes(series.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) downloadAllMovies() {
|
||||
log.Infof("begin check all movie resources")
|
||||
allSeries := c.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
|
||||
for _, series := range allSeries {
|
||||
if _, err := c.DownloadMovieByID(series.ID); err != nil {
|
||||
log.Errorf("download movie error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) DownloadMovieByID(id int) (string, error) {
|
||||
detail, err := c.db.GetMediaDetails(id)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "get media details")
|
||||
}
|
||||
if len(detail.Episodes) == 0 {
|
||||
return "", fmt.Errorf("no related dummy episode: %v", detail.NameEn)
|
||||
}
|
||||
ep := detail.Episodes[0]
|
||||
if ep.Status != episode.StatusMissing {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if name, err := c.downloadMovieSingleEpisode(detail.Media, ep); err != nil {
|
||||
return "", errors.Wrap(err, "download movie")
|
||||
} else {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) downloadMovieSingleEpisode(m *ent.Media, ep *ent.Episode) (string, error) {
|
||||
|
||||
qiangban := c.db.GetSetting(db.SettingAllowQiangban)
|
||||
allowQiangban := false
|
||||
if qiangban == "true" {
|
||||
allowQiangban = false
|
||||
}
|
||||
|
||||
res, err := SearchMovie(c.db, &SearchParam{
|
||||
MediaId: ep.MediaID,
|
||||
CheckFileSize: true,
|
||||
CheckResolution: true,
|
||||
FilterQiangban: !allowQiangban,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "search movie")
|
||||
}
|
||||
r1 := res[0]
|
||||
log.Infof("begin download torrent resource: %v", r1.Name)
|
||||
|
||||
s, err := c.downloadTorrent(m, r1, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *s, nil
|
||||
}
|
||||
|
||||
func (c *Client) checkAllSeriesNewSeason() error {
|
||||
log.Infof("begin checking series all new season")
|
||||
allSeries := c.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
for _, series := range allSeries {
|
||||
err := c.checkSeiesNewSeason(series)
|
||||
if err != nil {
|
||||
log.Errorf("check series new season error: series name %v, error: %v", series.NameEn, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) checkSeiesNewSeason(media *ent.Media) error {
|
||||
d, err := c.MustTMDB().GetTvDetails(media.TmdbID, c.language)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tmdb")
|
||||
}
|
||||
lastsSason := d.NumberOfSeasons
|
||||
seasonDetail, err := c.MustTMDB().GetSeasonDetails(media.TmdbID, lastsSason, c.language)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tmdb season")
|
||||
}
|
||||
|
||||
for _, ep := range seasonDetail.Episodes {
|
||||
epDb, err := c.db.GetEpisode(media.ID, ep.SeasonNumber, ep.EpisodeNumber)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
log.Infof("add new episode: %+v", ep)
|
||||
episode := &ent.Episode{
|
||||
MediaID: media.ID,
|
||||
SeasonNumber: ep.SeasonNumber,
|
||||
EpisodeNumber: ep.EpisodeNumber,
|
||||
Title: ep.Name,
|
||||
Overview: ep.Overview,
|
||||
AirDate: ep.AirDate,
|
||||
Status: episode.StatusMissing,
|
||||
Monitored: true,
|
||||
}
|
||||
c.db.SaveEposideDetail2(episode)
|
||||
}
|
||||
} else { //update episode
|
||||
if ep.Name != epDb.Title || ep.Overview != epDb.Overview || ep.AirDate != epDb.AirDate {
|
||||
log.Infof("update new episode: %+v", ep)
|
||||
c.db.UpdateEpiode2(epDb.ID, ep.Name, ep.Overview, ep.AirDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) isSeedRatioLimitReached(indexId int, t pkg.Torrent) (float64, bool) {
|
||||
indexer, err := c.db.GetIndexer(indexId)
|
||||
if err != nil {
|
||||
return 0, true
|
||||
}
|
||||
currentRatio, err := t.SeedRatio()
|
||||
if err != nil {
|
||||
log.Warnf("get current seed ratio error: %v", err)
|
||||
return currentRatio, indexer.SeedRatio == 0
|
||||
}
|
||||
return currentRatio, currentRatio >= float64(indexer.SeedRatio)
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/torznab"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SearchParam struct {
|
||||
MediaId int
|
||||
SeasonNum int //for tv
|
||||
Episodes []int //for tv
|
||||
CheckResolution bool
|
||||
CheckFileSize bool
|
||||
FilterQiangban bool //for movie, 是否过滤枪版电影
|
||||
}
|
||||
|
||||
func names2Query(media *ent.Media) []string {
|
||||
var names = []string{media.NameEn}
|
||||
|
||||
if media.NameCn != "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if media.NameCn == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, media.NameCn)
|
||||
}
|
||||
|
||||
}
|
||||
if media.OriginalName != "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if media.OriginalName == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, media.OriginalName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, t := range media.AlternativeTitles {
|
||||
if (t.Iso3166_1 == "CN" || t.Iso3166_1 == "US") && t.Type == "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if t.Title == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, t.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("name to query %+v", names)
|
||||
return names
|
||||
}
|
||||
|
||||
func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
series, err := db1.GetMediaDetails(param.MediaId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no tv series of id %v: %v", param.MediaId, err)
|
||||
}
|
||||
limiter, err := db1.GetSizeLimiter("tv")
|
||||
if err != nil {
|
||||
log.Warnf("get tv size limiter: %v", err)
|
||||
limiter = &db.MediaSizeLimiter{}
|
||||
}
|
||||
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
|
||||
|
||||
names := names2Query(series.Media)
|
||||
|
||||
res := searchWithTorznab(db1, SearchTypeTv, names...)
|
||||
|
||||
var filtered []torznab.Result
|
||||
lo:
|
||||
for _, r := range res {
|
||||
//log.Infof("torrent resource: %+v", r)
|
||||
meta := metadata.ParseTv(r.Name)
|
||||
meta.ParseExtraDescription(r.Description)
|
||||
|
||||
if isImdbidNotMatch(series.ImdbID, r.ImdbId) { //has imdb id and not match
|
||||
continue
|
||||
}
|
||||
|
||||
if !imdbIDMatchExact(series.ImdbID, r.ImdbId) { //imdb id not exact match, check file name
|
||||
if !torrentNameOk(series, meta) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !isNoSeasonSeries(series) && meta.Season != param.SeasonNum { //do not check season on series that only rely on episode number
|
||||
continue
|
||||
|
||||
}
|
||||
if isNoSeasonSeries(series) && len(param.Episodes) == 0 {
|
||||
//should not want season
|
||||
continue
|
||||
}
|
||||
|
||||
if len(param.Episodes) > 0 { //not season pack, but episode number not equal
|
||||
if meta.StartEpisode <= 0 {
|
||||
continue lo
|
||||
}
|
||||
for i := meta.StartEpisode; i <= meta.EndEpisode; i++ {
|
||||
if !slices.Contains(param.Episodes, i) {
|
||||
continue lo
|
||||
}
|
||||
}
|
||||
} else if len(param.Episodes) == 0 && !meta.IsSeasonPack { //want season pack, but not season pack
|
||||
continue
|
||||
}
|
||||
|
||||
if param.CheckResolution &&
|
||||
series.Resolution != media.ResolutionAny &&
|
||||
meta.Resolution != series.Resolution.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !torrentSizeOk(series, limiter, r.Size, meta.EndEpisode+1-meta.StartEpisode, param) {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, errors.New("no resource found")
|
||||
}
|
||||
filtered = dedup(filtered)
|
||||
return filtered, nil
|
||||
|
||||
}
|
||||
|
||||
// imdbid not exist consider match
|
||||
func isImdbidNotMatch(id1, id2 string) bool {
|
||||
if id1 == "" || id2 == "" {
|
||||
return false
|
||||
}
|
||||
id1 = strings.TrimPrefix(id1, "tt")
|
||||
id2 = strings.TrimPrefix(id2, "tt")
|
||||
return id1 != id2
|
||||
}
|
||||
|
||||
// imdbid not exist consider not match
|
||||
func imdbIDMatchExact(id1, id2 string) bool {
|
||||
if id1 == "" || id2 == "" {
|
||||
return false
|
||||
}
|
||||
id1 = strings.TrimPrefix(id1, "tt")
|
||||
id2 = strings.TrimPrefix(id2, "tt")
|
||||
return id1 == id2
|
||||
}
|
||||
|
||||
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
|
||||
torrentEpisodeNum int, param *SearchParam) bool {
|
||||
|
||||
multiplier := 1 //大小倍数,正常为1,如果是季包则为季内集数
|
||||
if detail.MediaType == media.MediaTypeTv {
|
||||
if len(param.Episodes) == 0 { //want tv season pack
|
||||
multiplier = seasonEpisodeCount(detail, param.SeasonNum)
|
||||
} else {
|
||||
multiplier = torrentEpisodeNum
|
||||
}
|
||||
}
|
||||
|
||||
if param.CheckFileSize { //check file size when trigger automatic download
|
||||
|
||||
if detail.Limiter.SizeMin > 0 { //min size
|
||||
sizeMin := detail.Limiter.SizeMin * int64(multiplier)
|
||||
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
|
||||
return false
|
||||
}
|
||||
} else if globalLimiter != nil {
|
||||
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
|
||||
sizeMin := resLimiter.MinSize * int64(multiplier)
|
||||
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if detail.Limiter.SizeMax > 0 { //max size
|
||||
sizeMax := detail.Limiter.SizeMax * int64(multiplier)
|
||||
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
|
||||
return false
|
||||
}
|
||||
} else if globalLimiter != nil {
|
||||
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
|
||||
sizeMax := resLimiter.MaxSIze * int64(multiplier)
|
||||
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
|
||||
count := 0
|
||||
for _, ep := range detail.Episodes {
|
||||
if ep.SeasonNumber == seasonNum {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func isNoSeasonSeries(detail *db.MediaDetails) bool {
|
||||
hasSeason2 := false
|
||||
season2HasEpisode1 := false
|
||||
for _, ep := range detail.Episodes {
|
||||
if ep.SeasonNumber == 2 {
|
||||
hasSeason2 = true
|
||||
if ep.EpisodeNumber == 1 {
|
||||
season2HasEpisode1 = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return hasSeason2 && !season2HasEpisode1 //only one 1st episode
|
||||
}
|
||||
|
||||
func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
movieDetail, err := db1.GetMediaDetails(param.MediaId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limiter, err := db1.GetSizeLimiter("movie")
|
||||
if err != nil {
|
||||
log.Warnf("get tv size limiter: %v", err)
|
||||
limiter = &db.MediaSizeLimiter{}
|
||||
}
|
||||
names := names2Query(movieDetail.Media)
|
||||
|
||||
res := searchWithTorznab(db1, SearchTypeMovie, names...)
|
||||
if movieDetail.Extras.IsJav() {
|
||||
res1 := searchWithTorznab(db1, SearchTypeMovie, movieDetail.Extras.JavId)
|
||||
res = append(res, res1...)
|
||||
}
|
||||
|
||||
if len(res) == 0 {
|
||||
return nil, fmt.Errorf("no resource found")
|
||||
}
|
||||
var filtered []torznab.Result
|
||||
for _, r := range res {
|
||||
meta := metadata.ParseMovie(r.Name)
|
||||
|
||||
if isImdbidNotMatch(movieDetail.ImdbID, r.ImdbId) { //imdb id not match
|
||||
continue
|
||||
}
|
||||
|
||||
if !imdbIDMatchExact(movieDetail.ImdbID, r.ImdbId) {
|
||||
if !torrentNameOk(movieDetail, meta) {
|
||||
continue
|
||||
}
|
||||
if !movieDetail.Extras.IsJav() {
|
||||
ss := strings.Split(movieDetail.AirDate, "-")[0]
|
||||
year, _ := strconv.Atoi(ss)
|
||||
if meta.Year != year && meta.Year != year-1 && meta.Year != year+1 { //year not match
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if param.CheckResolution &&
|
||||
movieDetail.Resolution != media.ResolutionAny &&
|
||||
meta.Resolution != movieDetail.Resolution.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
if param.FilterQiangban && meta.IsQingban { //过滤枪版电影
|
||||
continue
|
||||
}
|
||||
|
||||
if !torrentSizeOk(movieDetail, limiter, r.Size, 1, param) {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, r)
|
||||
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, errors.New("no resource found")
|
||||
}
|
||||
filtered = dedup(filtered)
|
||||
|
||||
return filtered, nil
|
||||
|
||||
}
|
||||
|
||||
type SearchType int
|
||||
|
||||
const (
|
||||
SearchTypeTv SearchType = 1
|
||||
SearchTypeMovie SearchType = 2
|
||||
)
|
||||
|
||||
func searchWithTorznab(db *db.Client, t SearchType, queries ...string) []torznab.Result {
|
||||
|
||||
var res []torznab.Result
|
||||
allTorznab := db.GetAllIndexers()
|
||||
|
||||
resChan := make(chan []torznab.Result)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, tor := range allTorznab {
|
||||
if tor.Disabled {
|
||||
continue
|
||||
}
|
||||
if t == SearchTypeTv && !tor.TvSearch {
|
||||
continue
|
||||
}
|
||||
if t == SearchTypeMovie && !tor.MovieSearch {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
log.Debugf("search torznab %v with %v", tor.Name, queries)
|
||||
defer wg.Done()
|
||||
|
||||
resp, err := torznab.Search(tor, q)
|
||||
if err != nil {
|
||||
log.Warnf("search %s with query %s error: %v", tor.Name, q, err)
|
||||
return
|
||||
}
|
||||
resChan <- resp
|
||||
}()
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resChan) // 在所有的worker完成后关闭Channel
|
||||
}()
|
||||
|
||||
for result := range resChan {
|
||||
res = append(res, result...)
|
||||
}
|
||||
|
||||
res = dedup(res)
|
||||
|
||||
sort.SliceStable(res, func(i, j int) bool { //先按做种人数排序
|
||||
var s1 = res[i]
|
||||
var s2 = res[j]
|
||||
return s1.Seeders > s2.Seeders
|
||||
})
|
||||
|
||||
sort.SliceStable(res, func(i, j int) bool { //再按优先级排序,优先级高的种子排前面
|
||||
var s1 = res[i]
|
||||
var s2 = res[j]
|
||||
return s1.Priority > s2.Priority
|
||||
})
|
||||
|
||||
//pt资源中,同一indexer内部,优先下载free的资源
|
||||
sort.SliceStable(res, func(i, j int) bool {
|
||||
var s1 = res[i]
|
||||
var s2 = res[j]
|
||||
if s1.IndexerId == s2.IndexerId && s1.IsPrivate {
|
||||
return s1.DownloadVolumeFactor < s2.DownloadVolumeFactor
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
//同一indexer内部,如果下载消耗一样,则优先下载上传奖励较多的
|
||||
sort.SliceStable(res, func(i, j int) bool {
|
||||
var s1 = res[i]
|
||||
var s2 = res[j]
|
||||
if s1.IndexerId == s2.IndexerId && s1.IsPrivate && s1.DownloadVolumeFactor == s2.DownloadVolumeFactor {
|
||||
return s1.UploadVolumeFactor > s2.UploadVolumeFactor
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func dedup(list []torznab.Result) []torznab.Result {
|
||||
var res = make([]torznab.Result, 0, len(list))
|
||||
seen := make(map[string]bool, 0)
|
||||
for _, r := range list {
|
||||
key := fmt.Sprintf("%s%s%d%d", r.Name, r.Source, r.Seeders, r.Peers)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
res = append(res, r)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type NameTester interface {
|
||||
IsAcceptable(names ...string) bool
|
||||
}
|
||||
|
||||
func torrentNameOk(detail *db.MediaDetails, tester NameTester) bool {
|
||||
if detail.Extras.IsJav() && tester.IsAcceptable(detail.Extras.JavId) {
|
||||
return true
|
||||
}
|
||||
names := names2Query(detail.Media)
|
||||
|
||||
return tester.IsAcceptable(names...)
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/engine"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/torznab"
|
||||
"polaris/server/core"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*string, error) {
|
||||
|
||||
res, err := core.SearchTvSeries(s.db, &core.SearchParam{
|
||||
res, err := engine.SearchTvSeries(s.db, &engine.SearchParam{
|
||||
MediaId: seriesId,
|
||||
SeasonNum: seasonNum,
|
||||
Episodes: nil,
|
||||
@@ -54,7 +54,7 @@ func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) {
|
||||
if in.Episode == 0 {
|
||||
//search season package
|
||||
log.Infof("search series season package S%02d", in.Season)
|
||||
res, err = core.SearchTvSeries(s.db, &core.SearchParam{
|
||||
res, err = engine.SearchTvSeries(s.db, &engine.SearchParam{
|
||||
MediaId: in.ID,
|
||||
SeasonNum: in.Season,
|
||||
Episodes: nil,
|
||||
@@ -64,7 +64,7 @@ func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) {
|
||||
}
|
||||
} else {
|
||||
log.Infof("search series episode S%02dE%02d", in.Season, in.Episode)
|
||||
res, err = core.SearchTvSeries(s.db, &core.SearchParam{
|
||||
res, err = engine.SearchTvSeries(s.db, &engine.SearchParam{
|
||||
MediaId: in.ID,
|
||||
SeasonNum: in.Season,
|
||||
Episodes: []int{in.Episode},
|
||||
@@ -85,7 +85,7 @@ func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) {
|
||||
allowQiangban = true
|
||||
}
|
||||
|
||||
res, err = core.SearchMovie(s.db, &core.SearchParam{
|
||||
res, err = engine.SearchMovie(s.db, &engine.SearchParam{
|
||||
MediaId: in.ID,
|
||||
FilterQiangban: !allowQiangban,
|
||||
})
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"polaris/db"
|
||||
"polaris/engine"
|
||||
"polaris/log"
|
||||
"polaris/pkg/cache"
|
||||
"polaris/pkg/tmdb"
|
||||
"polaris/server/core"
|
||||
"polaris/ui"
|
||||
"time"
|
||||
|
||||
@@ -30,14 +30,14 @@ func NewServer(db *db.Client) *Server {
|
||||
monitorNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
downloadNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
}
|
||||
s.core = core.NewClient(db, s.language)
|
||||
s.core = engine.NewEngine(db, s.language)
|
||||
return s
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
r *gin.Engine
|
||||
db *db.Client
|
||||
core *core.Client
|
||||
core *engine.Engine
|
||||
language string
|
||||
jwtSerect string
|
||||
monitorNumCache *cache.Cache[int, int]
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/db"
|
||||
"polaris/engine"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/server/core"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -68,7 +68,7 @@ type addWatchlistIn struct {
|
||||
}
|
||||
|
||||
func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
|
||||
var in core.AddWatchlistIn
|
||||
var in engine.AddWatchlistIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind query")
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
|
||||
}
|
||||
|
||||
func (s *Server) AddMovie2Watchlist(c *gin.Context) (interface{}, error) {
|
||||
var in core.AddWatchlistIn
|
||||
var in engine.AddWatchlistIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind query")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user