mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-09 03:27:39 +08:00
refactor
This commit is contained in:
238
internal/biz/engine/client.go
Normal file
238
internal/biz/engine/client.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"polaris/internal/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 NewEngine(db db.Database, language string) *Engine {
|
||||
return &Engine{
|
||||
db: db,
|
||||
cron: cron.New(),
|
||||
tasks: utils.Map[int, *Task]{},
|
||||
schedulers: utils.Map[string, scheduler]{},
|
||||
language: language,
|
||||
}
|
||||
}
|
||||
|
||||
type scheduler struct {
|
||||
cron string
|
||||
f func() error
|
||||
}
|
||||
type Engine struct {
|
||||
db db.Database
|
||||
cron *cron.Cron
|
||||
tasks utils.Map[int, *Task]
|
||||
language string
|
||||
schedulers utils.Map[string, scheduler]
|
||||
buildin *buildin.Downloader
|
||||
}
|
||||
|
||||
func (c *Engine) registerCronJob(name string, cron string, f func() error) {
|
||||
c.schedulers.Store(name, scheduler{
|
||||
cron: cron,
|
||||
f: f,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Engine) Init() {
|
||||
go c.reloadTasks()
|
||||
c.addSysCron()
|
||||
go c.checkW500PosterOnStartup()
|
||||
}
|
||||
|
||||
func (c *Engine) GetTask(id int) (*Task, bool) {
|
||||
return c.tasks.Load(id)
|
||||
}
|
||||
|
||||
func (c *Engine) reloadUsingBuildinDownloader(h *ent.History) error {
|
||||
cl, err := c.buildInDownloader()
|
||||
if err != nil {
|
||||
log.Warnf("buildin downloader error: %v", err)
|
||||
}
|
||||
t, err := cl.Download(h.Link, h.Hash, c.db.GetDownloadDir())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "download torrent")
|
||||
}
|
||||
t.Start()
|
||||
|
||||
c.tasks.Store(h.ID, &Task{Torrent: t})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Engine) reloadTasks() {
|
||||
allTasks := c.db.GetRunningHistories()
|
||||
for _, t := range allTasks {
|
||||
if t.DownloadClientID == 0 {
|
||||
log.Warnf("assume buildin downloader: %v", t.SourceTitle)
|
||||
err := c.reloadUsingBuildinDownloader(t)
|
||||
if err != nil {
|
||||
log.Warnf("buildin downloader error: %v", err)
|
||||
} else {
|
||||
log.Infof("success reloading buildin task: %v", t.SourceTitle)
|
||||
}
|
||||
continue
|
||||
}
|
||||
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.Store(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.Store(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.Store(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.Store(t.ID, &Task{Torrent: to})
|
||||
}
|
||||
} else if dl.Implementation == downloadclients.ImplementationBuildin {
|
||||
err := c.reloadUsingBuildinDownloader(t)
|
||||
if err != nil {
|
||||
log.Warnf("buildin downloader error: %v", err)
|
||||
} else {
|
||||
log.Infof("success reloading buildin task: %v", t.SourceTitle)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
log.Infof("------ task reloading done ------")
|
||||
}
|
||||
|
||||
func (c *Engine) buildInDownloader() (pkg.Downloader, error) {
|
||||
if c.buildin != nil {
|
||||
return c.buildin, nil
|
||||
}
|
||||
dir := c.db.GetDownloadDir()
|
||||
d, err := buildin.NewDownloader(dir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "buildin downloader")
|
||||
}
|
||||
c.buildin = d
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (c *Engine) 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", err)
|
||||
continue
|
||||
}
|
||||
return bin, d, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, errors.Errorf("no available download client")
|
||||
}
|
||||
|
||||
func (c *Engine) 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 *Engine) MustTMDB() *tmdb.Client {
|
||||
t, err := c.TMDB()
|
||||
if err != nil {
|
||||
log.Panicf("get tmdb: %v", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (c *Engine) RemoveTaskAndTorrent(id int) error {
|
||||
torrent, ok := c.tasks.Load(id)
|
||||
if ok {
|
||||
if err := torrent.Remove(); err != nil {
|
||||
return errors.Wrap(err, "remove torrent")
|
||||
}
|
||||
c.tasks.Delete(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Engine) GetTasks() *utils.Map[int, *Task] {
|
||||
return &c.tasks
|
||||
}
|
||||
1
internal/biz/engine/fliters.go
Normal file
1
internal/biz/engine/fliters.go
Normal file
@@ -0,0 +1 @@
|
||||
package engine
|
||||
594
internal/biz/engine/importlist.go
Normal file
594
internal/biz/engine/importlist.go
Normal file
@@ -0,0 +1,594 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/internal/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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) downloadBackdrop(path string, mediaID int) error {
|
||||
url := "https://image.tmdb.org/t/p/original" + path
|
||||
return c.downloadImage(url, mediaID, "backdrop.jpg")
|
||||
}
|
||||
|
||||
func (c *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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), " ")
|
||||
}
|
||||
79
internal/biz/engine/indexer.go
Normal file
79
internal/biz/engine/indexer.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"polaris/ent"
|
||||
"polaris/log"
|
||||
"polaris/pkg/prowlarr"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const prowlarrPrefix = "Prowlarr_"
|
||||
|
||||
func (c *Engine) 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 *Engine) 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 *Engine) 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
|
||||
}
|
||||
287
internal/biz/engine/integration.go
Normal file
287
internal/biz/engine/integration.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"polaris/internal/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 *Engine) 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 *Engine) 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 *Engine) plexmatchEnabled() bool {
|
||||
return c.db.GetSetting(db.SettingPlexMatchEnabled) == "true"
|
||||
}
|
||||
|
||||
func (c *Engine) nfoSupportEnabled() bool {
|
||||
return c.db.GetSetting(db.SettingNfoSupportEnabled) == "true"
|
||||
}
|
||||
|
||||
func (c *Engine) 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 *Engine) 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 *Engine) findEpisodeFilesPreMoving(historyId int) error {
|
||||
his := c.db.GetHistory(historyId)
|
||||
|
||||
episodeIds := c.GetEpisodeIds(his)
|
||||
|
||||
task, _ := c.tasks.Load(historyId)
|
||||
|
||||
ff, err := c.db.GetAcceptedVideoFormats()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range episodeIds {
|
||||
ep, err := c.db.GetEpisodeByID(id)
|
||||
if err != nil {
|
||||
log.Warnf("query episode error (%d): %v", id, err)
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
253
internal/biz/engine/nfo.go
Normal file
253
internal/biz/engine/nfo.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package engine
|
||||
|
||||
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"`
|
||||
}
|
||||
246
internal/biz/engine/resources.go
Normal file
246
internal/biz/engine/resources.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package engine
|
||||
|
||||
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 *Engine) DownloadEpisodeTorrent(r1 torznab.Result, op DownloadOptions) (*string, error) {
|
||||
|
||||
series, err := c.db.GetMedia(op.MediaId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no tv series of id %v", op.MediaId)
|
||||
}
|
||||
|
||||
return c.downloadTorrent(series, r1, op)
|
||||
}
|
||||
|
||||
/*
|
||||
tmdb 校验获取的资源名,如果用资源名在tmdb搜索出来的结果能匹配上想要的资源,则认为资源有效,否则无效
|
||||
解决名称过于简单的影视会匹配过多资源的问题, 例如:梦魇绝镇 FROM
|
||||
*/
|
||||
func (c *Engine) 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 *Engine) 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, DownloadOptions{
|
||||
SeasonNum: seasonNum,
|
||||
MediaId: seriesId,
|
||||
HashFilterFn: c.hashInBlacklist,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("download season pack error, continue next item: %v", err)
|
||||
continue lo
|
||||
}
|
||||
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, DownloadOptions{
|
||||
SeasonNum: seasonNum,
|
||||
MediaId: seriesId,
|
||||
EpisodeNums: torrentEpisodes,
|
||||
HashFilterFn: c.hashInBlacklist,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("download episode error, continue next item: %v", err)
|
||||
continue lo
|
||||
}
|
||||
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 *Engine) DownloadMovie(m *ent.Media, r1 torznab.Result) (*string, error) {
|
||||
return c.downloadTorrent(m, r1, DownloadOptions{
|
||||
SeasonNum: 0,
|
||||
MediaId: m.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Engine) hashInBlacklist(hash string) bool {
|
||||
blacklist, err := c.db.GetTorrentBlacklist()
|
||||
if err!= nil {
|
||||
log.Warnf("get torrent blacklist error: %v", err)
|
||||
return false
|
||||
}
|
||||
for _, b := range blacklist {
|
||||
if b.TorrentHash == hash {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Engine) downloadTorrent(m *ent.Media, r1 torznab.Result, op DownloadOptions) (*string, error) {
|
||||
trc, dlc, err := c.GetDownloadClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get download client")
|
||||
}
|
||||
|
||||
downloadDir := c.db.GetDownloadDir()
|
||||
|
||||
//due to reported bug by user, this will be temporarily disabled
|
||||
// size := utils.AvailableSpace(downloadDir)
|
||||
// if size < uint64(r1.Size) {
|
||||
// log.Errorf("space available %v, space needed %v", size, r1.Size)
|
||||
// return nil, errors.New("not 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, op.SeasonNum)
|
||||
|
||||
if len(op.EpisodeNums) > 0 {
|
||||
for _, epNum := range op.EpisodeNums {
|
||||
ep, err := c.db.GetEpisode(m.ID, op.SeasonNum, epNum)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("no episode of season %d episode %d", op.SeasonNum, epNum)
|
||||
|
||||
}
|
||||
if ep.Status == episode.StatusMissing {
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
}
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
for i, ep := range op.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, op.SeasonNum, episode.StatusDownloading)
|
||||
}
|
||||
|
||||
} else {//movie download
|
||||
ep, _ := c.db.GetMovieDummyEpisode(m.ID)
|
||||
if ep.Status == episode.StatusMissing {
|
||||
c.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
link, hash, err := utils.GetRealLinkAndHash(r1.Link)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get hash")
|
||||
}
|
||||
|
||||
if op.HashFilterFn != nil && op.HashFilterFn(hash) {
|
||||
return nil, errors.Errorf("hash is filtered: %s", hash)
|
||||
}
|
||||
|
||||
r1.Link = link
|
||||
|
||||
history, err := c.db.SaveHistoryRecord(ent.History{
|
||||
MediaID: m.ID,
|
||||
EpisodeNums: op.EpisodeNums,
|
||||
SeasonNum: op.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.Store(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
|
||||
}
|
||||
572
internal/biz/engine/scheduler.go
Normal file
572
internal/biz/engine/scheduler.go
Normal file
@@ -0,0 +1,572 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/internal/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"
|
||||
)
|
||||
|
||||
type DownloadOptions struct {
|
||||
HashFilterFn func(hash string) bool
|
||||
SeasonNum int
|
||||
EpisodeNums []int
|
||||
MediaId int
|
||||
}
|
||||
|
||||
func (c *Engine) 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 */20 * * * *", 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 *Engine) 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 *Engine) 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 *Engine) checkTasks() error {
|
||||
log.Debug("begin check tasks...")
|
||||
c.tasks.Range(func(id int, t *Task) bool {
|
||||
r := c.db.GetHistory(id)
|
||||
if !t.Exists() {
|
||||
log.Infof("task no longer exists: %v", id)
|
||||
|
||||
c.tasks.Delete(id)
|
||||
return true
|
||||
}
|
||||
name, err := t.Name()
|
||||
if err != nil {
|
||||
log.Warnf("get task name error: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
progress, err := t.Progress()
|
||||
if err != nil {
|
||||
log.Warnf("get task progress error: %v", err)
|
||||
return true
|
||||
}
|
||||
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.Load(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()
|
||||
c.tasks.Delete(id)
|
||||
c.setHistoryStatus(id, history.StatusSuccess)
|
||||
} else {
|
||||
log.Infof("torrent file still sedding: %v, current seed ratio: %v", name, ratio)
|
||||
}
|
||||
return true
|
||||
} 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 true
|
||||
})
|
||||
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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) GetEpisodeIds(r *ent.History) []int {
|
||||
|
||||
series, err := c.db.GetMediaDetails(r.MediaID)
|
||||
if err != nil {
|
||||
log.Errorf("get media details error: %v", err)
|
||||
return []int{}
|
||||
}
|
||||
if series.MediaType == media.MediaTypeMovie { //movie
|
||||
ep, _ := c.db.GetMovieDummyEpisode(series.ID)
|
||||
return []int{ep.ID}
|
||||
} else { //tv
|
||||
var episodeIds []int
|
||||
seasonNum := getSeasonNum(r)
|
||||
|
||||
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 *Engine) moveCompletedTask(id int) (err1 error) {
|
||||
torrent, _ := c.tasks.Load(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")
|
||||
c.tasks.Delete(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)
|
||||
c.tasks.Delete(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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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 *Engine) 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, DownloadOptions{MediaId: m.ID, SeasonNum: 0, HashFilterFn: c.hashInBlacklist})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *s, nil
|
||||
}
|
||||
|
||||
func (c *Engine) 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 *Engine) 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 *Engine) 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)
|
||||
}
|
||||
56
internal/biz/engine/stun.go
Normal file
56
internal/biz/engine/stun.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"polaris/ent/downloadclients"
|
||||
"polaris/log"
|
||||
"polaris/pkg/nat"
|
||||
"polaris/pkg/qbittorrent"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
)
|
||||
|
||||
func (s *Engine) stunProxyDownloadClient() error {
|
||||
|
||||
return s.StartStunProxy("")
|
||||
}
|
||||
|
||||
|
||||
func (s *Engine) StartStunProxy(name string) error {
|
||||
downloaders := s.db.GetAllDonloadClients()
|
||||
for _, d := range downloaders {
|
||||
if !d.Enable {
|
||||
continue
|
||||
}
|
||||
if !d.UseNatTraversal {
|
||||
continue
|
||||
}
|
||||
if name != "" && d.Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
if d.Implementation != downloadclients.ImplementationQbittorrent { //TODO only support qbittorrent for now
|
||||
continue
|
||||
}
|
||||
|
||||
qbt, err := qbittorrent.NewClient(d.URL, d.User, d.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to download client error: %v", d.URL)
|
||||
}
|
||||
u, err := url.Parse(d.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("start stun proxy for %s", d.Name)
|
||||
n, err := nat.NewNatTraversal(func(xa stun.XORMappedAddress) error {
|
||||
return qbt.SetListenPort(xa.Port)
|
||||
}, u.Hostname())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.StartProxy()
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
460
internal/biz/engine/torrent.go
Normal file
460
internal/biz/engine/torrent.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/internal/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/torznab"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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 getSeasonReleaseYear(series *db.MediaDetails, seasonNum int) int {
|
||||
if seasonNum == 0 {
|
||||
return 0
|
||||
}
|
||||
releaseYear := 0
|
||||
for _, s := range series.Episodes {
|
||||
if s.SeasonNumber == seasonNum && s.AirDate != "" {
|
||||
ss := strings.Split(s.AirDate, "-")[0]
|
||||
y, err := strconv.Atoi(ss)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
releaseYear = y
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
return releaseYear
|
||||
}
|
||||
|
||||
func SearchTvSeries(db1 db.Database, 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...)
|
||||
|
||||
ss := strings.Split(series.AirDate, "-")[0]
|
||||
releaseYear, _ := strconv.Atoi(ss)
|
||||
|
||||
seasonYear := getSeasonReleaseYear(series, param.SeasonNum)
|
||||
|
||||
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 meta.Year > 0 && releaseYear > 0 {
|
||||
if meta.Year != releaseYear && meta.Year != releaseYear-1 && meta.Year != releaseYear+1 { //year not match
|
||||
if seasonYear > 0 { // if tv release year is not match, check season release year
|
||||
if meta.Year != seasonYear && meta.Year != seasonYear-1 && meta.Year != seasonYear+1 { //season year not match
|
||||
continue lo
|
||||
}
|
||||
} else {
|
||||
continue lo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.Database, 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.Database, t SearchType, queries ...string) []torznab.Result {
|
||||
t1 := time.Now()
|
||||
defer func() {
|
||||
log.Infof("search with torznab took %v", time.Since(t1))
|
||||
}()
|
||||
|
||||
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...)
|
||||
}
|
||||
207
internal/biz/server/activity.go
Normal file
207
internal/biz/server/activity.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/internal/biz/engine"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/log"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Activity struct {
|
||||
*ent.History
|
||||
Progress int `json:"progress"`
|
||||
SeedRatio float64 `json:"seed_ratio"`
|
||||
UploadProgress float64 `json:"upload_progress"`
|
||||
}
|
||||
|
||||
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||
q := c.Query("status")
|
||||
var activities = make([]Activity, 0)
|
||||
if q == "active" {
|
||||
his := s.db.GetRunningHistories()
|
||||
for _, h := range his {
|
||||
a := Activity{
|
||||
History: h,
|
||||
}
|
||||
tasks := s.core.GetTasks()
|
||||
tasks.Range(func(id int, task *engine.Task) bool {
|
||||
if h.ID == id && task.Exists() {
|
||||
p, err := task.Progress()
|
||||
if err != nil {
|
||||
log.Warnf("get task progress error: %v", err)
|
||||
} else {
|
||||
a.Progress = p
|
||||
}
|
||||
r, err := task.SeedRatio()
|
||||
if err != nil {
|
||||
log.Warnf("get task seed ratio error: %v", err)
|
||||
} else {
|
||||
a.SeedRatio = r
|
||||
}
|
||||
if task.UploadProgresser != nil {
|
||||
a.UploadProgress = task.UploadProgresser()
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
activities = append(activities, a)
|
||||
}
|
||||
} else {
|
||||
his := s.db.GetHistories()
|
||||
for _, h := range his {
|
||||
if h.Status == history.StatusRunning || h.Status == history.StatusUploading || h.Status == history.StatusSeeding {
|
||||
continue //archived downloads
|
||||
}
|
||||
|
||||
a := Activity{
|
||||
History: h,
|
||||
}
|
||||
activities = append(activities, a)
|
||||
}
|
||||
|
||||
}
|
||||
return activities, nil
|
||||
}
|
||||
|
||||
type removeActivityIn struct {
|
||||
ID int `json:"id"`
|
||||
Add2Blacklist bool `json:"add_2_blacklist"`
|
||||
}
|
||||
|
||||
func (s *Server) RemoveActivity(c *gin.Context) (interface{}, error) {
|
||||
var in removeActivityIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
|
||||
his := s.db.GetHistory(in.ID)
|
||||
if his == nil {
|
||||
log.Errorf("no record of id: %d", in.ID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := s.core.RemoveTaskAndTorrent(his.ID); err != nil {
|
||||
return nil, errors.Wrap(err, "remove torrent")
|
||||
}
|
||||
if his.Status == history.StatusSeeding {
|
||||
//seeding, will mark as complete
|
||||
log.Infof("history is now seeding, will only mark history as success: (%d) %s", his.ID, his.SourceTitle)
|
||||
if err := s.db.SetHistoryStatus(his.ID, history.StatusSuccess); err!= nil {
|
||||
return nil, errors.Wrap(err, "set status")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if in.Add2Blacklist {
|
||||
//should add to blacklist
|
||||
if err := s.addTorrent2Blacklist(his); err != nil {
|
||||
return nil, errors.Errorf("add to blacklist: %v", err)
|
||||
} else {
|
||||
log.Infof("success add magnet link to blacklist: %v", his.Link)
|
||||
}
|
||||
}
|
||||
|
||||
err := s.db.DeleteHistory(in.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
|
||||
episodeIds := s.core.GetEpisodeIds(his)
|
||||
|
||||
for _, id := range episodeIds {
|
||||
ep, err := s.db.GetEpisodeByID(id)
|
||||
if err != nil {
|
||||
log.Warnf("get episode (%d) error: %v", id, err)
|
||||
continue
|
||||
}
|
||||
if !s.db.IsEpisodeDownloadingOrDownloaded(id) && ep.Status != episode.StatusDownloaded {
|
||||
//没有正在下载中或者下载完成的任务,并且episode状态不是已经下载完成
|
||||
log.Debugf("set episode (%d) status to missing", id)
|
||||
s.db.SetEpisodeStatus(id, episode.StatusMissing)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("history record successful deleted: %v", his.SourceTitle)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) addTorrent2Blacklist(h *ent.History) error {
|
||||
if h.Hash == "" { //没有hash,不添加
|
||||
return nil
|
||||
}
|
||||
return s.db.AddTorrent2Blacklist(h.Hash, h.SourceTitle, h.MediaID)
|
||||
}
|
||||
|
||||
func (s *Server) GetAllBlacklistItems(c *gin.Context) (interface{}, error) {
|
||||
list, err := s.db.GetTorrentBlacklist()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
func (s *Server) RemoveBlacklistItem(c *gin.Context) (interface{}, error) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("id is empty")
|
||||
}
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not int: %v", id)
|
||||
}
|
||||
if err := s.db.DeleteTorrentBlacklist(idInt); err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetMediaDownloadHistory(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
his, err := s.db.GetDownloadHistory(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return his, nil
|
||||
}
|
||||
|
||||
type TorrentInfo struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
SeedRatio float32 `json:"seed_ratio"`
|
||||
Progress int `json:"progress"`
|
||||
}
|
||||
|
||||
func (s *Server) GetAllTorrents(c *gin.Context) (interface{}, error) {
|
||||
trc, _, err := s.core.GetDownloadClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "connect transmission")
|
||||
}
|
||||
all, err := trc.GetAll()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get all")
|
||||
}
|
||||
var infos []TorrentInfo
|
||||
for _, t := range all {
|
||||
if !t.Exists() {
|
||||
continue
|
||||
}
|
||||
name, _ := t.Name()
|
||||
p, _ := t.Progress()
|
||||
infos = append(infos, TorrentInfo{
|
||||
Name: name,
|
||||
ID: t.GetHash(),
|
||||
Progress: p,
|
||||
})
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
148
internal/biz/server/auth.go
Normal file
148
internal/biz/server/auth.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"polaris/internal/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) isAuthEnabled() bool {
|
||||
authEnabled := s.db.GetSetting(db.SettingAuthEnabled)
|
||||
return authEnabled == "true"
|
||||
}
|
||||
|
||||
func (s *Server) authModdleware(c *gin.Context) {
|
||||
if !s.isAuthEnabled() {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
token, err := c.Cookie("polaris_token")
|
||||
if err != nil {
|
||||
log.Errorf("token error: %v", err)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
//log.Debugf("current token: %v", auth)
|
||||
tokenParsed, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(s.jwtSerect), nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("parse token error: %v", err)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !tokenParsed.Valid {
|
||||
log.Errorf("token is not valid: %v", token)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
claim := tokenParsed.Claims.(*jwt.RegisteredClaims)
|
||||
|
||||
if time.Until(claim.ExpiresAt.Time) <= 0 {
|
||||
log.Infof("token is no longer valid: %s", token)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
|
||||
}
|
||||
|
||||
type LoginIn struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (s *Server) Login(c *gin.Context) (interface{}, error) {
|
||||
var in LoginIn
|
||||
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
|
||||
if !s.isAuthEnabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user := s.db.GetSetting(db.SettingUsername)
|
||||
if user != in.User {
|
||||
return nil, errors.New("login fail")
|
||||
}
|
||||
password := s.db.GetSetting(db.SettingPassword)
|
||||
if !utils.VerifyPassword(in.Password, password) {
|
||||
return nil, errors.New("login fail")
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
Issuer: "system",
|
||||
Subject: in.User,
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
})
|
||||
sig, err := token.SignedString([]byte(s.jwtSerect))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sign")
|
||||
}
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie("polaris_token", sig, 0, "/", "", false, false)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) Logout(c *gin.Context) (interface{}, error) {
|
||||
if !s.isAuthEnabled() {
|
||||
return nil, errors.New( "auth is not enabled")
|
||||
}
|
||||
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie("polaris_token", "", -1, "/", "", false, false)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type EnableAuthIn struct {
|
||||
Enable bool `json:"enable"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (s *Server) EnableAuth(c *gin.Context) (interface{}, error) {
|
||||
var in EnableAuthIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
|
||||
if in.Enable && (in.User == "" || in.Password == "") {
|
||||
return nil, errors.New("user password should not empty")
|
||||
}
|
||||
if !in.Enable {
|
||||
log.Infof("disable auth")
|
||||
s.db.SetSetting(db.SettingAuthEnabled, "false")
|
||||
} else {
|
||||
log.Info("enable auth")
|
||||
s.db.SetSetting(db.SettingAuthEnabled, "true")
|
||||
s.db.SetSetting(db.SettingUsername, in.User)
|
||||
|
||||
hash, err := utils.HashPassword(in.Password)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hash password")
|
||||
}
|
||||
s.db.SetSetting(db.SettingPassword, hash)
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAuthSetting(c *gin.Context) (interface{}, error) {
|
||||
enabled := s.db.GetSetting(db.SettingAuthEnabled)
|
||||
user := s.db.GetSetting(db.SettingUsername)
|
||||
|
||||
return EnableAuthIn{
|
||||
Enable: enabled == "true",
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
51
internal/biz/server/common.go
Normal file
51
internal/biz/server/common.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"polaris/log"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Coder interface {
|
||||
Code() int
|
||||
}
|
||||
|
||||
|
||||
func HttpHandler(f func(*gin.Context) (interface{}, error)) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
|
||||
r, err := f(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("url %v return error: %v", ctx.Request.URL, err)
|
||||
cc, ok := err.(Coder)
|
||||
if ok {
|
||||
ctx.JSON(200, Response{
|
||||
Code: cc.Code(),
|
||||
Message: fmt.Sprintf("%v", err),
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
ctx.JSON(200, Response{
|
||||
Code: 1,
|
||||
Message: fmt.Sprintf("%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Debugf("url %v return: %+v", ctx.Request.URL, r)
|
||||
|
||||
ctx.JSON(200, Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: r,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
63
internal/biz/server/importlist.go
Normal file
63
internal/biz/server/importlist.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/ent"
|
||||
"polaris/ent/importlist"
|
||||
"polaris/pkg/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) getAllImportLists(c *gin.Context) (interface{}, error) {
|
||||
lists, err := s.db.GetAllImportLists()
|
||||
return lists, err
|
||||
}
|
||||
|
||||
type addImportlistIn struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Url string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
Qulity string `json:"qulity"`
|
||||
StorageId int `json:"storage_id"`
|
||||
}
|
||||
|
||||
func (s *Server) addImportlist(c *gin.Context) (interface{}, error) {
|
||||
var in addImportlistIn
|
||||
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
st := s.db.GetStorage(in.StorageId)
|
||||
if st == nil {
|
||||
return nil, fmt.Errorf("storage id not exist: %v", in.StorageId)
|
||||
}
|
||||
err := s.db.AddImportlist(&ent.ImportList{
|
||||
Name: in.Name,
|
||||
URL: in.Url,
|
||||
Type: importlist.Type(in.Type),
|
||||
Qulity: in.Qulity,
|
||||
StorageID: in.StorageId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
type deleteImportlistIn struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
func (s *Server) deleteImportList(c *gin.Context) (interface{}, error) {
|
||||
var in deleteImportlistIn
|
||||
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "json")
|
||||
}
|
||||
s.db.DeleteImportlist(in.ID)
|
||||
return "sucess", nil
|
||||
}
|
||||
46
internal/biz/server/notify_client.go
Normal file
46
internal/biz/server/notify_client.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"polaris/ent"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) GetAllNotificationClients(c *gin.Context) (interface{}, error) {
|
||||
return s.db.GetAllNotificationClients()
|
||||
}
|
||||
|
||||
func (s *Server) GetNotificationClient(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
return s.db.GetNotificationClient(id)
|
||||
}
|
||||
|
||||
func (s *Server) DeleteNotificationClient(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
return nil, s.db.DeleteNotificationClient(id)
|
||||
}
|
||||
|
||||
func (s *Server) AddNotificationClient(c *gin.Context) (interface{}, error) {
|
||||
var in ent.NotificationClient
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
err := s.db.AddNotificationClient(in.Name, in.Service, in.Settings, in.Enabled)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "save db")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
235
internal/biz/server/resources.go
Normal file
235
internal/biz/server/resources.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/internal/db"
|
||||
"polaris/internal/biz/engine"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/torznab"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) searchAndDownloadSeasonPackage(seriesId, seasonNum int) (*string, error) {
|
||||
|
||||
res, err := engine.SearchTvSeries(s.db, &engine.SearchParam{
|
||||
MediaId: seriesId,
|
||||
SeasonNum: seasonNum,
|
||||
Episodes: nil,
|
||||
CheckResolution: true,
|
||||
CheckFileSize: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r1 := res[0]
|
||||
log.Infof("found resource to download: %+v", r1)
|
||||
return s.core.DownloadEpisodeTorrent(r1, engine.DownloadOptions{
|
||||
SeasonNum: seasonNum,
|
||||
MediaId: seriesId,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type searchAndDownloadIn struct {
|
||||
ID int `json:"id" binding:"required"`
|
||||
Season int `json:"season"`
|
||||
Episode int `json:"episode"`
|
||||
}
|
||||
|
||||
func (s *Server) SearchAvailableTorrents(c *gin.Context) (interface{}, error) {
|
||||
var in searchAndDownloadIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
m, err := s.db.GetMedia(in.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get media")
|
||||
}
|
||||
log.Infof("search torrents resources link: %+v", in)
|
||||
|
||||
var res []torznab.Result
|
||||
if m.MediaType == media.MediaTypeTv {
|
||||
if in.Episode == 0 {
|
||||
//search season package
|
||||
log.Infof("search series season package S%02d", in.Season)
|
||||
res, err = engine.SearchTvSeries(s.db, &engine.SearchParam{
|
||||
MediaId: in.ID,
|
||||
SeasonNum: in.Season,
|
||||
Episodes: nil,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "search season package")
|
||||
}
|
||||
} else {
|
||||
log.Infof("search series episode S%02dE%02d", in.Season, in.Episode)
|
||||
res, err = engine.SearchTvSeries(s.db, &engine.SearchParam{
|
||||
MediaId: in.ID,
|
||||
SeasonNum: in.Season,
|
||||
Episodes: []int{in.Episode},
|
||||
})
|
||||
if err != nil {
|
||||
if err.Error() == "no resource found" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "search episode")
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
log.Info("search movie %d", in.ID)
|
||||
qiangban := s.db.GetSetting(db.SettingAllowQiangban)
|
||||
allowQiangban := false
|
||||
if qiangban == "true" {
|
||||
allowQiangban = true
|
||||
}
|
||||
|
||||
res, err = engine.SearchMovie(s.db, &engine.SearchParam{
|
||||
MediaId: in.ID,
|
||||
FilterQiangban: !allowQiangban,
|
||||
})
|
||||
if err != nil {
|
||||
if err.Error() == "no resource found" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) SearchTvAndDownload(c *gin.Context) (interface{}, error) {
|
||||
var in searchAndDownloadIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
log.Infof("search episode resources link: %v", in)
|
||||
var name string
|
||||
if in.Episode == 0 {
|
||||
log.Infof("season package search")
|
||||
//search season package
|
||||
name1, err := s.searchAndDownloadSeasonPackage(in.ID, in.Season)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "download")
|
||||
}
|
||||
name = *name1
|
||||
} else {
|
||||
log.Infof("season episode search")
|
||||
name1, err := s.core.SearchAndDownload(in.ID, in.Season, in.Episode)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "download")
|
||||
}
|
||||
if len(name1) == 0 {
|
||||
return nil, fmt.Errorf("no torrent found")
|
||||
}
|
||||
name = name1[0]
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"name": name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type downloadTorrentIn struct {
|
||||
MediaID int `json:"id" binding:"required"`
|
||||
Season int `json:"season"`
|
||||
Episode int `json:"episode"`
|
||||
torznab.Result
|
||||
}
|
||||
|
||||
func (s *Server) DownloadTorrent(c *gin.Context) (interface{}, error) {
|
||||
var in downloadTorrentIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
log.Infof("download torrent input: %+v", in)
|
||||
|
||||
m, err := s.db.GetMedia(in.MediaID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no tv series of id %v", in.MediaID)
|
||||
}
|
||||
if m.MediaType == media.MediaTypeTv {
|
||||
if in.Episode == 0 {
|
||||
//download season package
|
||||
name := in.Name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("%v S%02d", m.OriginalName, in.Season)
|
||||
}
|
||||
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size}
|
||||
return s.core.DownloadEpisodeTorrent(res, engine.DownloadOptions{
|
||||
SeasonNum: in.Season,
|
||||
MediaId: in.MediaID,
|
||||
})
|
||||
}
|
||||
name := in.Name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("%v S%02dE%02d", m.OriginalName, in.Season, in.Episode)
|
||||
}
|
||||
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size, IndexerId: in.IndexerId}
|
||||
return s.core.DownloadEpisodeTorrent(res, engine.DownloadOptions{
|
||||
SeasonNum: in.Season,
|
||||
MediaId: in.MediaID,
|
||||
EpisodeNums: []int{in.Episode},
|
||||
})
|
||||
} else {
|
||||
//movie
|
||||
name := in.Name
|
||||
if name == "" {
|
||||
name = m.OriginalName
|
||||
}
|
||||
|
||||
res := torznab.Result{Name: name, Link: in.Link, Size: in.Size, IndexerId: in.IndexerId}
|
||||
return s.core.DownloadMovie(m, res)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
return s.downloadAllEpisodes(id)
|
||||
}
|
||||
|
||||
func (s *Server) downloadAllEpisodes(id int) (interface{}, error) {
|
||||
m, err := s.db.GetMedia(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get media")
|
||||
}
|
||||
if m.MediaType == media.MediaTypeTv {
|
||||
return s.core.DownloadSeriesAllEpisodes(m.ID), nil
|
||||
}
|
||||
name, err := s.core.DownloadMovieByID(m.ID)
|
||||
|
||||
return []string{name}, err
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAllTv(c *gin.Context) (interface{}, error) {
|
||||
tvs := s.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
var allNames []string
|
||||
for _, tv := range tvs {
|
||||
names, err := s.downloadAllEpisodes(tv.ID)
|
||||
if err == nil {
|
||||
allNames = append(allNames, names.([]string)...)
|
||||
}
|
||||
}
|
||||
return allNames, nil
|
||||
}
|
||||
|
||||
func (s *Server) DownloadAllMovies(c *gin.Context) (interface{}, error) {
|
||||
movies := s.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
var allNames []string
|
||||
for _, mv := range movies {
|
||||
names, err := s.downloadAllEpisodes(mv.ID)
|
||||
if err == nil {
|
||||
allNames = append(allNames, names.([]string)...)
|
||||
}
|
||||
}
|
||||
return allNames, nil
|
||||
}
|
||||
214
internal/biz/server/server.go
Normal file
214
internal/biz/server/server.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"polaris/internal/db"
|
||||
"polaris/internal/biz/engine"
|
||||
"polaris/log"
|
||||
"polaris/pkg/cache"
|
||||
"polaris/pkg/tmdb"
|
||||
"polaris/ui"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
ginzap "github.com/gin-contrib/zap"
|
||||
|
||||
"github.com/gin-contrib/static"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewServer(db db.Database) *Server {
|
||||
s := &Server{
|
||||
db: db,
|
||||
srv: &http.Server{},
|
||||
language: db.GetLanguage(),
|
||||
monitorNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
downloadNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
}
|
||||
s.core = engine.NewEngine(db, s.language)
|
||||
s.setupRoutes()
|
||||
return s
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
srv *http.Server
|
||||
db db.Database
|
||||
core *engine.Engine
|
||||
language string
|
||||
jwtSerect string
|
||||
monitorNumCache *cache.Cache[int, int]
|
||||
downloadNumCache *cache.Cache[int, int]
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
s.core.Init()
|
||||
|
||||
r := gin.Default()
|
||||
s.jwtSerect = s.db.GetSetting(db.JwtSerectKey)
|
||||
//st, _ := fs.Sub(ui.Web, "build/web")
|
||||
r.Use(static.Serve("/", static.EmbedFolder(ui.Web, "build/web")))
|
||||
//s.r.Use(ginzap.Ginzap(log.Logger().Desugar(), time.RFC3339, false))
|
||||
r.Use(ginzap.RecoveryWithZap(log.Logger().Desugar(), true))
|
||||
|
||||
log.SetLogLevel(s.db.GetSetting(db.SettingLogLevel)) //restore log level
|
||||
|
||||
r.POST("/api/login", HttpHandler(s.Login))
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
api.Use(s.authModdleware)
|
||||
api.StaticFS("/img", http.Dir(db.ImgPath))
|
||||
api.StaticFS("/logs", http.Dir(db.LogPath))
|
||||
api.Any("/posters/*proxyPath", s.proxyPosters)
|
||||
|
||||
setting := api.Group("/setting")
|
||||
{
|
||||
setting.GET("/logout", HttpHandler(s.Logout))
|
||||
setting.POST("/general", HttpHandler(s.SetSetting))
|
||||
setting.GET("/general", HttpHandler(s.GetSetting))
|
||||
setting.POST("/auth", HttpHandler(s.EnableAuth))
|
||||
setting.GET("/auth", HttpHandler(s.GetAuthSetting))
|
||||
setting.GET("/logfiles", HttpHandler(s.GetAllLogs))
|
||||
setting.GET("/about", HttpHandler(s.About))
|
||||
setting.POST("/parse/tv", HttpHandler(s.ParseTv))
|
||||
setting.POST("/parse/movie", HttpHandler(s.ParseMovie))
|
||||
setting.POST("/monitoring", HttpHandler(s.ChangeEpisodeMonitoring))
|
||||
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
|
||||
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
|
||||
setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting))
|
||||
setting.GET("/limiter", HttpHandler(s.GetSizeLimiter))
|
||||
setting.POST("/limiter", HttpHandler(s.SetSizeLimiter))
|
||||
}
|
||||
activity := api.Group("/activity")
|
||||
{
|
||||
activity.GET("/", HttpHandler(s.GetAllActivities))
|
||||
activity.POST("/delete", HttpHandler(s.RemoveActivity))
|
||||
activity.GET("/media/:id", HttpHandler(s.GetMediaDownloadHistory))
|
||||
activity.GET("/blacklist", HttpHandler(s.GetAllBlacklistItems))
|
||||
activity.DELETE("/blacklist/:id", HttpHandler(s.RemoveBlacklistItem))
|
||||
//activity.GET("/torrents", HttpHandler(s.GetAllTorrents))
|
||||
}
|
||||
|
||||
tv := api.Group("/media")
|
||||
{
|
||||
tv.GET("/search", HttpHandler(s.SearchMedia))
|
||||
tv.POST("/edit", HttpHandler(s.EditMediaMetadata))
|
||||
tv.POST("/tv/watchlist", HttpHandler(s.AddTv2Watchlist))
|
||||
tv.GET("/tv/watchlist", HttpHandler(s.GetTvWatchlist))
|
||||
tv.POST("/torrents", HttpHandler(s.SearchAvailableTorrents))
|
||||
tv.POST("/torrents/download", HttpHandler(s.DownloadTorrent))
|
||||
tv.POST("/movie/watchlist", HttpHandler(s.AddMovie2Watchlist))
|
||||
tv.GET("/movie/watchlist", HttpHandler(s.GetMovieWatchlist))
|
||||
tv.GET("/record/:id", HttpHandler(s.GetMediaDetails))
|
||||
tv.DELETE("/record/:id", HttpHandler(s.DeleteFromWatchlist))
|
||||
tv.GET("/suggest/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName))
|
||||
tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName))
|
||||
tv.GET("/downloadall/:id", HttpHandler(s.DownloadAll))
|
||||
tv.GET("/download/tv", HttpHandler(s.DownloadAllTv))
|
||||
tv.GET("/download/movie", HttpHandler(s.DownloadAllMovies))
|
||||
}
|
||||
indexer := api.Group("/indexer")
|
||||
{
|
||||
indexer.GET("/", HttpHandler(s.GetAllIndexers))
|
||||
indexer.POST("/add", HttpHandler(s.AddTorznabInfo))
|
||||
indexer.POST("/download", HttpHandler(s.SearchTvAndDownload))
|
||||
indexer.DELETE("/del/:id", HttpHandler(s.DeleteTorznabInfo))
|
||||
}
|
||||
|
||||
downloader := api.Group("/downloader")
|
||||
{
|
||||
downloader.GET("/", HttpHandler(s.GetAllDonloadClients))
|
||||
downloader.POST("/add", HttpHandler(s.AddDownloadClient))
|
||||
downloader.DELETE("/del/:id", HttpHandler(s.DeleteDownloadCLient))
|
||||
}
|
||||
storage := api.Group("/storage")
|
||||
{
|
||||
storage.GET("/", HttpHandler(s.GetAllStorage))
|
||||
storage.POST("/", HttpHandler(s.AddStorage))
|
||||
storage.DELETE("/:id", HttpHandler(s.DeleteStorage))
|
||||
}
|
||||
notifier := api.Group("/notifier")
|
||||
{
|
||||
notifier.GET("/all", HttpHandler(s.GetAllNotificationClients))
|
||||
notifier.GET("/id/:id", HttpHandler(s.GetNotificationClient))
|
||||
notifier.DELETE("/id/:id", HttpHandler(s.DeleteNotificationClient))
|
||||
notifier.POST("/add", HttpHandler(s.AddNotificationClient))
|
||||
}
|
||||
importlist := api.Group("/importlist")
|
||||
{
|
||||
importlist.GET("/", HttpHandler(s.getAllImportLists))
|
||||
importlist.POST("/add", HttpHandler(s.addImportlist))
|
||||
importlist.DELETE("/delete", HttpHandler(s.deleteImportList))
|
||||
}
|
||||
s.srv.Handler = r
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) (int, error) {
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1:0" // 0 means any available port
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to listen on port: %w", err)
|
||||
}
|
||||
|
||||
_, port, _ := net.SplitHostPort(ln.Addr().String())
|
||||
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to convert port to int: %w", err)
|
||||
}
|
||||
go func() {
|
||||
defer ln.Close()
|
||||
if err := s.srv.Serve(ln); err != nil {
|
||||
log.Errorf("failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Infof("----------- Polaris Server Successfully Started on Port %d------------", p)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
log.Infof("Stopping Polaris Server...")
|
||||
return s.srv.Close()
|
||||
}
|
||||
|
||||
func (s *Server) TMDB() (*tmdb.Client, error) {
|
||||
api := s.db.GetTmdbApiKey()
|
||||
if api == "" {
|
||||
return nil, errors.New("TMDB apiKey not set")
|
||||
}
|
||||
proxy := s.db.GetSetting(db.SettingProxy)
|
||||
adult := s.db.GetSetting(db.SettingEnableTmdbAdultContent)
|
||||
return tmdb.NewClient(api, proxy, adult == "true")
|
||||
}
|
||||
|
||||
func (s *Server) MustTMDB() *tmdb.Client {
|
||||
t, err := s.TMDB()
|
||||
if err != nil {
|
||||
log.Panicf("get tmdb: %v", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (s *Server) proxyPosters(c *gin.Context) {
|
||||
remote, _ := url.Parse("https://image.tmdb.org")
|
||||
proxy := httputil.NewSingleHostReverseProxy(remote)
|
||||
|
||||
proxy.Director = func(req *http.Request) {
|
||||
req.Header = c.Request.Header
|
||||
req.Host = remote.Host
|
||||
req.URL.Scheme = remote.Scheme
|
||||
req.URL.Host = remote.Host
|
||||
req.URL.Path = fmt.Sprintf("/t/p/w500/%v", c.Param("proxyPath"))
|
||||
}
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
367
internal/biz/server/setting.go
Normal file
367
internal/biz/server/setting.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"polaris/internal/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/downloadclients"
|
||||
"polaris/log"
|
||||
"polaris/pkg/qbittorrent"
|
||||
"polaris/pkg/torznab"
|
||||
"polaris/pkg/transmission"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GeneralSettings struct {
|
||||
TmdbApiKey string `json:"tmdb_api_key"`
|
||||
DownloadDir string `json:"download_dir"`
|
||||
LogLevel string `json:"log_level"`
|
||||
Proxy string `json:"proxy"`
|
||||
EnablePlexmatch bool `json:"enable_plexmatch"`
|
||||
EnableNfo bool `json:"enable_nfo"`
|
||||
AllowQiangban bool `json:"allow_qiangban"`
|
||||
EnableAdultContent bool `json:"enable_adult_content"`
|
||||
TvNamingFormat string `json:"tv_naming_format"`
|
||||
MovieNamingFormat string `json:"movie_naming_format"`
|
||||
}
|
||||
|
||||
func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
|
||||
var in GeneralSettings
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
log.Infof("set setting input: %+v", in)
|
||||
if in.TmdbApiKey != "" {
|
||||
if err := s.db.SetSetting(db.SettingTmdbApiKey, in.TmdbApiKey); err != nil {
|
||||
return nil, errors.Wrap(err, "save tmdb api")
|
||||
}
|
||||
}
|
||||
if in.DownloadDir != "" {
|
||||
log.Info("set download dir to %s", in.DownloadDir)
|
||||
if err := s.db.SetSetting(db.SettingDownloadDir, in.DownloadDir); err != nil {
|
||||
return nil, errors.Wrap(err, "save download dir")
|
||||
}
|
||||
}
|
||||
if in.LogLevel != "" {
|
||||
log.SetLogLevel(in.LogLevel)
|
||||
if err := s.db.SetSetting(db.SettingLogLevel, in.LogLevel); err != nil {
|
||||
return nil, errors.Wrap(err, "save log level")
|
||||
}
|
||||
}
|
||||
if in.TvNamingFormat != "" {
|
||||
if _, err := template.New("test").Parse(in.TvNamingFormat); err != nil {
|
||||
return nil, errors.Wrap(err, "tv format")
|
||||
}
|
||||
|
||||
s.db.SetSetting(db.SettingTvNamingFormat, in.TvNamingFormat)
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingTvNamingFormat, "")
|
||||
}
|
||||
|
||||
if in.MovieNamingFormat != "" {
|
||||
if _, err := template.New("test").Parse(in.MovieNamingFormat); err != nil {
|
||||
return nil, errors.Wrap(err, "movie format")
|
||||
}
|
||||
s.db.SetSetting(db.SettingMovieNamingFormat, in.MovieNamingFormat)
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingMovieNamingFormat, "")
|
||||
}
|
||||
|
||||
plexmatchEnabled := s.db.GetSetting(db.SettingPlexMatchEnabled)
|
||||
if in.EnablePlexmatch && plexmatchEnabled != "true" {
|
||||
s.db.SetSetting(db.SettingPlexMatchEnabled, "true")
|
||||
} else if !in.EnablePlexmatch && plexmatchEnabled != "false" {
|
||||
s.db.SetSetting(db.SettingPlexMatchEnabled, "false")
|
||||
}
|
||||
|
||||
s.db.SetSetting(db.SettingProxy, in.Proxy)
|
||||
|
||||
if in.AllowQiangban {
|
||||
s.db.SetSetting(db.SettingAllowQiangban, "true")
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingAllowQiangban, "false")
|
||||
}
|
||||
|
||||
if in.EnableNfo {
|
||||
s.db.SetSetting(db.SettingNfoSupportEnabled, "true")
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingNfoSupportEnabled, "false")
|
||||
}
|
||||
if in.EnableAdultContent {
|
||||
s.db.SetSetting(db.SettingEnableTmdbAdultContent, "true")
|
||||
} else {
|
||||
s.db.SetSetting(db.SettingEnableTmdbAdultContent, "false")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
|
||||
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
|
||||
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
|
||||
logLevel := s.db.GetSetting(db.SettingLogLevel)
|
||||
plexmatchEnabled := s.db.GetSetting(db.SettingPlexMatchEnabled)
|
||||
allowQiangban := s.db.GetSetting(db.SettingAllowQiangban)
|
||||
enableNfo := s.db.GetSetting(db.SettingNfoSupportEnabled)
|
||||
enableAdult := s.db.GetSetting(db.SettingEnableTmdbAdultContent)
|
||||
tvFormat := s.db.GetTvNamingFormat()
|
||||
movieFormat := s.db.GetMovingNamingFormat()
|
||||
return &GeneralSettings{
|
||||
TmdbApiKey: tmdb,
|
||||
DownloadDir: downloadDir,
|
||||
LogLevel: logLevel,
|
||||
Proxy: s.db.GetSetting(db.SettingProxy),
|
||||
EnablePlexmatch: plexmatchEnabled == "true",
|
||||
AllowQiangban: allowQiangban == "true",
|
||||
EnableNfo: enableNfo == "true",
|
||||
EnableAdultContent: enableAdult == "true",
|
||||
TvNamingFormat: tvFormat,
|
||||
MovieNamingFormat: movieFormat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type addTorznabIn struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
ApiKey string `json:"api_key" binding:"required"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Priority int `json:"priority"`
|
||||
SeedRatio float32 `json:"seed_ratio"`
|
||||
}
|
||||
|
||||
func (s *Server) AddTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
var in addTorznabIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
log.Infof("add indexer settings: %+v", in)
|
||||
|
||||
if in.Priority > 128 {
|
||||
in.Priority = 128
|
||||
}
|
||||
if in.Priority < 1 {
|
||||
in.Priority = 1
|
||||
}
|
||||
|
||||
indexer := ent.Indexers{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
Implementation: "torznab",
|
||||
Priority: in.Priority,
|
||||
Disabled: in.Disabled,
|
||||
SeedRatio: in.SeedRatio,
|
||||
APIKey: in.ApiKey,
|
||||
URL: in.URL,
|
||||
}
|
||||
err := s.db.SaveIndexer(&indexer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "add ")
|
||||
}
|
||||
|
||||
torznab.CleanCache() //need to clean exist cache, so next request will do actaul query
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteTorznabInfo(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
s.db.DeleteIndexer(id)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAllIndexers(c *gin.Context) (interface{}, error) {
|
||||
indexers := s.db.GetAllIndexers()
|
||||
if len(indexers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return indexers, nil
|
||||
}
|
||||
|
||||
type downloadClientIn struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Implementation string `json:"implementation" binding:"required"`
|
||||
Priority int `json:"priority"`
|
||||
UseNatTraversal bool `json:"use_nat_traversal"`
|
||||
}
|
||||
|
||||
func (s *Server) AddDownloadClient(c *gin.Context) (interface{}, error) {
|
||||
var in downloadClientIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
if in.Priority == 0 {
|
||||
in.Priority = 1 //make default
|
||||
}
|
||||
//test connection
|
||||
if in.Implementation == downloadclients.ImplementationTransmission.String() {
|
||||
_, err := transmission.NewClient(transmission.Config{
|
||||
URL: in.URL,
|
||||
User: in.User,
|
||||
Password: in.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tranmission setting")
|
||||
}
|
||||
|
||||
} else if in.Implementation == downloadclients.ImplementationQbittorrent.String() {
|
||||
_, err := qbittorrent.NewClient(in.URL, in.User, in.Password)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "qbittorrent")
|
||||
}
|
||||
}
|
||||
if err := s.db.SaveDownloader(&ent.DownloadClients{
|
||||
Name: in.Name,
|
||||
Implementation: downloadclients.Implementation(in.Implementation),
|
||||
Priority1: in.Priority,
|
||||
URL: in.URL,
|
||||
User: in.User,
|
||||
Password: in.Password,
|
||||
UseNatTraversal: in.UseNatTraversal,
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "save downloader")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAllDonloadClients(c *gin.Context) (interface{}, error) {
|
||||
res := s.db.GetAllDonloadClients()
|
||||
if len(res) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteDownloadCLient(c *gin.Context) (interface{}, error) {
|
||||
var ids = c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not correct: %v", ids)
|
||||
}
|
||||
s.db.DeleteDownloadCLient(id)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
type episodeMonitoringIn struct {
|
||||
EpisodeID int `json:"episode_id"`
|
||||
Monitor bool `json:"monitor"`
|
||||
}
|
||||
|
||||
func (s *Server) ChangeEpisodeMonitoring(c *gin.Context) (interface{}, error) {
|
||||
var in episodeMonitoringIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind")
|
||||
}
|
||||
s.db.SetEpisodeMonitoring(in.EpisodeID, in.Monitor)
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) EditMediaMetadata(c *gin.Context) (interface{}, error) {
|
||||
var in db.EditMediaData
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind")
|
||||
}
|
||||
err := s.db.EditMediaMetadata(in)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "save db")
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
type triggerCronJobIn struct {
|
||||
JobName string `json:"job_name"`
|
||||
}
|
||||
|
||||
func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
|
||||
var in triggerCronJobIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind")
|
||||
}
|
||||
|
||||
err := s.core.TriggerCronJob(in.JobName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
|
||||
se, err := s.db.GetProwlarrSetting()
|
||||
if err != nil {
|
||||
return &db.ProwlarrSetting{}, nil
|
||||
}
|
||||
return se, nil
|
||||
}
|
||||
func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
|
||||
var in db.ProwlarrSetting
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if in.Disabled {
|
||||
if err := s.core.DeleteAllProwlarrIndexers(); err != nil {
|
||||
return nil, errors.Wrap(err, "delete prowlarr indexers")
|
||||
}
|
||||
} else {
|
||||
if err := s.core.SyncProwlarrIndexers(in.ApiKey, in.URL); err != nil {
|
||||
return nil, errors.Wrap(err, "verify prowlarr")
|
||||
}
|
||||
|
||||
}
|
||||
err := s.db.SaveProwlarrSetting(&in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
type ResolutionSizeLimiter struct {
|
||||
TvLimiter *db.MediaSizeLimiter `json:"tv_limiter"`
|
||||
MovieLimiter *db.MediaSizeLimiter `json:"movie_limiter"`
|
||||
}
|
||||
|
||||
func (s *Server) GetSizeLimiter(c *gin.Context) (interface{}, error) {
|
||||
tv, err := s.db.GetSizeLimiter("tv")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
movie, err := s.db.GetSizeLimiter("movie")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
r := ResolutionSizeLimiter{
|
||||
TvLimiter: tv,
|
||||
MovieLimiter: movie,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetSizeLimiter(c *gin.Context) (interface{}, error) {
|
||||
var in ResolutionSizeLimiter
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.SetSizeLimiter("tv", in.TvLimiter); err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
if err := s.db.SetSizeLimiter("movie", in.MovieLimiter); err != nil {
|
||||
return nil, errors.Wrap(err, "db")
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
107
internal/biz/server/storage.go
Normal file
107
internal/biz/server/storage.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"polaris/internal/db"
|
||||
"strings"
|
||||
|
||||
"polaris/log"
|
||||
"polaris/pkg/alist"
|
||||
"polaris/pkg/storage"
|
||||
"polaris/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) GetAllStorage(c *gin.Context) (interface{}, error) {
|
||||
data := s.db.GetAllStorage()
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
|
||||
var in db.StorageInfo
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
utils.TrimFields(&in)
|
||||
|
||||
if in.Implementation == "webdav" {
|
||||
//test webdav
|
||||
wd := in.ToWebDavSetting()
|
||||
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false, nil, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "new webdav")
|
||||
}
|
||||
fs, err := st.ReadDir(".")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "test read")
|
||||
}
|
||||
for _, f := range fs {
|
||||
log.Infof("file name: %v", f.Name())
|
||||
}
|
||||
} else if in.Implementation == "alist" {
|
||||
cfg := in.ToAlistSetting()
|
||||
_, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, in.TvPath, nil, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "alist")
|
||||
}
|
||||
} else if in.Implementation == "local" {
|
||||
_, err := os.Stat(in.TvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasSuffix(in.TvPath, string(os.PathSeparator)) {
|
||||
in.TvPath = in.TvPath + string(os.PathSeparator)
|
||||
}
|
||||
_, err = os.Stat(in.MoviePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasSuffix(in.MoviePath, string(os.PathSeparator)) {
|
||||
in.MoviePath = in.MoviePath + string(os.PathSeparator)
|
||||
}
|
||||
|
||||
}
|
||||
log.Infof("received add storage input: %v", in)
|
||||
err := s.db.AddStorage(&in)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *Server) DeleteStorage(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not int: %v", ids)
|
||||
}
|
||||
err = s.db.DeleteStorage(id)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *Server) SuggestedSeriesFolderName(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("tmdb_id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not int: %v", ids)
|
||||
}
|
||||
name, err := s.core.SuggestedSeriesFolderName(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gin.H{"name": name}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SuggestedMovieFolderName(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("tmdb_id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id is not int: %v", ids)
|
||||
}
|
||||
name, err := s.core.SuggestedMovieFolderName(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gin.H{"name": name}, nil
|
||||
}
|
||||
74
internal/biz/server/systems.go
Normal file
74
internal/biz/server/systems.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"polaris/internal/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/metadata"
|
||||
"polaris/pkg/uptime"
|
||||
"runtime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LogFile struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
func (s *Server) GetAllLogs(c *gin.Context) (interface{}, error) {
|
||||
fs, err := os.ReadDir(db.LogPath)
|
||||
if err != nil {
|
||||
return []LogFile{}, nil
|
||||
}
|
||||
var logs []LogFile
|
||||
for _, f := range fs {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
log.Warnf("get log file error: %v", err)
|
||||
continue
|
||||
}
|
||||
l := LogFile{
|
||||
Name: f.Name(),
|
||||
Size: info.Size(),
|
||||
}
|
||||
logs = append(logs, l)
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (s *Server) About(c *gin.Context) (interface{}, error) {
|
||||
|
||||
return gin.H{
|
||||
"intro": "Polaris © Simon Ding",
|
||||
"homepage": "https://github.com/simon-ding/polaris",
|
||||
"uptime": uptime.Uptime(),
|
||||
"chat_group": "https://t.me/+8R2nzrlSs2JhMDgx",
|
||||
"go_version": runtime.Version(),
|
||||
"version": db.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type parseIn struct {
|
||||
S string `json:"s" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *Server) ParseTv(c *gin.Context) (interface{}, error) {
|
||||
var in parseIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind")
|
||||
}
|
||||
return metadata.ParseTv(in.S), nil
|
||||
}
|
||||
|
||||
func (s *Server) ParseMovie(c *gin.Context) (interface{}, error) {
|
||||
var in parseIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind")
|
||||
}
|
||||
return metadata.ParseMovie(in.S), nil
|
||||
}
|
||||
208
internal/biz/server/watchlist.go
Normal file
208
internal/biz/server/watchlist.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"polaris/internal/db"
|
||||
"polaris/internal/biz/engine"
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type searchTvParam struct {
|
||||
Query string `form:"query"`
|
||||
Page int `form:"page"`
|
||||
}
|
||||
|
||||
func (s *Server) SearchTvSeries(c *gin.Context) (interface{}, error) {
|
||||
var q searchTvParam
|
||||
if err := c.ShouldBindQuery(&q); err != nil {
|
||||
return nil, errors.Wrap(err, "bind query")
|
||||
}
|
||||
log.Infof("search tv series with keyword: %v", q.Query)
|
||||
r, err := s.MustTMDB().SearchTvShow(q.Query, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "search tv")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *Server) SearchMedia(c *gin.Context) (interface{}, error) {
|
||||
var q searchTvParam
|
||||
if err := c.ShouldBindQuery(&q); err != nil {
|
||||
return nil, errors.Wrap(err, "bind query")
|
||||
}
|
||||
log.Infof("search media with keyword: %v", q.Query)
|
||||
tmdb, err := s.TMDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := tmdb.SearchMedia(q.Query, s.language, q.Page)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "search tv")
|
||||
}
|
||||
for i, res := range r.Results {
|
||||
if s.db.TmdbIdInWatchlist(int(res.ID)) {
|
||||
r.Results[i].InWatchlist = true
|
||||
}
|
||||
}
|
||||
return r, 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 int `json:"size_min"`
|
||||
SizeMax int `json:"size_max"`
|
||||
}
|
||||
|
||||
func (s *Server) AddTv2Watchlist(c *gin.Context) (interface{}, error) {
|
||||
var in engine.AddWatchlistIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind query")
|
||||
}
|
||||
return s.core.AddTv2Watchlist(in)
|
||||
}
|
||||
|
||||
func (s *Server) AddMovie2Watchlist(c *gin.Context) (interface{}, error) {
|
||||
var in engine.AddWatchlistIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind query")
|
||||
}
|
||||
return s.core.AddMovie2Watchlist(in)
|
||||
}
|
||||
|
||||
type MediaWithStatus struct {
|
||||
*ent.Media
|
||||
MonitoredNum int `json:"monitored_num"`
|
||||
DownloadedNum int `json:"downloaded_num"`
|
||||
}
|
||||
|
||||
//missing: episode aired missing
|
||||
//downloaded: all monitored episode downloaded
|
||||
//monitoring: episode aired downloaded, but still has not aired episode
|
||||
//for movie, only monitoring/downloaded
|
||||
|
||||
func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
|
||||
list := s.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
res := make([]MediaWithStatus, len(list))
|
||||
for i, item := range list {
|
||||
var ms = MediaWithStatus{
|
||||
Media: item,
|
||||
MonitoredNum: 0,
|
||||
DownloadedNum: 0,
|
||||
}
|
||||
mon, ok1 := s.monitorNumCache.Get(item.ID)
|
||||
dow, ok2 := s.downloadNumCache.Get(item.ID)
|
||||
if ok1 && ok2 {
|
||||
ms.MonitoredNum = mon
|
||||
ms.DownloadedNum = dow
|
||||
} else {
|
||||
details, err := s.db.GetMediaDetails(item.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get details")
|
||||
}
|
||||
for _, ep := range details.Episodes {
|
||||
if ep.Monitored {
|
||||
ms.MonitoredNum++
|
||||
if ep.Status == episode.StatusDownloaded {
|
||||
ms.DownloadedNum++
|
||||
}
|
||||
}
|
||||
}
|
||||
s.monitorNumCache.Set(item.ID, ms.MonitoredNum)
|
||||
s.downloadNumCache.Set(item.ID, ms.DownloadedNum)
|
||||
}
|
||||
|
||||
res[i] = ms
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetMovieWatchlist(c *gin.Context) (interface{}, error) {
|
||||
list := s.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
res := make([]MediaWithStatus, len(list))
|
||||
for i, item := range list {
|
||||
var ms = MediaWithStatus{
|
||||
Media: item,
|
||||
MonitoredNum: 1,
|
||||
DownloadedNum: 0,
|
||||
}
|
||||
dummyEp, err := s.db.GetMovieDummyEpisode(item.ID)
|
||||
if err != nil {
|
||||
log.Errorf("get dummy episode: %v", err)
|
||||
} else {
|
||||
if dummyEp.Status == episode.StatusDownloaded {
|
||||
ms.DownloadedNum++
|
||||
}
|
||||
}
|
||||
res[i] = ms
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type MediaDetails struct {
|
||||
*db.MediaDetails
|
||||
Storage *ent.Storage `json:"storage"`
|
||||
}
|
||||
|
||||
func (s *Server) GetMediaDetails(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
detail, err := s.db.GetMediaDetails(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get details")
|
||||
}
|
||||
st := s.db.GetStorage(detail.StorageID)
|
||||
return MediaDetails{MediaDetails: detail, Storage: &st.Storage}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) {
|
||||
ids := c.Param("id")
|
||||
id, err := strconv.Atoi(ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
|
||||
deleteFiles := c.Query("delete_files")
|
||||
if strings.ToLower(deleteFiles) == "true" {
|
||||
//will delete local media file
|
||||
log.Infof("will delete local media files for %d", id)
|
||||
m, err := s.db.GetMedia(id)
|
||||
if err != nil {
|
||||
log.Warnf("get media: %v", err)
|
||||
} else {
|
||||
st, err := s.core.GetStorage(m.StorageID, m.MediaType)
|
||||
if err != nil {
|
||||
log.Warnf("get storage error: %v", err)
|
||||
} else {
|
||||
if err := st.RemoveAll(m.TargetDir); err != nil {
|
||||
log.Warnf("remove all : %v", err)
|
||||
} else {
|
||||
log.Infof("delete media files success: %v", m.TargetDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.DeleteMedia(id); err != nil {
|
||||
return nil, errors.Wrap(err, "delete db")
|
||||
}
|
||||
os.RemoveAll(filepath.Join(db.ImgPath, ids)) //delete image related
|
||||
|
||||
return "success", nil
|
||||
}
|
||||
108
internal/db/const.go
Normal file
108
internal/db/const.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"polaris/ent/media"
|
||||
"polaris/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "undefined"
|
||||
DefaultTmdbApiKey = ""
|
||||
)
|
||||
|
||||
const (
|
||||
SettingTmdbApiKey = "tmdb_api_key"
|
||||
SettingLanguage = "language"
|
||||
SettingJacketUrl = "jacket_url"
|
||||
SettingJacketApiKey = "jacket_api_key"
|
||||
SettingDownloadDir = "download_dir"
|
||||
SettingLogLevel = "log_level"
|
||||
SettingProxy = "proxy"
|
||||
SettingPlexMatchEnabled = "plexmatch_enabled"
|
||||
SettingNfoSupportEnabled = "nfo_support_enabled"
|
||||
SettingAllowQiangban = "filter_qiangban"
|
||||
SettingEnableTmdbAdultContent = "tmdb_adult_content"
|
||||
SettingTvNamingFormat = "tv_naming_format"
|
||||
SettingMovieNamingFormat = "movie_naming_format"
|
||||
SettingProwlarrInfo = "prowlarr_info"
|
||||
|
||||
SettingTvSizeLimiter = "tv_size_limiter"
|
||||
SettingMovieSizeLimiter = "movie_size_limiter"
|
||||
SettingAcceptedVideoFormats = "accepted_video_formats"
|
||||
SettingAcceptedSubtitleFormats = "accepted_subtitle_formats"
|
||||
)
|
||||
|
||||
const (
|
||||
SettingAuthEnabled = "auth_enbled"
|
||||
SettingUsername = "auth_username"
|
||||
SettingPassword = "auth_password"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexerTorznabImpl = "torznab"
|
||||
)
|
||||
|
||||
var (
|
||||
DataPath = utils.GetUserDataDir()
|
||||
ImgPath = DataPath + "/img"
|
||||
LogPath = DataPath + "/logs"
|
||||
)
|
||||
|
||||
const (
|
||||
LanguageEN = "en-US"
|
||||
LanguageCN = "zh-CN"
|
||||
)
|
||||
|
||||
const DefaultNamingFormat = "{{.NameCN}} {{.NameEN}} {{if .Year}} ({{.Year}}) {{end}}"
|
||||
|
||||
// https://en.wikipedia.org/wiki/Video_file_format
|
||||
var defaultAcceptedVideoFormats = []string{
|
||||
".webm", ".mkv", ".flv", ".vob", ".ogv", ".ogg", ".drc", ".mng", ".avi", ".mts", ".m2ts", ".ts",
|
||||
".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".viv", ".amv", ".mp4", ".m4p", ".m4v",
|
||||
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".m4v",
|
||||
".svi", ".3gp", ".3g2", ".nsv",
|
||||
}
|
||||
|
||||
var defaultAcceptedSubtitleFormats = []string{
|
||||
".ass", ".srt", ".vtt", ".webvtt", ".sub", ".idx",
|
||||
}
|
||||
|
||||
type NamingInfo struct {
|
||||
NameCN string
|
||||
NameEN string
|
||||
Year string
|
||||
TmdbID int
|
||||
}
|
||||
|
||||
type ResolutionType string
|
||||
|
||||
const JwtSerectKey = "jwt_secrect_key"
|
||||
|
||||
type MediaSizeLimiter struct {
|
||||
P720p SizeLimiter `json:"720p"`
|
||||
P1080 SizeLimiter `json:"1080p"`
|
||||
P2160 SizeLimiter `json:"2160p"`
|
||||
}
|
||||
|
||||
func (m *MediaSizeLimiter) GetLimiter(r media.Resolution) SizeLimiter {
|
||||
if r == media.Resolution1080p {
|
||||
return m.P1080
|
||||
} else if r == media.Resolution720p {
|
||||
return m.P720p
|
||||
} else if r == media.Resolution2160p {
|
||||
return m.P2160
|
||||
}
|
||||
return SizeLimiter{}
|
||||
}
|
||||
|
||||
type SizeLimiter struct {
|
||||
MaxSIze int64 `json:"max_size"`
|
||||
MinSize int64 `json:"min_size"`
|
||||
PreferSIze int64 `json:"prefer_size"`
|
||||
}
|
||||
|
||||
type ProwlarrSetting struct {
|
||||
Disabled bool `json:"disabled"`
|
||||
ApiKey string `json:"api_key"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
776
internal/db/db.go
Normal file
776
internal/db/db.go
Normal file
@@ -0,0 +1,776 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"polaris/ent"
|
||||
"polaris/ent/blacklist"
|
||||
"polaris/ent/downloadclients"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/ent/importlist"
|
||||
"polaris/ent/indexers"
|
||||
"polaris/ent/media"
|
||||
"polaris/ent/schema"
|
||||
"polaris/ent/settings"
|
||||
"polaris/ent/storage"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/dialect"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
ent *ent.Client
|
||||
}
|
||||
|
||||
func Open() (Database, error) {
|
||||
os.Mkdir(DataPath, 0777)
|
||||
cl, err := ent.Open(dialect.SQLite, fmt.Sprintf("file:%v/polaris.db?cache=shared&_fk=1", DataPath))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed opening connection to sqlite")
|
||||
}
|
||||
//defer client.Close()
|
||||
c := &client{
|
||||
ent: cl,
|
||||
}
|
||||
// Run the auto migration tool.
|
||||
if err := c.migrate(); err != nil {
|
||||
return nil, errors.Wrap(err, "migrate")
|
||||
}
|
||||
|
||||
c.init()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *client) init() {
|
||||
c.generateJwtSerectIfNotExist()
|
||||
if err := c.generateDefaultLocalStorage(); err != nil {
|
||||
log.Errorf("generate default storage: %v", err)
|
||||
}
|
||||
|
||||
downloadDir := c.GetSetting(SettingDownloadDir)
|
||||
if downloadDir == "" {
|
||||
log.Infof("set default download dir")
|
||||
c.SetSetting(SettingDownloadDir, "/downloads")
|
||||
}
|
||||
logLevel := c.GetSetting(SettingLogLevel)
|
||||
if logLevel == "" {
|
||||
log.Infof("set default log level")
|
||||
c.SetSetting(SettingLogLevel, "info")
|
||||
}
|
||||
c.initBuildinClient()
|
||||
}
|
||||
|
||||
func (c *client) initBuildinClient() {
|
||||
hasBuildin := false
|
||||
tr := c.GetAllDonloadClients()
|
||||
for _, d := range tr {
|
||||
if d.Implementation == downloadclients.ImplementationBuildin {
|
||||
hasBuildin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasBuildin {
|
||||
log.Warnf("no buildin download client, set default download client")
|
||||
if err := c.SaveDownloader(&ent.DownloadClients{
|
||||
Enable: true,
|
||||
Name: "内建下载器",
|
||||
Implementation: downloadclients.ImplementationBuildin,
|
||||
URL: "buildin",
|
||||
Priority1: 50,
|
||||
RemoveCompletedDownloads: true,
|
||||
RemoveFailedDownloads: true,
|
||||
}); err != nil {
|
||||
log.Warnf("add buildin client error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *client) generateJwtSerectIfNotExist() {
|
||||
v := c.GetSetting(JwtSerectKey)
|
||||
if v == "" {
|
||||
log.Infof("generate jwt serect")
|
||||
key := utils.RandString(32)
|
||||
c.SetSetting(JwtSerectKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) generateDefaultLocalStorage() error {
|
||||
n, _ := c.ent.Storage.Query().Count(context.TODO())
|
||||
if n != 0 {
|
||||
return nil
|
||||
}
|
||||
log.Infof("add default storage")
|
||||
return c.AddStorage(&StorageInfo{
|
||||
Name: "local",
|
||||
Implementation: "local",
|
||||
TvPath: "/data/tv/",
|
||||
MoviePath: "/data/movies/",
|
||||
Default: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *client) GetSetting(key string) string {
|
||||
v, err := c.ent.Settings.Query().Where(settings.Key(key)).Only(context.TODO())
|
||||
if err != nil {
|
||||
log.Debugf("get setting by key: %s error: %v", key, err)
|
||||
return ""
|
||||
}
|
||||
return v.Value
|
||||
}
|
||||
|
||||
func (c *client) SetSetting(key, value string) error {
|
||||
v, err := c.ent.Settings.Query().Where(settings.Key(key)).Only(context.TODO())
|
||||
if err != nil {
|
||||
log.Infof("create new setting")
|
||||
_, err := c.ent.Settings.Create().SetKey(key).SetValue(value).Save(context.TODO())
|
||||
return err
|
||||
}
|
||||
_, err = c.ent.Settings.UpdateOneID(v.ID).SetValue(value).Save(context.TODO())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) GetLanguage() string {
|
||||
lang := c.GetSetting(SettingLanguage)
|
||||
log.Infof("get application language: %s", lang)
|
||||
if lang == "" {
|
||||
return LanguageCN
|
||||
}
|
||||
return lang
|
||||
}
|
||||
|
||||
func (c *client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, error) {
|
||||
count := c.ent.Media.Query().Where(media.TmdbID(m.TmdbID)).CountX(context.Background())
|
||||
if count > 0 {
|
||||
return nil, fmt.Errorf("tv series %s already in watchlist", m.NameEn)
|
||||
}
|
||||
|
||||
if m.StorageID == 0 {
|
||||
r, err := c.ent.Storage.Query().Where(storage.And(storage.Default(true), storage.Deleted(false))).First(context.TODO())
|
||||
if err == nil {
|
||||
log.Infof("use default storage: %v", r.Name)
|
||||
m.StorageID = r.ID
|
||||
}
|
||||
}
|
||||
r, err := c.ent.Media.Create().
|
||||
SetTmdbID(m.TmdbID).
|
||||
SetImdbID(m.ImdbID).
|
||||
SetStorageID(m.StorageID).
|
||||
SetOverview(m.Overview).
|
||||
SetNameCn(m.NameCn).
|
||||
SetNameEn(m.NameEn).
|
||||
SetOriginalName(m.OriginalName).
|
||||
SetMediaType(m.MediaType).
|
||||
SetAirDate(m.AirDate).
|
||||
SetResolution(m.Resolution).
|
||||
SetTargetDir(m.TargetDir).
|
||||
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
|
||||
SetLimiter(m.Limiter).
|
||||
SetExtras(m.Extras).
|
||||
SetAlternativeTitles(m.AlternativeTitles).
|
||||
AddEpisodeIDs(episodes...).
|
||||
Save(context.TODO())
|
||||
return r, err
|
||||
|
||||
}
|
||||
|
||||
func (c *client) GetMediaWatchlist(mediaType media.MediaType) []*ent.Media {
|
||||
list, err := c.ent.Media.Query().Where(media.MediaTypeEQ(mediaType)).Order(ent.Desc(media.FieldID)).All(context.TODO())
|
||||
if err != nil {
|
||||
log.Infof("query wtach list error: %v", err)
|
||||
return nil
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *client) GetEpisode(seriesId, seasonNum, episodeNum int) (*ent.Episode, error) {
|
||||
return c.ent.Episode.Query().Where(episode.MediaID(seriesId), episode.SeasonNumber(seasonNum),
|
||||
episode.EpisodeNumber(episodeNum)).First(context.TODO())
|
||||
}
|
||||
func (c *client) GetEpisodeByID(epID int) (*ent.Episode, error) {
|
||||
return c.ent.Episode.Query().Where(episode.ID(epID)).First(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) UpdateEpiode(episodeId int, name, overview string) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(episodeId)).SetTitle(name).SetOverview(overview).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) UpdateEpiode2(episodeId int, name, overview, airdate string) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(episodeId)).SetTitle(name).SetOverview(overview).SetAirDate(airdate).Exec(context.TODO())
|
||||
}
|
||||
|
||||
type MediaDetails struct {
|
||||
*ent.Media
|
||||
Episodes []*ent.Episode `json:"episodes"`
|
||||
}
|
||||
|
||||
func (c *client) GetMediaDetails(id int) (*MediaDetails, error) {
|
||||
se, err := c.ent.Media.Query().Where(media.ID(id)).First(context.TODO())
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("get series %d: %v", id, err)
|
||||
}
|
||||
var md = &MediaDetails{
|
||||
Media: se,
|
||||
}
|
||||
|
||||
ep, err := se.QueryEpisodes().All(context.Background())
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("get episodes %d: %v", id, err)
|
||||
}
|
||||
md.Episodes = ep
|
||||
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func (c *client) GetMedia(id int) (*ent.Media, error) {
|
||||
return c.ent.Media.Query().Where(media.ID(id)).First(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) DeleteMedia(id int) error {
|
||||
_, err := c.ent.Episode.Delete().Where(episode.MediaID(id)).Exec(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.ent.Media.Delete().Where(media.ID(id)).Exec(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.CleanAllDanglingEpisodes()
|
||||
}
|
||||
|
||||
func (c *client) SaveEposideDetail(d *ent.Episode) (int, error) {
|
||||
ep, err := c.ent.Episode.Create().
|
||||
SetAirDate(d.AirDate).
|
||||
SetSeasonNumber(d.SeasonNumber).
|
||||
SetEpisodeNumber(d.EpisodeNumber).
|
||||
SetOverview(d.Overview).
|
||||
SetMonitored(d.Monitored).
|
||||
SetTitle(d.Title).Save(context.TODO())
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "save episode")
|
||||
}
|
||||
return ep.ID, nil
|
||||
}
|
||||
|
||||
func (c *client) SaveEposideDetail2(d *ent.Episode) (int, error) {
|
||||
ep, err := c.ent.Episode.Create().
|
||||
SetAirDate(d.AirDate).
|
||||
SetSeasonNumber(d.SeasonNumber).
|
||||
SetEpisodeNumber(d.EpisodeNumber).
|
||||
SetMediaID(d.MediaID).
|
||||
SetStatus(d.Status).
|
||||
SetOverview(d.Overview).
|
||||
SetMonitored(d.Monitored).
|
||||
SetTitle(d.Title).Save(context.TODO())
|
||||
|
||||
return ep.ID, err
|
||||
}
|
||||
|
||||
type TorznabSetting struct {
|
||||
URL string `json:"url"`
|
||||
ApiKey string `json:"api_key"`
|
||||
}
|
||||
|
||||
func (c *client) SaveIndexer(in *ent.Indexers) error {
|
||||
|
||||
count := c.ent.Indexers.Query().Where(indexers.Name(in.Name)).CountX(context.TODO())
|
||||
|
||||
if count > 0 {
|
||||
//update setting
|
||||
return c.ent.Indexers.Update().Where(indexers.Name(in.Name)).SetName(in.Name).SetImplementation(in.Implementation).
|
||||
SetPriority(in.Priority).SetSeedRatio(in.SeedRatio).SetDisabled(in.Disabled).
|
||||
SetTvSearch(in.TvSearch).SetMovieSearch(in.MovieSearch).SetSettings("").SetSynced(in.Synced).
|
||||
SetAPIKey(in.APIKey).SetURL(in.URL).
|
||||
Exec(context.Background())
|
||||
}
|
||||
//create new one
|
||||
|
||||
_, err := c.ent.Indexers.Create().
|
||||
SetName(in.Name).SetImplementation(in.Implementation).SetPriority(in.Priority).SetSeedRatio(in.SeedRatio).
|
||||
SetTvSearch(in.TvSearch).SetMovieSearch(in.MovieSearch).SetSettings("").SetSynced(in.Synced).
|
||||
SetAPIKey(in.APIKey).SetURL(in.URL).SetDisabled(in.Disabled).Save(context.TODO())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "save db")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) DeleteIndexer(id int) {
|
||||
c.ent.Indexers.Delete().Where(indexers.ID(id)).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) GetIndexer(id int) (*ent.Indexers, error) {
|
||||
res, err := c.ent.Indexers.Query().Where(indexers.ID(id)).First(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *client) GetAllIndexers() []*ent.Indexers {
|
||||
res := c.ent.Indexers.Query().Where(indexers.Implementation(IndexerTorznabImpl)).Order(ent.Asc(indexers.FieldID)).AllX(context.TODO())
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *client) SaveDownloader(downloader *ent.DownloadClients) error {
|
||||
count := c.ent.DownloadClients.Query().Where(downloadclients.Name(downloader.Name)).CountX(context.TODO())
|
||||
if count != 0 {
|
||||
err := c.ent.DownloadClients.Update().Where(downloadclients.Name(downloader.Name)).SetImplementation(downloader.Implementation).
|
||||
SetURL(downloader.URL).SetUser(downloader.User).SetUseNatTraversal(downloader.UseNatTraversal).SetPassword(downloader.Password).SetPriority1(downloader.Priority1).Exec(context.TODO())
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := c.ent.DownloadClients.Create().SetEnable(true).SetImplementation(downloader.Implementation).SetUseNatTraversal(downloader.UseNatTraversal).
|
||||
SetName(downloader.Name).SetURL(downloader.URL).SetUser(downloader.User).SetPriority1(downloader.Priority1).SetPassword(downloader.Password).Save(context.TODO())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) GetAllDonloadClients() []*ent.DownloadClients {
|
||||
cc, err := c.ent.DownloadClients.Query().Order(ent.Asc(downloadclients.FieldPriority1)).All(context.TODO())
|
||||
if err != nil {
|
||||
log.Errorf("no download client")
|
||||
return nil
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func (c *client) DeleteDownloadCLient(id int) { //not delete buildin client
|
||||
c.ent.DownloadClients.Delete().Where(downloadclients.ID(id), downloadclients.ImplementationNEQ(downloadclients.ImplementationBuildin)).Exec(context.TODO())
|
||||
}
|
||||
|
||||
// Storage is the model entity for the Storage schema.
|
||||
type StorageInfo struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Implementation string `json:"implementation" binding:"required"`
|
||||
Settings map[string]string `json:"settings" binding:"required"`
|
||||
TvPath string `json:"tv_path" binding:"required"`
|
||||
MoviePath string `json:"movie_path" binding:"required"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
func (s *StorageInfo) ToWebDavSetting() WebdavSetting {
|
||||
if s.Implementation != storage.ImplementationWebdav.String() {
|
||||
panic("not webdav storage")
|
||||
}
|
||||
return WebdavSetting{
|
||||
URL: s.Settings["url"],
|
||||
User: s.Settings["user"],
|
||||
Password: s.Settings["password"],
|
||||
ChangeFileHash: s.Settings["change_file_hash"],
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageInfo) ToAlistSetting() WebdavSetting {
|
||||
return WebdavSetting{
|
||||
URL: s.Settings["url"],
|
||||
User: s.Settings["user"],
|
||||
Password: s.Settings["password"],
|
||||
ChangeFileHash: s.Settings["change_file_hash"],
|
||||
}
|
||||
}
|
||||
|
||||
type WebdavSetting struct {
|
||||
URL string `json:"url"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
ChangeFileHash string `json:"change_file_hash"`
|
||||
}
|
||||
|
||||
func (c *client) AddStorage(st *StorageInfo) error {
|
||||
if st.Implementation != storage.ImplementationLocal.String() { //add seperator if not local storage
|
||||
if !strings.HasSuffix(st.TvPath, "/") {
|
||||
st.TvPath += "/"
|
||||
}
|
||||
if !strings.HasSuffix(st.MoviePath, "/") {
|
||||
st.MoviePath += "/"
|
||||
}
|
||||
}
|
||||
if st.Settings == nil {
|
||||
st.Settings = map[string]string{}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(st.Settings)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "json marshal")
|
||||
}
|
||||
|
||||
count := c.ent.Storage.Query().Where(storage.Name(st.Name)).CountX(context.TODO())
|
||||
if count > 0 {
|
||||
//storage already exist, edit exist one
|
||||
return c.ent.Storage.Update().Where(storage.Name(st.Name)).
|
||||
SetImplementation(storage.Implementation(st.Implementation)).SetTvPath(st.TvPath).SetMoviePath(st.MoviePath).
|
||||
SetSettings(string(data)).Exec(context.TODO())
|
||||
}
|
||||
countAll := c.ent.Storage.Query().Where(storage.Deleted(false)).CountX(context.TODO())
|
||||
if countAll == 0 {
|
||||
log.Infof("first storage, make it default: %s", st.Name)
|
||||
st.Default = true
|
||||
}
|
||||
_, err = c.ent.Storage.Create().SetName(st.Name).
|
||||
SetImplementation(storage.Implementation(st.Implementation)).SetTvPath(st.TvPath).SetMoviePath(st.MoviePath).
|
||||
SetSettings(string(data)).SetDefault(st.Default).Save(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetAllStorage() []*ent.Storage {
|
||||
data, err := c.ent.Storage.Query().Where(storage.Deleted(false)).All(context.TODO())
|
||||
if err != nil {
|
||||
log.Errorf("get storage: %v", err)
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
ent.Storage
|
||||
}
|
||||
|
||||
func (s *Storage) ToWebDavSetting() WebdavSetting {
|
||||
if s.Implementation != storage.ImplementationWebdav && s.Implementation != storage.ImplementationAlist {
|
||||
panic("not webdav storage")
|
||||
}
|
||||
var webdavSetting WebdavSetting
|
||||
json.Unmarshal([]byte(s.Settings), &webdavSetting)
|
||||
return webdavSetting
|
||||
}
|
||||
|
||||
func (c *client) GetStorage(id int) *Storage {
|
||||
r, err := c.ent.Storage.Query().Where(storage.ID(id)).First(context.TODO())
|
||||
if err != nil {
|
||||
//use default storage
|
||||
r := c.ent.Storage.Query().Where(storage.Default(true)).FirstX(context.TODO())
|
||||
return &Storage{*r}
|
||||
}
|
||||
return &Storage{*r}
|
||||
}
|
||||
|
||||
func (c *client) DeleteStorage(id int) error {
|
||||
return c.ent.Storage.Update().Where(storage.ID(id)).SetDeleted(true).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) SetDefaultStorage(id int) error {
|
||||
err := c.ent.Storage.Update().Where(storage.ID(id)).SetDefault(true).Exec(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.ent.Storage.Update().Where(storage.Or(storage.ID(id))).SetDefault(false).Exec(context.TODO())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) SetDefaultStorageByName(name string) error {
|
||||
err := c.ent.Storage.Update().Where(storage.Name(name)).SetDefault(true).Exec(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.ent.Storage.Update().Where(storage.Or(storage.Name(name))).SetDefault(false).Exec(context.TODO())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) SaveHistoryRecord(h ent.History) (*ent.History, error) {
|
||||
return c.ent.History.Create().SetMediaID(h.MediaID).SetDate(time.Now()).
|
||||
SetStatus(h.Status).SetTargetDir(h.TargetDir).SetSourceTitle(h.SourceTitle).SetIndexerID(h.IndexerID).
|
||||
SetDownloadClientID(h.DownloadClientID).SetSize(h.Size).SetSeasonNum(h.SeasonNum).
|
||||
SetEpisodeNums(h.EpisodeNums).SetHash(h.Hash).SetLink(h.Link).Save(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) SetHistoryStatus(id int, status history.Status) error {
|
||||
return c.ent.History.Update().Where(history.ID(id)).SetStatus(status).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) GetHistories() ent.Histories {
|
||||
h, err := c.ent.History.Query().Order(history.ByID(sql.OrderDesc())).All(context.TODO())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (c *client) GetRunningHistories() ent.Histories {
|
||||
h, err := c.ent.History.Query().Where(history.Or(history.StatusEQ(history.StatusRunning),
|
||||
history.StatusEQ(history.StatusUploading), history.StatusEQ(history.StatusSeeding))).All(context.TODO())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (c *client) GetHistory(id int) *ent.History {
|
||||
return c.ent.History.Query().Where(history.ID(id)).FirstX(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) DeleteHistory(id int) error {
|
||||
err := c.ent.History.Update().Where(history.ID(id)).SetStatus(history.StatusRemoved).Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) GetDownloadDir() string {
|
||||
r, err := c.ent.Settings.Query().Where(settings.Key(SettingDownloadDir)).First(context.TODO())
|
||||
if err != nil {
|
||||
return "/downloads"
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
func (c *client) UpdateEpisodeStatus(mediaID int, seasonNum, episodeNum int) error {
|
||||
ep, err := c.ent.Episode.Query().Where(episode.MediaID(mediaID)).Where(episode.EpisodeNumber(episodeNum)).
|
||||
Where(episode.SeasonNumber(seasonNum)).First(context.TODO())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "finding episode")
|
||||
}
|
||||
return ep.Update().SetStatus(episode.StatusDownloaded).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) SetEpisodeStatus(id int, status episode.Status) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(id)).SetStatus(status).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) IsEpisodeDownloadingOrDownloaded(id int) bool {
|
||||
ep, _ := c.GetEpisodeByID(id)
|
||||
his := c.ent.History.Query().Where(history.MediaID(ep.MediaID), history.SeasonNum(ep.SeasonNumber), history.StatusNEQ(history.StatusRemoved), history.StatusNEQ(history.StatusFail)).AllX(context.Background())
|
||||
for _, h := range his {
|
||||
if len(h.EpisodeNums) == 0 { //season pack download
|
||||
return true
|
||||
}
|
||||
if slices.Contains(h.EpisodeNums, ep.EpisodeNumber) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *client) SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episode.Status) error {
|
||||
return c.ent.Episode.Update().Where(episode.MediaID(mediaID), episode.SeasonNumber(seasonNum)).SetStatus(status).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) TmdbIdInWatchlist(tmdb_id int) bool {
|
||||
return c.ent.Media.Query().Where(media.TmdbID(tmdb_id)).CountX(context.TODO()) > 0
|
||||
}
|
||||
|
||||
func (c *client) GetDownloadHistory(mediaID int) ([]*ent.History, error) {
|
||||
return c.ent.History.Query().Where(history.MediaID(mediaID)).All(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) GetMovieDummyEpisode(movieId int) (*ent.Episode, error) {
|
||||
_, err := c.ent.Media.Query().Where(media.ID(movieId), media.MediaTypeEQ(media.MediaTypeMovie)).First(context.TODO())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get movie")
|
||||
}
|
||||
ep, err := c.ent.Episode.Query().Where(episode.MediaID(movieId)).First(context.TODO())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "query episode")
|
||||
}
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func (c *client) GetDownloadClient(id int) (*ent.DownloadClients, error) {
|
||||
return c.ent.DownloadClients.Query().Where(downloadclients.ID(id)).First(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) SetEpisodeMonitoring(id int, b bool) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(id)).SetMonitored(b).Exec(context.Background())
|
||||
}
|
||||
|
||||
type EditMediaData struct {
|
||||
ID int `json:"id"`
|
||||
Resolution media.Resolution `json:"resolution"`
|
||||
TargetDir string `json:"target_dir"`
|
||||
Limiter schema.MediaLimiter `json:"limiter"`
|
||||
}
|
||||
|
||||
func (c *client) EditMediaMetadata(in EditMediaData) error {
|
||||
return c.ent.Media.Update().Where(media.ID(in.ID)).SetResolution(in.Resolution).SetTargetDir(in.TargetDir).SetLimiter(in.Limiter).
|
||||
Exec(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) UpdateEpisodeTargetFile(id int, filename string) error {
|
||||
return c.ent.Episode.Update().Where(episode.ID(id)).SetTargetFile(filename).Exec(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) GetSeasonEpisodes(mediaId, seasonNum int) ([]*ent.Episode, error) {
|
||||
return c.ent.Episode.Query().Where(episode.MediaID(mediaId), episode.SeasonNumber(seasonNum)).All(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) GetAllImportLists() ([]*ent.ImportList, error) {
|
||||
return c.ent.ImportList.Query().All(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) AddImportlist(il *ent.ImportList) error {
|
||||
count, err := c.ent.ImportList.Query().Where(importlist.Name(il.Name)).Count(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
//edit exist record
|
||||
return c.ent.ImportList.Update().Where(importlist.Name(il.Name)).
|
||||
SetURL(il.URL).SetQulity(il.Qulity).SetType(il.Type).SetStorageID(il.StorageID).Exec(context.Background())
|
||||
}
|
||||
return c.ent.ImportList.Create().SetName(il.Name).SetURL(il.URL).SetQulity(il.Qulity).SetStorageID(il.StorageID).
|
||||
SetType(il.Type).Exec(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) DeleteImportlist(id int) error {
|
||||
return c.ent.ImportList.DeleteOneID(id).Exec(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) GetSizeLimiter(mediaType string) (*MediaSizeLimiter, error) {
|
||||
var v string
|
||||
if mediaType == "tv" {
|
||||
v = c.GetSetting(SettingTvSizeLimiter)
|
||||
} else if mediaType == "movie" {
|
||||
v = c.GetSetting(SettingMovieSizeLimiter)
|
||||
} else {
|
||||
return nil, errors.Errorf("media type not supported: %v", mediaType)
|
||||
}
|
||||
|
||||
var limiter MediaSizeLimiter
|
||||
if v == "" {
|
||||
return &limiter, nil
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(v), &limiter)
|
||||
return &limiter, err
|
||||
}
|
||||
|
||||
func (c *client) SetSizeLimiter(mediaType string, limiter *MediaSizeLimiter) error {
|
||||
data, err := json.Marshal(limiter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mediaType == "tv" {
|
||||
return c.SetSetting(SettingTvSizeLimiter, string(data))
|
||||
} else if mediaType == "movie" {
|
||||
return c.SetSetting(SettingMovieSizeLimiter, string(data))
|
||||
} else {
|
||||
return errors.Errorf("media type not supported: %v", mediaType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *client) GetTvNamingFormat() string {
|
||||
s := c.GetSetting(SettingTvNamingFormat)
|
||||
if s == "" {
|
||||
return DefaultNamingFormat
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *client) GetMovingNamingFormat() string {
|
||||
s := c.GetSetting(SettingMovieNamingFormat)
|
||||
if s == "" {
|
||||
return DefaultNamingFormat
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *client) CleanAllDanglingEpisodes() error {
|
||||
_, err := c.ent.Episode.Delete().Where(episode.Not(episode.HasMedia())).Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
func (c *client) GetProwlarrSetting() (*ProwlarrSetting, error) {
|
||||
s := c.GetSetting(SettingProwlarrInfo)
|
||||
if s == "" {
|
||||
return nil, errors.New("prowlarr setting not set")
|
||||
}
|
||||
var se ProwlarrSetting
|
||||
if err := json.Unmarshal([]byte(s), &se); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &se, nil
|
||||
}
|
||||
|
||||
func (c *client) SaveProwlarrSetting(se *ProwlarrSetting) error {
|
||||
data, err := json.Marshal(se)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetSetting(SettingProwlarrInfo, string(data))
|
||||
}
|
||||
|
||||
func (c *client) getAcceptedFormats(key string) ([]string, error) {
|
||||
v := c.GetSetting(key)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var res []string
|
||||
|
||||
err := json.Unmarshal([]byte(v), &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (c *client) setAcceptedFormats(key string, v []string) error {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetSetting(key, string(data))
|
||||
}
|
||||
|
||||
func (c *client) GetAcceptedVideoFormats() ([]string, error) {
|
||||
res, err := c.getAcceptedFormats(SettingAcceptedVideoFormats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res == nil {
|
||||
return defaultAcceptedVideoFormats, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *client) SetAcceptedVideoFormats(key string, v []string) error {
|
||||
return c.setAcceptedFormats(SettingAcceptedVideoFormats, v)
|
||||
}
|
||||
|
||||
func (c *client) GetAcceptedSubtitleFormats() ([]string, error) {
|
||||
res, err := c.getAcceptedFormats(SettingAcceptedSubtitleFormats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res == nil {
|
||||
return defaultAcceptedSubtitleFormats, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *client) SetAcceptedSubtitleFormats(key string, v []string) error {
|
||||
return c.setAcceptedFormats(SettingAcceptedSubtitleFormats, v)
|
||||
}
|
||||
|
||||
func (c *client) GetTmdbApiKey() string {
|
||||
k := c.GetSetting(SettingTmdbApiKey)
|
||||
if k == "" {
|
||||
return DefaultTmdbApiKey
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func (c *client) AddTorrent2Blacklist(hash, name string, mediaId int) error {
|
||||
count := c.ent.Blacklist.Query().Where(blacklist.TorrentHash(hash), blacklist.MediaID(mediaId)).CountX(context.TODO())
|
||||
if count > 0 { //already exist
|
||||
log.Infof("torrent %s already in blacklist", hash)
|
||||
return nil
|
||||
}
|
||||
return c.ent.Blacklist.Create().SetType(blacklist.TypeTorrent).SetTorrentHash(hash).SetTorrentName(name).SetMediaID(mediaId).Exec(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) GetTorrentBlacklist() (ent.Blacklists, error) {
|
||||
return c.ent.Blacklist.Query().Where(blacklist.TypeEQ(blacklist.TypeTorrent)).All(context.Background())
|
||||
}
|
||||
func (c *client) DeleteTorrentBlacklist(id int) error {
|
||||
return c.ent.Blacklist.DeleteOneID(id).Exec(context.Background())
|
||||
}
|
||||
112
internal/db/interface.go
Normal file
112
internal/db/interface.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"polaris/ent"
|
||||
"polaris/ent/episode"
|
||||
"polaris/ent/history"
|
||||
"polaris/ent/media"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
Settings
|
||||
|
||||
MediaApis
|
||||
|
||||
EpisodeApis
|
||||
|
||||
IndexerApis
|
||||
|
||||
HistoryApis
|
||||
|
||||
SaveDownloader(downloader *ent.DownloadClients) error
|
||||
GetAllDonloadClients() []*ent.DownloadClients
|
||||
DeleteDownloadCLient(id int)
|
||||
GetDownloadClient(id int) (*ent.DownloadClients, error)
|
||||
|
||||
AddStorage(st *StorageInfo) error
|
||||
GetAllStorage() []*ent.Storage
|
||||
GetStorage(id int) *Storage
|
||||
DeleteStorage(id int) error
|
||||
SetDefaultStorage(id int) error
|
||||
SetDefaultStorageByName(name string) error
|
||||
|
||||
GetAllImportLists() ([]*ent.ImportList, error)
|
||||
AddImportlist(il *ent.ImportList) error
|
||||
DeleteImportlist(id int) error
|
||||
|
||||
GetAllNotificationClients2() ([]*ent.NotificationClient, error)
|
||||
GetAllNotificationClients() ([]*NotificationClient, error)
|
||||
AddNotificationClient(name, service string, setting string, enabled bool) error
|
||||
DeleteNotificationClient(id int) error
|
||||
GetNotificationClient(id int) (*NotificationClient, error)
|
||||
|
||||
AddTorrent2Blacklist(hash, name string, mediaId int) error
|
||||
GetTorrentBlacklist() (ent.Blacklists, error)
|
||||
DeleteTorrentBlacklist(id int) error
|
||||
}
|
||||
|
||||
type Settings interface {
|
||||
GetSetting(key string) string
|
||||
SetSetting(key, value string) error
|
||||
GetLanguage() string
|
||||
GetDownloadDir() string
|
||||
GetTvNamingFormat() string
|
||||
GetMovingNamingFormat() string
|
||||
GetProwlarrSetting() (*ProwlarrSetting, error)
|
||||
SaveProwlarrSetting(se *ProwlarrSetting) error
|
||||
GetAcceptedVideoFormats() ([]string, error)
|
||||
SetAcceptedVideoFormats(key string, v []string) error
|
||||
GetAcceptedSubtitleFormats() ([]string, error)
|
||||
SetAcceptedSubtitleFormats(key string, v []string) error
|
||||
GetTmdbApiKey() string
|
||||
|
||||
}
|
||||
|
||||
type MediaApis interface {
|
||||
AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, error)
|
||||
GetMediaWatchlist(mediaType media.MediaType) []*ent.Media
|
||||
GetMediaDetails(id int) (*MediaDetails, error)
|
||||
GetMedia(id int) (*ent.Media, error)
|
||||
DeleteMedia(id int) error
|
||||
TmdbIdInWatchlist(tmdb_id int) bool
|
||||
EditMediaMetadata(in EditMediaData) error
|
||||
GetSizeLimiter(mediaType string) (*MediaSizeLimiter, error)
|
||||
SetSizeLimiter(mediaType string, limiter *MediaSizeLimiter) error
|
||||
}
|
||||
|
||||
type EpisodeApis interface {
|
||||
GetEpisode(seriesId, seasonNum, episodeNum int) (*ent.Episode, error)
|
||||
GetEpisodeByID(epID int) (*ent.Episode, error)
|
||||
UpdateEpiode(episodeId int, name, overview string) error
|
||||
UpdateEpiode2(episodeId int, name, overview, airdate string) error
|
||||
SaveEposideDetail(d *ent.Episode) (int, error)
|
||||
SaveEposideDetail2(d *ent.Episode) (int, error)
|
||||
UpdateEpisodeStatus(mediaID int, seasonNum, episodeNum int) error
|
||||
SetEpisodeStatus(id int, status episode.Status) error
|
||||
IsEpisodeDownloadingOrDownloaded(id int) bool
|
||||
SetSeasonAllEpisodeStatus(mediaID, seasonNum int, status episode.Status) error
|
||||
SetEpisodeMonitoring(id int, b bool) error
|
||||
UpdateEpisodeTargetFile(id int, filename string) error
|
||||
GetSeasonEpisodes(mediaId, seasonNum int) ([]*ent.Episode, error)
|
||||
CleanAllDanglingEpisodes() error
|
||||
|
||||
}
|
||||
|
||||
type IndexerApis interface {
|
||||
SaveIndexer(in *ent.Indexers) error
|
||||
DeleteIndexer(id int)
|
||||
GetIndexer(id int) (*ent.Indexers, error)
|
||||
GetAllIndexers() []*ent.Indexers
|
||||
|
||||
}
|
||||
|
||||
type HistoryApis interface {
|
||||
SaveHistoryRecord(h ent.History) (*ent.History, error)
|
||||
SetHistoryStatus(id int, status history.Status) error
|
||||
GetRunningHistories() ent.Histories
|
||||
GetHistory(id int) *ent.History
|
||||
GetHistories() ent.Histories
|
||||
DeleteHistory(id int) error
|
||||
GetDownloadHistory(mediaID int) ([]*ent.History, error)
|
||||
GetMovieDummyEpisode(movieId int) (*ent.Episode, error)
|
||||
}
|
||||
46
internal/db/migrate.go
Normal file
46
internal/db/migrate.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"polaris/log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *client) migrate() error {
|
||||
// Run the auto migration tool.
|
||||
if err := c.ent.Schema.Create(context.Background()); err != nil {
|
||||
return errors.Wrap(err, "failed creating schema resources")
|
||||
}
|
||||
|
||||
if err := c.migrateIndexerSetting(); err != nil {
|
||||
return errors.Wrap(err, "migrate indexer setting")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) migrateIndexerSetting() error {
|
||||
indexers := c.GetAllIndexers()
|
||||
for _, in := range indexers {
|
||||
|
||||
if in.Settings == "" {
|
||||
continue
|
||||
}
|
||||
if in.APIKey != "" && in.URL != "" {
|
||||
continue
|
||||
}
|
||||
var setting TorznabSetting
|
||||
err := json.Unmarshal([]byte(in.Settings), &setting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.APIKey = setting.ApiKey
|
||||
in.URL = setting.URL
|
||||
if err := c.SaveIndexer(in); err != nil {
|
||||
return errors.Wrap(err, "save indexer")
|
||||
}
|
||||
log.Infof("success migrate indexer setting field: %s", in.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
99
internal/db/notification.go
Normal file
99
internal/db/notification.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"polaris/ent"
|
||||
"polaris/ent/notificationclient"
|
||||
"polaris/pkg/notifier"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *client) GetAllNotificationClients2() ([]*ent.NotificationClient, error) {
|
||||
return c.ent.NotificationClient.Query().All(context.TODO())
|
||||
}
|
||||
|
||||
func (c *client) GetAllNotificationClients() ([]*NotificationClient, error) {
|
||||
all, err := c.ent.NotificationClient.Query().All(context.TODO())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "query db")
|
||||
}
|
||||
var all1 []*NotificationClient
|
||||
for _, item := range all {
|
||||
cl, err := toNotificationClient(item)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
all1 = append(all1, cl)
|
||||
}
|
||||
return all1, nil
|
||||
}
|
||||
|
||||
func (c *client) AddNotificationClient(name, service string, setting string, enabled bool) error {
|
||||
// data, err := json.Marshal(setting)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "json")
|
||||
// }
|
||||
service = strings.ToLower(service)
|
||||
count, err := c.ent.NotificationClient.Query().Where(notificationclient.Name(name)).Count(context.Background())
|
||||
if err == nil && count > 0 {
|
||||
//update exist one
|
||||
return c.ent.NotificationClient.Update().Where(notificationclient.Name(name)).SetService(service).
|
||||
SetSettings(setting).SetEnabled(enabled).Exec(context.Background())
|
||||
}
|
||||
|
||||
return c.ent.NotificationClient.Create().SetName(name).SetService(service).
|
||||
SetSettings(setting).SetEnabled(enabled).Exec(context.Background())
|
||||
}
|
||||
|
||||
func (c *client) DeleteNotificationClient(id int) error {
|
||||
_, err := c.ent.NotificationClient.Delete().Where(notificationclient.ID(id)).Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *client) GetNotificationClient(id int) (*NotificationClient, error) {
|
||||
noti, err := c.ent.NotificationClient.Query().Where(notificationclient.ID(id)).First(context.Background())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "query")
|
||||
}
|
||||
|
||||
return toNotificationClient(noti)
|
||||
}
|
||||
|
||||
func toNotificationClient(cl *ent.NotificationClient) (*NotificationClient, error) {
|
||||
var settings interface{}
|
||||
switch cl.Service {
|
||||
case "pushover":
|
||||
settings = notifier.PushoverConfig{}
|
||||
case "dingtalk":
|
||||
settings = notifier.DingTalkConfig{}
|
||||
case "telegram":
|
||||
settings = notifier.TelegramConfig{}
|
||||
case "bark":
|
||||
settings = notifier.BarkConfig{}
|
||||
case "serverchan":
|
||||
settings = notifier.ServerChanConfig{}
|
||||
}
|
||||
err := json.Unmarshal([]byte(cl.Settings), &settings)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "json")
|
||||
}
|
||||
return &NotificationClient{
|
||||
ID: cl.ID,
|
||||
Name: cl.Name,
|
||||
Service: cl.Service,
|
||||
Enabled: cl.Enabled,
|
||||
Settings: settings,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
type NotificationClient struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Service string `json:"service"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Settings interface{} `json:"settings"`
|
||||
}
|
||||
Reference in New Issue
Block a user