mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
feat(filters): add music filters (#91)
* feat(filters): add music filters * feat: improve parsing and filtering * feat: add red api support
This commit is contained in:
parent
30c11d4ef1
commit
00bc8298ac
20 changed files with 1053 additions and 52 deletions
|
@ -58,7 +58,7 @@ func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
|
||||||
//r.db.lock.RLock()
|
//r.db.lock.RLock()
|
||||||
//defer r.db.lock.RUnlock()
|
//defer r.db.lock.RUnlock()
|
||||||
|
|
||||||
row := r.db.handler.QueryRow("SELECT id, enabled, name, min_size, max_size, delay, match_releases, except_releases, use_regex, match_release_groups, except_release_groups, scene, freeleech, freeleech_percent, shows, seasons, episodes, resolutions, codecs, sources, containers, match_hdr, except_hdr, years, match_categories, except_categories, match_uploaders, except_uploaders, tags, except_tags, created_at, updated_at FROM filter WHERE id = ?", filterID)
|
row := r.db.handler.QueryRow("SELECT id, enabled, name, min_size, max_size, delay, match_releases, except_releases, use_regex, match_release_groups, except_release_groups, scene, freeleech, freeleech_percent, shows, seasons, episodes, resolutions, codecs, sources, containers, match_hdr, except_hdr, years, artists, albums, release_types_match, formats, quality, log_score, has_log, has_cue, perfect_flac, match_categories, except_categories, match_uploaders, except_uploaders, tags, except_tags, created_at, updated_at FROM filter WHERE id = ?", filterID)
|
||||||
|
|
||||||
var f domain.Filter
|
var f domain.Filter
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
|
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
|
||||||
var useRegex, scene, freeleech sql.NullBool
|
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool
|
||||||
var delay sql.NullInt32
|
var delay, logScore sql.NullInt32
|
||||||
|
|
||||||
if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), &years, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||||
log.Error().Stack().Err(err).Msgf("filter: %v : error scanning data to struct", filterID)
|
log.Error().Stack().Err(err).Msgf("filter: %v : error scanning data to struct", filterID)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,12 @@ func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
|
||||||
f.Seasons = seasons.String
|
f.Seasons = seasons.String
|
||||||
f.Episodes = episodes.String
|
f.Episodes = episodes.String
|
||||||
f.Years = years.String
|
f.Years = years.String
|
||||||
|
f.Artists = artists.String
|
||||||
|
f.Albums = albums.String
|
||||||
|
f.LogScore = int(logScore.Int32)
|
||||||
|
f.Log = hasLog.Bool
|
||||||
|
f.Cue = hasCue.Bool
|
||||||
|
f.PerfectFlac = perfectFlac.Bool
|
||||||
f.MatchCategories = matchCategories.String
|
f.MatchCategories = matchCategories.String
|
||||||
f.ExceptCategories = exceptCategories.String
|
f.ExceptCategories = exceptCategories.String
|
||||||
f.MatchUploaders = matchUploaders.String
|
f.MatchUploaders = matchUploaders.String
|
||||||
|
@ -131,6 +137,15 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
|
||||||
f.match_hdr,
|
f.match_hdr,
|
||||||
f.except_hdr,
|
f.except_hdr,
|
||||||
f.years,
|
f.years,
|
||||||
|
f.artists,
|
||||||
|
f.albums,
|
||||||
|
f.release_types_match,
|
||||||
|
f.formats,
|
||||||
|
f.quality,
|
||||||
|
f.log_score,
|
||||||
|
f.has_log,
|
||||||
|
f.has_cue,
|
||||||
|
f.perfect_flac,
|
||||||
f.match_categories,
|
f.match_categories,
|
||||||
f.except_categories,
|
f.except_categories,
|
||||||
f.match_uploaders,
|
f.match_uploaders,
|
||||||
|
@ -155,11 +170,11 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var f domain.Filter
|
var f domain.Filter
|
||||||
|
|
||||||
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
|
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
|
||||||
var useRegex, scene, freeleech sql.NullBool
|
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool
|
||||||
var delay sql.NullInt32
|
var delay, logScore sql.NullInt32
|
||||||
|
|
||||||
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), &years, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||||
log.Error().Stack().Err(err).Msg("error scanning data to struct")
|
log.Error().Stack().Err(err).Msg("error scanning data to struct")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -176,6 +191,12 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
|
||||||
f.Seasons = seasons.String
|
f.Seasons = seasons.String
|
||||||
f.Episodes = episodes.String
|
f.Episodes = episodes.String
|
||||||
f.Years = years.String
|
f.Years = years.String
|
||||||
|
f.Artists = artists.String
|
||||||
|
f.Albums = albums.String
|
||||||
|
f.LogScore = int(logScore.Int32)
|
||||||
|
f.Log = hasLog.Bool
|
||||||
|
f.Cue = hasCue.Bool
|
||||||
|
f.PerfectFlac = perfectFlac.Bool
|
||||||
f.MatchCategories = matchCategories.String
|
f.MatchCategories = matchCategories.String
|
||||||
f.ExceptCategories = exceptCategories.String
|
f.ExceptCategories = exceptCategories.String
|
||||||
f.MatchUploaders = matchUploaders.String
|
f.MatchUploaders = matchUploaders.String
|
||||||
|
@ -234,9 +255,18 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
|
||||||
match_uploaders,
|
match_uploaders,
|
||||||
except_uploaders,
|
except_uploaders,
|
||||||
tags,
|
tags,
|
||||||
except_tags
|
except_tags,
|
||||||
|
artists,
|
||||||
|
albums,
|
||||||
|
release_types_match,
|
||||||
|
formats,
|
||||||
|
quality,
|
||||||
|
log_score,
|
||||||
|
has_log,
|
||||||
|
has_cue,
|
||||||
|
perfect_flac
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29) ON CONFLICT DO NOTHING`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38) ON CONFLICT DO NOTHING`,
|
||||||
filter.Name,
|
filter.Name,
|
||||||
filter.Enabled,
|
filter.Enabled,
|
||||||
filter.MinSize,
|
filter.MinSize,
|
||||||
|
@ -266,6 +296,15 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
|
||||||
filter.ExceptUploaders,
|
filter.ExceptUploaders,
|
||||||
filter.Tags,
|
filter.Tags,
|
||||||
filter.ExceptTags,
|
filter.ExceptTags,
|
||||||
|
filter.Artists,
|
||||||
|
filter.Albums,
|
||||||
|
pq.Array(filter.MatchReleaseTypes),
|
||||||
|
pq.Array(filter.Formats),
|
||||||
|
pq.Array(filter.Quality),
|
||||||
|
filter.LogScore,
|
||||||
|
filter.Log,
|
||||||
|
filter.Cue,
|
||||||
|
filter.PerfectFlac,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Stack().Err(err).Msg("error executing query")
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
@ -317,6 +356,15 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.
|
||||||
except_uploaders = ?,
|
except_uploaders = ?,
|
||||||
tags = ?,
|
tags = ?,
|
||||||
except_tags = ?,
|
except_tags = ?,
|
||||||
|
artists = ?,
|
||||||
|
albums = ?,
|
||||||
|
release_types_match = ?,
|
||||||
|
formats = ?,
|
||||||
|
quality = ?,
|
||||||
|
log_score = ?,
|
||||||
|
has_log = ?,
|
||||||
|
has_cue = ?,
|
||||||
|
perfect_flac = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
filter.Name,
|
filter.Name,
|
||||||
|
@ -348,6 +396,15 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.
|
||||||
filter.ExceptUploaders,
|
filter.ExceptUploaders,
|
||||||
filter.Tags,
|
filter.Tags,
|
||||||
filter.ExceptTags,
|
filter.ExceptTags,
|
||||||
|
filter.Artists,
|
||||||
|
filter.Albums,
|
||||||
|
pq.Array(filter.MatchReleaseTypes),
|
||||||
|
pq.Array(filter.Formats),
|
||||||
|
pq.Array(filter.Quality),
|
||||||
|
filter.LogScore,
|
||||||
|
filter.Log,
|
||||||
|
filter.Cue,
|
||||||
|
filter.PerfectFlac,
|
||||||
filter.ID,
|
filter.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -84,6 +84,16 @@ CREATE TABLE filter
|
||||||
match_hdr TEXT [] DEFAULT '{}',
|
match_hdr TEXT [] DEFAULT '{}',
|
||||||
except_hdr TEXT [] DEFAULT '{}',
|
except_hdr TEXT [] DEFAULT '{}',
|
||||||
years TEXT,
|
years TEXT,
|
||||||
|
artists TEXT,
|
||||||
|
albums TEXT,
|
||||||
|
release_types_match TEXT [] DEFAULT '{}',
|
||||||
|
release_types_ignore TEXT [] DEFAULT '{}',
|
||||||
|
formats TEXT [] DEFAULT '{}',
|
||||||
|
quality TEXT [] DEFAULT '{}',
|
||||||
|
log_score INTEGER,
|
||||||
|
has_log BOOLEAN,
|
||||||
|
has_cue BOOLEAN,
|
||||||
|
perfect_flac BOOLEAN,
|
||||||
match_categories TEXT,
|
match_categories TEXT,
|
||||||
except_categories TEXT,
|
except_categories TEXT,
|
||||||
match_uploaders TEXT,
|
match_uploaders TEXT,
|
||||||
|
@ -178,7 +188,7 @@ CREATE TABLE "release"
|
||||||
artists TEXT [] DEFAULT '{}' NOT NULL,
|
artists TEXT [] DEFAULT '{}' NOT NULL,
|
||||||
type TEXT,
|
type TEXT,
|
||||||
format TEXT,
|
format TEXT,
|
||||||
bitrate TEXT,
|
quality TEXT,
|
||||||
log_score INTEGER,
|
log_score INTEGER,
|
||||||
has_log BOOLEAN,
|
has_log BOOLEAN,
|
||||||
has_cue BOOLEAN,
|
has_cue BOOLEAN,
|
||||||
|
@ -289,6 +299,40 @@ var migrations = []string{
|
||||||
ALTER TABLE "filter"
|
ALTER TABLE "filter"
|
||||||
ADD COLUMN except_hdr TEXT [] DEFAULT '{}';
|
ADD COLUMN except_hdr TEXT [] DEFAULT '{}';
|
||||||
`,
|
`,
|
||||||
|
`
|
||||||
|
ALTER TABLE "release"
|
||||||
|
RENAME COLUMN bitrate TO quality;
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN artists TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN albums TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN release_types_match TEXT [] DEFAULT '{}';
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN release_types_ignore TEXT [] DEFAULT '{}';
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN formats TEXT [] DEFAULT '{}';
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN quality TEXT [] DEFAULT '{}';
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN log_score INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN has_log BOOLEAN;
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN has_cue BOOLEAN;
|
||||||
|
|
||||||
|
ALTER TABLE "filter"
|
||||||
|
ADD COLUMN perfect_flac BOOLEAN;
|
||||||
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *SqliteDB) migrate() error {
|
func (db *SqliteDB) migrate() error {
|
||||||
|
|
|
@ -24,8 +24,8 @@ func (repo *ReleaseRepo) Store(ctx context.Context, r *domain.Release) (*domain.
|
||||||
|
|
||||||
query, args, err := sq.
|
query, args, err := sq.
|
||||||
Insert("release").
|
Insert("release").
|
||||||
Columns("filter_status", "rejections", "indexer", "filter", "protocol", "implementation", "timestamp", "group_id", "torrent_id", "torrent_name", "size", "raw", "title", "category", "season", "episode", "year", "resolution", "source", "codec", "container", "hdr", "audio", "release_group", "region", "language", "edition", "unrated", "hybrid", "proper", "repack", "website", "artists", "type", "format", "bitrate", "log_score", "has_log", "has_cue", "is_scene", "origin", "tags", "freeleech", "freeleech_percent", "uploader", "pre_time").
|
Columns("filter_status", "rejections", "indexer", "filter", "protocol", "implementation", "timestamp", "group_id", "torrent_id", "torrent_name", "size", "raw", "title", "category", "season", "episode", "year", "resolution", "source", "codec", "container", "hdr", "audio", "release_group", "region", "language", "edition", "unrated", "hybrid", "proper", "repack", "website", "artists", "type", "format", "quality", "log_score", "has_log", "has_cue", "is_scene", "origin", "tags", "freeleech", "freeleech_percent", "uploader", "pre_time").
|
||||||
Values(r.FilterStatus, pq.Array(r.Rejections), r.Indexer, r.FilterName, r.Protocol, r.Implementation, r.Timestamp, r.GroupID, r.TorrentID, r.TorrentName, r.Size, r.Raw, r.Title, r.Category, r.Season, r.Episode, r.Year, r.Resolution, r.Source, r.Codec, r.Container, r.HDR, r.Audio, r.Group, r.Region, r.Language, r.Edition, r.Unrated, r.Hybrid, r.Proper, r.Repack, r.Website, pq.Array(r.Artists), r.Type, r.Format, r.Bitrate, r.LogScore, r.HasLog, r.HasCue, r.IsScene, r.Origin, pq.Array(r.Tags), r.Freeleech, r.FreeleechPercent, r.Uploader, r.PreTime).
|
Values(r.FilterStatus, pq.Array(r.Rejections), r.Indexer, r.FilterName, r.Protocol, r.Implementation, r.Timestamp, r.GroupID, r.TorrentID, r.TorrentName, r.Size, r.Raw, r.Title, r.Category, r.Season, r.Episode, r.Year, r.Resolution, r.Source, r.Codec, r.Container, r.HDR, r.Audio, r.Group, r.Region, r.Language, r.Edition, r.Unrated, r.Hybrid, r.Proper, r.Repack, r.Website, pq.Array(r.Artists), r.Type, r.Format, r.Quality, r.LogScore, r.HasLog, r.HasCue, r.IsScene, r.Origin, pq.Array(r.Tags), r.Freeleech, r.FreeleechPercent, r.Uploader, r.PreTime).
|
||||||
ToSql()
|
ToSql()
|
||||||
|
|
||||||
res, err := repo.db.handler.ExecContext(ctx, query, args...)
|
res, err := repo.db.handler.ExecContext(ctx, query, args...)
|
||||||
|
|
|
@ -44,7 +44,7 @@ func (db *SqliteDB) Open() error {
|
||||||
|
|
||||||
// Set busy timeout
|
// Set busy timeout
|
||||||
if _, err = db.handler.Exec(`PRAGMA busy_timeout = 5000;`); err != nil {
|
if _, err = db.handler.Exec(`PRAGMA busy_timeout = 5000;`); err != nil {
|
||||||
return fmt.Errorf("busy timeout pragma")
|
return fmt.Errorf("busy timeout pragma: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable WAL. SQLite performs better with the WAL because it allows
|
// Enable WAL. SQLite performs better with the WAL because it allows
|
||||||
|
|
|
@ -53,22 +53,23 @@ type Filter struct {
|
||||||
Years string `json:"years"`
|
Years string `json:"years"`
|
||||||
Artists string `json:"artists"`
|
Artists string `json:"artists"`
|
||||||
Albums string `json:"albums"`
|
Albums string `json:"albums"`
|
||||||
MatchReleaseTypes string `json:"match_release_types"` // Album,Single,EP
|
MatchReleaseTypes []string `json:"match_release_types"` // Album,Single,EP
|
||||||
ExceptReleaseTypes string `json:"except_release_types"`
|
ExceptReleaseTypes string `json:"except_release_types"`
|
||||||
Formats []string `json:"formats"` // MP3, FLAC, Ogg, AAC, AC3, DTS
|
Formats []string `json:"formats"` // MP3, FLAC, Ogg, AAC, AC3, DTS
|
||||||
Bitrates []string `json:"bitrates"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
|
Quality []string `json:"quality"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
|
||||||
Media []string `json:"media"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
|
//Media []string `json:"media"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
|
||||||
Cue bool `json:"cue"`
|
PerfectFlac bool `json:"perfect_flac"`
|
||||||
Log bool `json:"log"`
|
Cue bool `json:"cue"`
|
||||||
LogScores string `json:"log_scores"`
|
Log bool `json:"log"`
|
||||||
MatchCategories string `json:"match_categories"`
|
LogScore int `json:"log_score"`
|
||||||
ExceptCategories string `json:"except_categories"`
|
MatchCategories string `json:"match_categories"`
|
||||||
MatchUploaders string `json:"match_uploaders"`
|
ExceptCategories string `json:"except_categories"`
|
||||||
ExceptUploaders string `json:"except_uploaders"`
|
MatchUploaders string `json:"match_uploaders"`
|
||||||
Tags string `json:"tags"`
|
ExceptUploaders string `json:"except_uploaders"`
|
||||||
ExceptTags string `json:"except_tags"`
|
Tags string `json:"tags"`
|
||||||
TagsAny string `json:"tags_any"`
|
ExceptTags string `json:"except_tags"`
|
||||||
ExceptTagsAny string `json:"except_tags_any"`
|
TagsAny string `json:"tags_any"`
|
||||||
Actions []Action `json:"actions"`
|
ExceptTagsAny string `json:"except_tags_any"`
|
||||||
Indexers []Indexer `json:"indexers"`
|
Actions []Action `json:"actions"`
|
||||||
|
Indexers []Indexer `json:"indexers"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ type Release struct {
|
||||||
Artists []string `json:"artists"`
|
Artists []string `json:"artists"`
|
||||||
Type string `json:"type"` // Album,Single,EP
|
Type string `json:"type"` // Album,Single,EP
|
||||||
Format string `json:"format"` // music only
|
Format string `json:"format"` // music only
|
||||||
Bitrate string `json:"bitrate"` // bitrate
|
Quality string `json:"quality"` // quality
|
||||||
LogScore int `json:"log_score"`
|
LogScore int `json:"log_score"`
|
||||||
HasLog bool `json:"has_log"`
|
HasLog bool `json:"has_log"`
|
||||||
HasCue bool `json:"has_cue"`
|
HasCue bool `json:"has_cue"`
|
||||||
|
@ -151,6 +151,10 @@ func (r *Release) Parse() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Release) extractYear() error {
|
func (r *Release) extractYear() error {
|
||||||
|
if r.Year > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
y, err := findLastInt(r.TorrentName, `\b(((?:19[0-9]|20[0-9])[0-9]))\b`)
|
y, err := findLastInt(r.TorrentName, `\b(((?:19[0-9]|20[0-9])[0-9]))\b`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -285,7 +289,7 @@ func (r *Release) extractHDR() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Release) extractAudio() error {
|
func (r *Release) extractAudio() error {
|
||||||
v, err := findLast(r.TorrentName, `(?i)(MP3|FLAC[\. ][1-7][\. ][0-2]|FLAC|Opus|DD-EX|DDP[\. ]?[124567][\. ][012] Atmos|DDP[\. ]?[124567][\. ][012]|DDP|DD[1-7][\. ][0-2]|Dual[\- ]Audio|LiNE|PCM|Dolby TrueHD [0-9][\. ][0-4]|TrueHD [0-9][\. ][0-4] Atmos|TrueHD [0-9][\. ][0-4]|DTS X|DTS-HD MA [0-9][\. ][0-4]|DTS-HD MA|DTS-ES|DTS [1-7][\. ][0-2]|DTS|DD|DD[12][\. ]0|Dolby Atmos|TrueHD ATMOS|TrueHD|Atmos|Dolby Digital Plus|Dolby Digital Audio|Dolby Digital|AAC[.-]LC|AAC (?:\.?[1-7]\.[0-2])?|AAC|eac3|AC3(?:\.5\.1)?)`)
|
v, err := findLast(r.TorrentName, `(?i)(FLAC[\. ][1-7][\. ][0-2]|FLAC|Opus|DD-EX|DDP[\. ]?[124567][\. ][012] Atmos|DDP[\. ]?[124567][\. ][012]|DDP|DD[1-7][\. ][0-2]|Dual[\- ]Audio|LiNE|PCM|Dolby TrueHD [0-9][\. ][0-4]|TrueHD [0-9][\. ][0-4] Atmos|TrueHD [0-9][\. ][0-4]|DTS X|DTS-HD MA [0-9][\. ][0-4]|DTS-HD MA|DTS-ES|DTS [1-7][\. ][0-2]|DTS|DD|DD[12][\. ]0|Dolby Atmos|TrueHD ATMOS|TrueHD|Atmos|Dolby Digital Plus|Dolby Digital Audio|Dolby Digital|AAC[.-]LC|AAC (?:\.?[1-7]\.[0-2])?|AAC|eac3|AC3(?:\.5\.1)?)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -299,7 +303,7 @@ func (r *Release) extractAudioFromTags(tag string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := findLast(tag, `(?i)(MP3|Ogg Vorbis|FLAC[\. ][1-7][\. ][0-2]|FLAC|Opus|DD-EX|DDP[\. ]?[124567][\. ][012] Atmos|DDP[\. ]?[124567][\. ][012]|DDP|DD[1-7][\. ][0-2]|Dual[\- ]Audio|LiNE|PCM|Dolby TrueHD [0-9][\. ][0-4]|TrueHD [0-9][\. ][0-4] Atmos|TrueHD [0-9][\. ][0-4]|DTS X|DTS-HD MA [0-9][\. ][0-4]|DTS-HD MA|DTS-ES|DTS [1-7][\. ][0-2]|DTS|DD|DD[12][\. ]0|Dolby Atmos|TrueHD ATMOS|TrueHD|Atmos|Dolby Digital Plus|Dolby Digital Audio|Dolby Digital|AAC[.-]LC|AAC (?:\.?[1-7]\.[0-2])?|AAC|eac3|AC3(?:\.5\.1)?)`)
|
v, err := findLast(tag, `(?i)(FLAC[\. ][1-7][\. ][0-2]|FLAC|Opus|DD-EX|DDP[\. ]?[124567][\. ][012] Atmos|DDP[\. ]?[124567][\. ][012]|DDP|DD[1-7][\. ][0-2]|Dual[\- ]Audio|LiNE|PCM|Dolby TrueHD [0-9][\. ][0-4]|TrueHD [0-9][\. ][0-4] Atmos|TrueHD [0-9][\. ][0-4]|DTS X|DTS-HD MA [0-9][\. ][0-4]|DTS-HD MA|DTS-ES|DTS [1-7][\. ][0-2]|DTS|DD|DD[12][\. ]0|Dolby Atmos|TrueHD ATMOS|TrueHD|Atmos|Dolby Digital Plus|Dolby Digital Audio|Dolby Digital|AAC[.-]LC|AAC (?:\.?[1-7]\.[0-2])?|AAC|eac3|AC3(?:\.5\.1)?)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -308,6 +312,20 @@ func (r *Release) extractAudioFromTags(tag string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Release) extractFormatsFromTags(tag string) error {
|
||||||
|
if r.Format != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := findLast(tag, `(?:MP3|FLAC|Ogg Vorbis|AAC|AC3|DTS)`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Format = v
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
//func (r *Release) extractCueFromTags(tag string) error {
|
//func (r *Release) extractCueFromTags(tag string) error {
|
||||||
// v, err := findLast(tag, `Cue`)
|
// v, err := findLast(tag, `Cue`)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
|
@ -477,14 +495,14 @@ func (r *Release) extractLogScoreFromTags(tag string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Release) extractBitrateFromTags(tag string) error {
|
func (r *Release) extractQualityFromTags(tag string) error {
|
||||||
if r.Bitrate != "" {
|
if r.Quality != "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with the basic most common ones
|
// Start with the basic most common ones
|
||||||
|
|
||||||
rxp, err := regexp.Compile(`^(?:vbr|aps|apx|v\d|\d{2,4}|\d+\.\d+|q\d+\.[\dx]+|Other)?(?:\s*kbps|\s*kbits?|\s*k)?(?:\s*\(?(?:vbr|cbr)\)?)?$`)
|
rxp, err := regexp.Compile(`(Lossless|24bit Lossless|V0 \(VBR\)|V1 \(VBR\)|V2 \(VBR\)|APS \(VBR\)|APX \(VBR\)|320|256|192)`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
//return errors.Wrapf(err, "invalid regex: %s", value)
|
//return errors.Wrapf(err, "invalid regex: %s", value)
|
||||||
|
@ -496,7 +514,7 @@ func (r *Release) extractBitrateFromTags(tag string) error {
|
||||||
if len(matches) >= 1 {
|
if len(matches) >= 1 {
|
||||||
last := matches[len(matches)-1]
|
last := matches[len(matches)-1]
|
||||||
|
|
||||||
r.Bitrate = last
|
r.Quality = last
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -516,13 +534,14 @@ func (r *Release) extractReleaseTags() error {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
err = r.extractAudioFromTags(t)
|
err = r.extractAudioFromTags(t)
|
||||||
|
err = r.extractFormatsFromTags(t)
|
||||||
err = r.extractResolutionFromTags(t)
|
err = r.extractResolutionFromTags(t)
|
||||||
err = r.extractCodecFromTags(t)
|
err = r.extractCodecFromTags(t)
|
||||||
err = r.extractContainerFromTags(t)
|
err = r.extractContainerFromTags(t)
|
||||||
err = r.extractSourceFromTags(t)
|
err = r.extractSourceFromTags(t)
|
||||||
err = r.extractFreeleechFromTags(t)
|
err = r.extractFreeleechFromTags(t)
|
||||||
err = r.extractLogScoreFromTags(t)
|
err = r.extractLogScoreFromTags(t)
|
||||||
err = r.extractBitrateFromTags(t)
|
err = r.extractQualityFromTags(t)
|
||||||
err = r.extractAnimeGroupFromTags(t)
|
err = r.extractAnimeGroupFromTags(t)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -781,6 +800,11 @@ func (r *Release) CheckFilter(filter Filter) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(filter.MatchReleaseTypes) > 0 && !checkFilterSlice(r.Category, filter.MatchReleaseTypes) {
|
||||||
|
r.addRejection("release type not matching")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (filter.MinSize != "" || filter.MaxSize != "") && !r.CheckSizeFilter(filter.MinSize, filter.MaxSize) {
|
if (filter.MinSize != "" || filter.MaxSize != "") && !r.CheckSizeFilter(filter.MinSize, filter.MaxSize) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -795,6 +819,49 @@ func (r *Release) CheckFilter(filter Filter) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(filter.Artists) > 0 && !checkFilterStrings(r.TorrentName, filter.Artists) {
|
||||||
|
r.addRejection("artists not matching")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Albums) > 0 && !checkFilterStrings(r.TorrentName, filter.Albums) {
|
||||||
|
r.addRejection("albums not matching")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perfect flac requires Cue, Log, Log Score 100, FLAC and 24bit Lossless
|
||||||
|
if filter.PerfectFlac {
|
||||||
|
if !r.HasLog || !r.HasCue || r.LogScore != 100 || r.Format != "FLAC" && !checkFilterSlice(r.Quality, []string{"Lossless", "24bit Lossless"}) {
|
||||||
|
r.addRejection("wanted: log")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Formats) > 0 && !checkFilterSlice(r.Format, filter.Formats) {
|
||||||
|
r.addRejection("formats not matching")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Quality) > 0 && !checkFilterSlice(r.Quality, filter.Quality) {
|
||||||
|
r.addRejection("formats not matching")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Log && r.HasLog != filter.Log {
|
||||||
|
r.addRejection("wanted: log")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Log && filter.LogScore != 0 && r.LogScore != filter.LogScore {
|
||||||
|
r.addRejection("wanted: log score")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Cue && r.HasCue != filter.Cue {
|
||||||
|
r.addRejection("wanted: cue")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,8 @@ func TestRelease_Parse(t *testing.T) {
|
||||||
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
Group: "",
|
Group: "",
|
||||||
Audio: "FLAC",
|
Audio: "FLAC",
|
||||||
|
Format: "FLAC",
|
||||||
|
Quality: "Lossless",
|
||||||
Source: "CD",
|
Source: "CD",
|
||||||
HasCue: true,
|
HasCue: true,
|
||||||
HasLog: true,
|
HasLog: true,
|
||||||
|
@ -178,9 +180,9 @@ func TestRelease_Parse(t *testing.T) {
|
||||||
Tags: []string{"house, techno, tech.house, electro.house, future.house, bass.house, melodic.house"},
|
Tags: []string{"house, techno, tech.house, electro.house, future.house, bass.house, melodic.house"},
|
||||||
ReleaseTags: "MP3 / 320 / Cassette",
|
ReleaseTags: "MP3 / 320 / Cassette",
|
||||||
Group: "",
|
Group: "",
|
||||||
Audio: "MP3",
|
Format: "MP3",
|
||||||
Source: "Cassette",
|
Source: "Cassette",
|
||||||
Bitrate: "320",
|
Quality: "320",
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
@ -195,7 +197,8 @@ func TestRelease_Parse(t *testing.T) {
|
||||||
Clean: "The artist (ザ・フリーダムユニティ) Long album name",
|
Clean: "The artist (ザ・フリーダムユニティ) Long album name",
|
||||||
ReleaseTags: "MP3 / V0 (VBR) / CD",
|
ReleaseTags: "MP3 / V0 (VBR) / CD",
|
||||||
Group: "",
|
Group: "",
|
||||||
Audio: "MP3",
|
Format: "MP3",
|
||||||
|
Quality: "V0 (VBR)",
|
||||||
Source: "CD",
|
Source: "CD",
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
@ -212,6 +215,29 @@ func TestRelease_Parse(t *testing.T) {
|
||||||
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
Group: "",
|
Group: "",
|
||||||
Audio: "FLAC",
|
Audio: "FLAC",
|
||||||
|
Format: "FLAC",
|
||||||
|
Quality: "Lossless",
|
||||||
|
Source: "CD",
|
||||||
|
HasCue: true,
|
||||||
|
HasLog: true,
|
||||||
|
LogScore: 100,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parse_music_5",
|
||||||
|
fields: Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / 24bit Lossless / Log / 100% / Cue / CD",
|
||||||
|
},
|
||||||
|
want: Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
Clean: "Artist Albumname",
|
||||||
|
ReleaseTags: "FLAC / 24bit Lossless / Log / 100% / Cue / CD",
|
||||||
|
Group: "",
|
||||||
|
Audio: "FLAC",
|
||||||
|
Format: "FLAC",
|
||||||
|
Quality: "24bit Lossless",
|
||||||
Source: "CD",
|
Source: "CD",
|
||||||
HasCue: true,
|
HasCue: true,
|
||||||
HasLog: true,
|
HasLog: true,
|
||||||
|
@ -1031,6 +1057,192 @@ func TestRelease_CheckFilter(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_1",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / 24bit Lossless / Log / 100% / Cue / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchCategories: "Album",
|
||||||
|
Artists: "Artist",
|
||||||
|
Sources: []string{"CD"},
|
||||||
|
Formats: []string{"FLAC"},
|
||||||
|
Quality: []string{"24bit Lossless"},
|
||||||
|
Log: true,
|
||||||
|
LogScore: 100,
|
||||||
|
Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_2",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "MP3 / 320 / WEB",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchCategories: "Album",
|
||||||
|
Artists: "Artist",
|
||||||
|
//Sources: []string{"CD"},
|
||||||
|
//Formats: []string{"FLAC"},
|
||||||
|
//Quality: []string{"24bit Lossless"},
|
||||||
|
PerfectFlac: true,
|
||||||
|
//Log: true,
|
||||||
|
//LogScore: 100,
|
||||||
|
//Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_3",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / Lossless / Log / 100% / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchCategories: "Album",
|
||||||
|
Artists: "Artist",
|
||||||
|
//Sources: []string{"CD"},
|
||||||
|
//Formats: []string{"FLAC"},
|
||||||
|
//Quality: []string{"24bit Lossless"},
|
||||||
|
PerfectFlac: true,
|
||||||
|
//Log: true,
|
||||||
|
//LogScore: 100,
|
||||||
|
//Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_4",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / Lossless / Log / 100% / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchCategories: "Album",
|
||||||
|
Artists: "Artist",
|
||||||
|
Sources: []string{"CD"},
|
||||||
|
Formats: []string{"FLAC"},
|
||||||
|
Quality: []string{"24bit Lossless"},
|
||||||
|
//PerfectFlac: true,
|
||||||
|
Log: true,
|
||||||
|
LogScore: 100,
|
||||||
|
Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_5",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
Year: 2022,
|
||||||
|
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchReleaseTypes: []string{"Album"},
|
||||||
|
Years: "2020-2022",
|
||||||
|
Artists: "Artist",
|
||||||
|
Sources: []string{"CD"},
|
||||||
|
Formats: []string{"FLAC"},
|
||||||
|
Quality: []string{"24bit Lossless", "Lossless"},
|
||||||
|
PerfectFlac: true,
|
||||||
|
Log: true,
|
||||||
|
LogScore: 100,
|
||||||
|
Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_6",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchReleaseTypes: []string{"Single"},
|
||||||
|
Artists: "Artist",
|
||||||
|
Sources: []string{"CD"},
|
||||||
|
Formats: []string{"FLAC"},
|
||||||
|
Quality: []string{"24bit Lossless", "Lossless"},
|
||||||
|
PerfectFlac: true,
|
||||||
|
Log: true,
|
||||||
|
LogScore: 100,
|
||||||
|
Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_7",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchReleaseTypes: []string{"Album"},
|
||||||
|
Artists: "Artiiiist",
|
||||||
|
Sources: []string{"CD"},
|
||||||
|
Formats: []string{"FLAC"},
|
||||||
|
Quality: []string{"24bit Lossless", "Lossless"},
|
||||||
|
PerfectFlac: true,
|
||||||
|
Log: true,
|
||||||
|
LogScore: 100,
|
||||||
|
Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_music_8",
|
||||||
|
fields: &Release{
|
||||||
|
TorrentName: "Artist - Albumname",
|
||||||
|
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
Category: "Album",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
filter: Filter{
|
||||||
|
Enabled: true,
|
||||||
|
MatchReleaseTypes: []string{"Album"},
|
||||||
|
Artists: "Artist",
|
||||||
|
Albums: "Albumname",
|
||||||
|
Sources: []string{"CD"},
|
||||||
|
Formats: []string{"FLAC"},
|
||||||
|
Quality: []string{"24bit Lossless", "Lossless"},
|
||||||
|
PerfectFlac: true,
|
||||||
|
Log: true,
|
||||||
|
LogScore: 100,
|
||||||
|
Cue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
|
@ -211,8 +211,8 @@ func (s *service) FindAndCheckFilters(release *domain.Release) (bool, *domain.Fi
|
||||||
if release.AdditionalSizeCheckRequired {
|
if release.AdditionalSizeCheckRequired {
|
||||||
log.Debug().Msgf("filter-service.find_and_check_filters: (%v) additional size check required", f.Name)
|
log.Debug().Msgf("filter-service.find_and_check_filters: (%v) additional size check required", f.Name)
|
||||||
|
|
||||||
// check if indexer = btn,ptp (ggn,red later)
|
// check if indexer = btn, ptp, ggn or red
|
||||||
if release.Indexer == "ptp" || release.Indexer == "btn" || release.Indexer == "ggn" {
|
if release.Indexer == "ptp" || release.Indexer == "btn" || release.Indexer == "ggn" || release.Indexer == "redacted" {
|
||||||
// fetch torrent info from api
|
// fetch torrent info from api
|
||||||
// save outside of loop to check multiple filters with only one fetch
|
// save outside of loop to check multiple filters with only one fetch
|
||||||
if torrentInfo == nil {
|
if torrentInfo == nil {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/autobrr/autobrr/pkg/btn"
|
"github.com/autobrr/autobrr/pkg/btn"
|
||||||
"github.com/autobrr/autobrr/pkg/ggn"
|
"github.com/autobrr/autobrr/pkg/ggn"
|
||||||
"github.com/autobrr/autobrr/pkg/ptp"
|
"github.com/autobrr/autobrr/pkg/ptp"
|
||||||
|
"github.com/autobrr/autobrr/pkg/red"
|
||||||
)
|
)
|
||||||
|
|
||||||
type APIService interface {
|
type APIService interface {
|
||||||
|
@ -104,6 +105,13 @@ func (s *apiService) AddClient(indexer string, settings map[string]string) error
|
||||||
}
|
}
|
||||||
s.apiClients[indexer] = ggn.NewClient("", key)
|
s.apiClients[indexer] = ggn.NewClient("", key)
|
||||||
|
|
||||||
|
case "redacted":
|
||||||
|
key, ok := settings["api_key"]
|
||||||
|
if !ok || key == "" {
|
||||||
|
return fmt.Errorf("api_service: could not initialize red client: missing var 'api_key'")
|
||||||
|
}
|
||||||
|
s.apiClients[indexer] = red.NewClient("", key)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("api_service: could not initialize client: unsupported indexer '%v'", indexer)
|
return fmt.Errorf("api_service: could not initialize client: unsupported indexer '%v'", indexer)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ urls:
|
||||||
privacy: private
|
privacy: private
|
||||||
protocol: torrent
|
protocol: torrent
|
||||||
supports:
|
supports:
|
||||||
|
- api
|
||||||
- irc
|
- irc
|
||||||
- rss
|
- rss
|
||||||
source: gazelle
|
source: gazelle
|
||||||
|
@ -21,6 +22,22 @@ settings:
|
||||||
type: secret
|
type: secret
|
||||||
label: Torrent pass
|
label: Torrent pass
|
||||||
help: Right click DL on a torrent and get the torrent_pass.
|
help: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
- name: api_key
|
||||||
|
type: secret
|
||||||
|
label: API Key
|
||||||
|
help: Settings -> Account Settings -> API Keys - Generate new api keys. Scope (User, Torrents)
|
||||||
|
|
||||||
|
api:
|
||||||
|
url: https://redacted.ch/ajax.php
|
||||||
|
type: json
|
||||||
|
limits:
|
||||||
|
max: 10
|
||||||
|
per: 10 seconds
|
||||||
|
settings:
|
||||||
|
- name: api_key
|
||||||
|
type: secret
|
||||||
|
label: API Key
|
||||||
|
help: Settings -> Account Settings -> API Keys - Generate new api keys. Scope (User, Torrents)
|
||||||
|
|
||||||
irc:
|
irc:
|
||||||
network: Scratch-Network
|
network: Scratch-Network
|
||||||
|
@ -55,12 +72,13 @@ parse:
|
||||||
- test:
|
- test:
|
||||||
- "Artist - Albumname [2008] [Single] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=0000000 / https://redacted.ch/torrents.php?action=download&id=0000000 - hip.hop,rhythm.and.blues,2000s"
|
- "Artist - Albumname [2008] [Single] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=0000000 / https://redacted.ch/torrents.php?action=download&id=0000000 - hip.hop,rhythm.and.blues,2000s"
|
||||||
- "A really long name here - Concertos 5 and 6, Suite No 2 [1991] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=0000000 / https://redacted.ch/torrents.php?action=download&id=0000000 - classical"
|
- "A really long name here - Concertos 5 and 6, Suite No 2 [1991] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=0000000 / https://redacted.ch/torrents.php?action=download&id=0000000 - classical"
|
||||||
pattern: '^(.*)\s+\[(.*)\] \[(.*)\] - (.*) -\s+https?:.*[&\?]id=.*(https?\:\/\/.*)\s* -\s*(.*)'
|
pattern: '(.*) (?:\[(.*)\] \[(.*)\] - (.*))? -\s+https?:.*[&\?]id=(\d+) \/ (https?\:\/\/.*)\s* -\s*(.*)'
|
||||||
vars:
|
vars:
|
||||||
- torrentName
|
- torrentName
|
||||||
- year
|
- year
|
||||||
- category
|
- category
|
||||||
- releaseTags
|
- releaseTags
|
||||||
|
- torrentId
|
||||||
- baseUrl
|
- baseUrl
|
||||||
- tags
|
- tags
|
||||||
|
|
||||||
|
|
212
pkg/red/red.go
Normal file
212
pkg/red/red.go
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
package red
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type REDClient interface {
|
||||||
|
GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
|
||||||
|
TestAPI() (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
URL string
|
||||||
|
Timeout int
|
||||||
|
client *http.Client
|
||||||
|
RateLimiter *rate.Limiter
|
||||||
|
APIKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(url string, apiKey string) REDClient {
|
||||||
|
if url == "" {
|
||||||
|
url = "https://redacted.ch/ajax.php"
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
APIKey: apiKey,
|
||||||
|
client: http.DefaultClient,
|
||||||
|
URL: url,
|
||||||
|
RateLimiter: rate.NewLimiter(rate.Every(10*time.Second), 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentDetailsResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Response struct {
|
||||||
|
Group Group `json:"group"`
|
||||||
|
Torrent Torrent `json:"torrent"`
|
||||||
|
} `json:"response"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
//WikiBody string `json:"wikiBody"`
|
||||||
|
//WikiImage string `json:"wikiImage"`
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Year int `json:"year"`
|
||||||
|
RecordLabel string `json:"recordLabel"`
|
||||||
|
CatalogueNumber string `json:"catalogueNumber"`
|
||||||
|
ReleaseType int `json:"releaseType"`
|
||||||
|
CategoryId int `json:"categoryId"`
|
||||||
|
CategoryName string `json:"categoryName"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
VanityHouse bool `json:"vanityHouse"`
|
||||||
|
//MusicInfo struct {
|
||||||
|
// Composers []interface{} `json:"composers"`
|
||||||
|
// Dj []interface{} `json:"dj"`
|
||||||
|
// Artists []struct {
|
||||||
|
// Id int `json:"id"`
|
||||||
|
// Name string `json:"name"`
|
||||||
|
// } `json:"artists"`
|
||||||
|
// With []struct {
|
||||||
|
// Id int `json:"id"`
|
||||||
|
// Name string `json:"name"`
|
||||||
|
// } `json:"with"`
|
||||||
|
// Conductor []interface{} `json:"conductor"`
|
||||||
|
// RemixedBy []interface{} `json:"remixedBy"`
|
||||||
|
// Producer []interface{} `json:"producer"`
|
||||||
|
//} `json:"musicInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Torrent struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
InfoHash string `json:"infoHash"`
|
||||||
|
Media string `json:"media"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Encoding string `json:"encoding"`
|
||||||
|
Remastered bool `json:"remastered"`
|
||||||
|
RemasterYear int `json:"remasterYear"`
|
||||||
|
RemasterTitle string `json:"remasterTitle"`
|
||||||
|
RemasterRecordLabel string `json:"remasterRecordLabel"`
|
||||||
|
RemasterCatalogueNumber string `json:"remasterCatalogueNumber"`
|
||||||
|
Scene bool `json:"scene"`
|
||||||
|
HasLog bool `json:"hasLog"`
|
||||||
|
HasCue bool `json:"hasCue"`
|
||||||
|
LogScore int `json:"logScore"`
|
||||||
|
FileCount int `json:"fileCount"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Seeders int `json:"seeders"`
|
||||||
|
Leechers int `json:"leechers"`
|
||||||
|
Snatched int `json:"snatched"`
|
||||||
|
FreeTorrent bool `json:"freeTorrent"`
|
||||||
|
IsNeutralleech bool `json:"isNeutralleech"`
|
||||||
|
IsFreeload bool `json:"isFreeload"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
FileList string `json:"fileList"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
UserId int `json:"userId"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
err := c.RateLimiter.Wait(ctx) // This is a blocking call. Honors the rate limit
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) get(url string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("red client request error : %v", url)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", c.APIKey)
|
||||||
|
req.Header.Set("User-Agent", "autobrr")
|
||||||
|
|
||||||
|
res, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("red client request error : %v", url)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusUnauthorized {
|
||||||
|
return nil, errors.New("unauthorized: bad credentials")
|
||||||
|
} else if res.StatusCode == http.StatusForbidden {
|
||||||
|
return nil, nil
|
||||||
|
} else if res.StatusCode == http.StatusBadRequest {
|
||||||
|
return nil, errors.New("bad id parameter")
|
||||||
|
} else if res.StatusCode == http.StatusTooManyRequests {
|
||||||
|
return nil, errors.New("rate-limited")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error) {
|
||||||
|
if torrentID == "" {
|
||||||
|
return nil, fmt.Errorf("red client: must have torrentID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r TorrentDetailsResponse
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("id", torrentID)
|
||||||
|
params := v.Encode()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%v?action=torrent&%v", c.URL, params)
|
||||||
|
|
||||||
|
resp, err := c.get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, readErr := ioutil.ReadAll(resp.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.TorrentBasic{
|
||||||
|
Id: strconv.Itoa(r.Response.Torrent.Id),
|
||||||
|
InfoHash: r.Response.Torrent.InfoHash,
|
||||||
|
Size: strconv.Itoa(r.Response.Torrent.Size),
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAPI try api access against torrents page
|
||||||
|
func (c *Client) TestAPI() (bool, error) {
|
||||||
|
resp, err := c.get(c.URL + "?action=index")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
109
pkg/red/red_test.go
Normal file
109
pkg/red/red_test.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package red
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestREDClient_GetTorrentByID(t *testing.T) {
|
||||||
|
// disable logger
|
||||||
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||||
|
|
||||||
|
key := "mock-key"
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// request validation logic
|
||||||
|
apiKey := r.Header.Get("Authorization")
|
||||||
|
if apiKey != key {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(r.RequestURI, "29991962") {
|
||||||
|
jsonPayload, _ := ioutil.ReadFile("testdata/get_torrent_by_id_not_found.json")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write(jsonPayload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// read json response
|
||||||
|
jsonPayload, _ := ioutil.ReadFile("testdata/get_torrent_by_id.json")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(jsonPayload)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
Url string
|
||||||
|
APIKey string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
torrentID string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *domain.TorrentBasic
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "get_by_id_1",
|
||||||
|
fields: fields{
|
||||||
|
Url: ts.URL,
|
||||||
|
APIKey: key,
|
||||||
|
},
|
||||||
|
args: args{torrentID: "29991962"},
|
||||||
|
want: &domain.TorrentBasic{
|
||||||
|
Id: "29991962",
|
||||||
|
InfoHash: "B2BABD3A361EAFC6C4E9142C422DF7DDF5D7E163",
|
||||||
|
Size: "527749302",
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_by_id_2",
|
||||||
|
fields: fields{
|
||||||
|
Url: ts.URL,
|
||||||
|
APIKey: key,
|
||||||
|
},
|
||||||
|
args: args{torrentID: "100002"},
|
||||||
|
want: nil,
|
||||||
|
wantErr: errors.New("bad id parameter"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_by_id_3",
|
||||||
|
fields: fields{
|
||||||
|
Url: ts.URL,
|
||||||
|
APIKey: "",
|
||||||
|
},
|
||||||
|
args: args{torrentID: "100002"},
|
||||||
|
want: nil,
|
||||||
|
wantErr: errors.New("unauthorized: bad credentials"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := NewClient(tt.fields.Url, tt.fields.APIKey)
|
||||||
|
|
||||||
|
got, err := c.GetTorrentByID(tt.args.torrentID)
|
||||||
|
if tt.wantErr != nil && assert.Error(t, err) {
|
||||||
|
assert.Equal(t, tt.wantErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
23
pkg/red/testdata/get_index.json
vendored
Normal file
23
pkg/red/testdata/get_index.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"response": {
|
||||||
|
"username": "username",
|
||||||
|
"id": 469,
|
||||||
|
"authkey": "redacted",
|
||||||
|
"passkey": "redacted",
|
||||||
|
"api_version": "redacted-v2.0",
|
||||||
|
"notifications": {
|
||||||
|
"messages": 0,
|
||||||
|
"notifications": 9000,
|
||||||
|
"newAnnouncement": false,
|
||||||
|
"newBlog": false
|
||||||
|
},
|
||||||
|
"userstats": {
|
||||||
|
"uploaded": 585564424629,
|
||||||
|
"downloaded": 177461229738,
|
||||||
|
"ratio": 3.29,
|
||||||
|
"requiredratio": 0.6,
|
||||||
|
"class": "VIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
pkg/red/testdata/get_torrent_by_id.json
vendored
Normal file
77
pkg/red/testdata/get_torrent_by_id.json
vendored
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"response": {
|
||||||
|
"group": {
|
||||||
|
"wikiBody": "",
|
||||||
|
"wikiImage": "http://whatimg.com/i/ralpc.jpg",
|
||||||
|
"id": 72189681,
|
||||||
|
"name": "Fear Not",
|
||||||
|
"year": 2012,
|
||||||
|
"recordLabel": "Hospital Records",
|
||||||
|
"catalogueNumber": "NHS209CD",
|
||||||
|
"releaseType": 1,
|
||||||
|
"categoryId": 1,
|
||||||
|
"categoryName": "Music",
|
||||||
|
"time": "2012-05-02 07:39:30",
|
||||||
|
"vanityHouse": false,
|
||||||
|
"musicInfo": {
|
||||||
|
"composers": [],
|
||||||
|
"dj": [],
|
||||||
|
"artists": [
|
||||||
|
{
|
||||||
|
"id": 1460,
|
||||||
|
"name": "Logistics"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"with": [
|
||||||
|
{
|
||||||
|
"id": 25351,
|
||||||
|
"name": "Alice Smith"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44545,
|
||||||
|
"name": "Nightshade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 249446,
|
||||||
|
"name": "Sarah Callander"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"conductor": [],
|
||||||
|
"remixedBy": [],
|
||||||
|
"producer": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"torrent": {
|
||||||
|
"id": 29991962,
|
||||||
|
"infoHash": "B2BABD3A361EAFC6C4E9142C422DF7DDF5D7E163",
|
||||||
|
"media": "CD",
|
||||||
|
"format": "FLAC",
|
||||||
|
"encoding": "Lossless",
|
||||||
|
"remastered": false,
|
||||||
|
"remasterYear": 0,
|
||||||
|
"remasterTitle": "",
|
||||||
|
"remasterRecordLabel": "",
|
||||||
|
"remasterCatalogueNumber": "",
|
||||||
|
"scene": true,
|
||||||
|
"hasLog": false,
|
||||||
|
"hasCue": false,
|
||||||
|
"logScore": 0,
|
||||||
|
"fileCount": 19,
|
||||||
|
"size": 527749302,
|
||||||
|
"seeders": 20,
|
||||||
|
"leechers": 0,
|
||||||
|
"snatched": 55,
|
||||||
|
"freeTorrent": false,
|
||||||
|
"isNeutralleech": false,
|
||||||
|
"isFreeload": false,
|
||||||
|
"time": "2012-04-14 15:57:00",
|
||||||
|
"description": "",
|
||||||
|
"fileList": "00-logistics-fear_not-cd-flac-2012.jpg{{{1233205}}}|||00-logistics-fear_not-cd-flac-2012.m3u{{{538}}}|||00-logistics-fear_not-cd-flac-2012.nfo{{{1607}}}|||00-logistics-fear_not-cd-flac-2012.sfv{{{688}}}|||01-logistics-fear_not.flac{{{38139451}}}|||02-logistics-timelapse.flac{{{39346037}}}|||03-logistics-2999_(wherever_you_go).flac{{{41491133}}}|||04-logistics-try_again.flac{{{32151567}}}|||05-logistics-we_are_one.flac{{{40778041}}}|||06-logistics-crystal_skies_(feat_nightshade_and_sarah_callander).flac{{{34544405}}}|||07-logistics-feels_so_good.flac{{{41363732}}}|||08-logistics-running_late.flac{{{16679269}}}|||09-logistics-early_again.flac{{{35373278}}}|||10-logistics-believe_in_me.flac{{{39495420}}}|||11-logistics-letting_go.flac{{{30846730}}}|||12-logistics-sendai_song.flac{{{35021141}}}|||13-logistics-over_and_out.flac{{{44621200}}}|||14-logistics-destination_unknown.flac{{{13189493}}}|||15-logistics-watching_the_world_go_by_(feat_alice_smith).flac{{{43472367}}}",
|
||||||
|
"filePath": "Logistics-Fear_Not-CD-FLAC-2012-TaBoo",
|
||||||
|
"userId": 567,
|
||||||
|
"username": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
pkg/red/testdata/get_torrent_by_id_not_found.json
vendored
Normal file
1
pkg/red/testdata/get_torrent_by_id_not_found.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"status":"failure","error":"bad id parameter"}
|
|
@ -14,4 +14,30 @@ const ErrorField: React.FC<ErrorFieldProps> = ({ name, classNames }) => (
|
||||||
}
|
}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
export { ErrorField }
|
|
||||||
|
interface CheckboxFieldProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
sublabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckboxField: React.FC<CheckboxFieldProps> = ({ name, label, sublabel }) => (
|
||||||
|
<div className="relative flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<Field
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
type="checkbox"
|
||||||
|
className="focus:ring-bkue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor={name} className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-500">{sublabel}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export { ErrorField, CheckboxField }
|
|
@ -1,4 +1,4 @@
|
||||||
export { ErrorField } from "./common";
|
export { ErrorField, CheckboxField } from "./common";
|
||||||
export { TextField, NumberField, PasswordField } from "./input";
|
export { TextField, NumberField, PasswordField } from "./input";
|
||||||
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||||
export { RadioFieldsetWide } from "./radio";
|
export { RadioFieldsetWide } from "./radio";
|
||||||
|
|
|
@ -53,7 +53,16 @@ export const sources = [
|
||||||
"HDTV",
|
"HDTV",
|
||||||
"Mixed",
|
"Mixed",
|
||||||
"SiteRip",
|
"SiteRip",
|
||||||
"Webrip"
|
"Webrip",
|
||||||
|
"CD",
|
||||||
|
"WEB",
|
||||||
|
"DVD",
|
||||||
|
"Vinyl",
|
||||||
|
"Soundboard",
|
||||||
|
"DAT",
|
||||||
|
"Cassette",
|
||||||
|
"Blu-Ray",
|
||||||
|
"SACD",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
|
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
|
||||||
|
@ -80,6 +89,68 @@ export const hdr = [
|
||||||
|
|
||||||
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
|
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
|
||||||
|
|
||||||
|
|
||||||
|
export const formatMusic = [
|
||||||
|
"MP3",
|
||||||
|
"FLAC",
|
||||||
|
"Ogg Vorbis",
|
||||||
|
"Ogg",
|
||||||
|
"AAC",
|
||||||
|
"AC3",
|
||||||
|
"DTS",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FORMATS_OPTIONS = formatMusic.map(r => ({ value: r, label: r, key: r}));
|
||||||
|
|
||||||
|
export const sourcesMusic = [
|
||||||
|
"CD",
|
||||||
|
"WEB",
|
||||||
|
"DVD",
|
||||||
|
"Vinyl",
|
||||||
|
"Soundboard",
|
||||||
|
"DAT",
|
||||||
|
"Cassette",
|
||||||
|
"Blu-Ray",
|
||||||
|
"SACD",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SOURCES_MUSIC_OPTIONS = sourcesMusic.map(v => ({ value: v, label: v, key: v}));
|
||||||
|
|
||||||
|
export const qualityMusic = [
|
||||||
|
"192",
|
||||||
|
"256",
|
||||||
|
"320",
|
||||||
|
"APS (VBR)",
|
||||||
|
"APX (VBR)",
|
||||||
|
"V2 (VBR)",
|
||||||
|
"V1 (VBR)",
|
||||||
|
"V0 (VBR)",
|
||||||
|
"Lossless",
|
||||||
|
"24bit Lossless",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const QUALITY_MUSIC_OPTIONS = qualityMusic.map(v => ({ value: v, label: v, key: v}));
|
||||||
|
|
||||||
|
export const releaseTypeMusic = [
|
||||||
|
"Album",
|
||||||
|
"Single",
|
||||||
|
"EP",
|
||||||
|
"Soundtrack",
|
||||||
|
"Anthology",
|
||||||
|
"Compilation",
|
||||||
|
"Live album",
|
||||||
|
"Remix",
|
||||||
|
"Bootleg",
|
||||||
|
"Interview",
|
||||||
|
"Mixtape",
|
||||||
|
"Demo",
|
||||||
|
"Concert Recording",
|
||||||
|
"DJ Mix",
|
||||||
|
"Unkown",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
|
||||||
|
|
||||||
export interface radioFieldsetOption {
|
export interface radioFieldsetOption {
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
|
@ -83,8 +83,17 @@ export interface Filter {
|
||||||
sources: string[];
|
sources: string[];
|
||||||
codecs: string[];
|
codecs: string[];
|
||||||
containers: string[];
|
containers: string[];
|
||||||
|
match_release_types: string[];
|
||||||
|
quality: string[];
|
||||||
|
formats: string[];
|
||||||
match_hdr: string[];
|
match_hdr: string[];
|
||||||
except_hdr: string[];
|
except_hdr: string[];
|
||||||
|
log_score: number;
|
||||||
|
log: boolean;
|
||||||
|
cue: boolean;
|
||||||
|
perfect_flac: boolean;
|
||||||
|
artists: string;
|
||||||
|
albums: string;
|
||||||
seasons: string;
|
seasons: string;
|
||||||
episodes: string;
|
episodes: string;
|
||||||
match_releases: string;
|
match_releases: string;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Action, ActionType, DownloadClient, Filter, Indexer } from "../../domai
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import { useMutation, useQuery } from "react-query";
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS, ActionTypeNameMap, ActionTypeOptions, HDR_OPTIONS } from "../../domain/constants";
|
import { CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS, ActionTypeNameMap, ActionTypeOptions, HDR_OPTIONS, FORMATS_OPTIONS, SOURCES_MUSIC_OPTIONS, QUALITY_MUSIC_OPTIONS, RELEASE_TYPE_MUSIC_OPTIONS } from "../../domain/constants";
|
||||||
|
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import { TitleSubtitle } from "../../components/headings";
|
import { TitleSubtitle } from "../../components/headings";
|
||||||
|
@ -30,11 +30,12 @@ import Toast from '../../components/notifications/Toast';
|
||||||
import { Field, FieldArray, Form, Formik } from "formik";
|
import { Field, FieldArray, Form, Formik } from "formik";
|
||||||
import { AlertWarning } from "../../components/alerts";
|
import { AlertWarning } from "../../components/alerts";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect } from "../../components/inputs";
|
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect, CheckboxField } from "../../components/inputs";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: 'General', href: '', current: true },
|
{ name: 'General', href: '', current: true },
|
||||||
{ name: 'Movies and TV', href: 'movies-tv', current: false },
|
{ name: 'Movies and TV', href: 'movies-tv', current: false },
|
||||||
|
{ name: 'Music', href: 'music', current: false },
|
||||||
// { name: 'P2P', href: 'p2p', current: false },
|
// { name: 'P2P', href: 'p2p', current: false },
|
||||||
{ name: 'Advanced', href: 'advanced', current: false },
|
{ name: 'Advanced', href: 'advanced', current: false },
|
||||||
{ name: 'Actions', href: 'actions', current: false },
|
{ name: 'Actions', href: 'actions', current: false },
|
||||||
|
@ -251,7 +252,16 @@ export default function FilterDetails() {
|
||||||
freeleech_percent: data.freeleech_percent,
|
freeleech_percent: data.freeleech_percent,
|
||||||
indexers: data.indexers || [],
|
indexers: data.indexers || [],
|
||||||
actions: data.actions || [],
|
actions: data.actions || [],
|
||||||
}}
|
formats: data.formats || [],
|
||||||
|
quality: data.quality || [],
|
||||||
|
match_release_types: data.match_release_types || [],
|
||||||
|
log_score: data.log_score,
|
||||||
|
log: data.log,
|
||||||
|
cue: data.cue,
|
||||||
|
perfect_flac: data.perfect_flac,
|
||||||
|
artists: data.artists,
|
||||||
|
albums: data.albums,
|
||||||
|
} as Filter}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{({ values, dirty, resetForm }) => (
|
{({ values, dirty, resetForm }) => (
|
||||||
|
@ -265,6 +275,10 @@ export default function FilterDetails() {
|
||||||
<MoviesTv />
|
<MoviesTv />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path={`${url}/music`}>
|
||||||
|
<Music />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/advanced`}>
|
<Route path={`${url}/advanced`}>
|
||||||
<Advanced />
|
<Advanced />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -405,6 +419,58 @@ function MoviesTv() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Music() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="artists" label="Artists" columns={4} placeholder="eg. Aritst One" />
|
||||||
|
<TextField name="albums" label="Albums" columns={4} placeholder="eg. That Album" />
|
||||||
|
<TextField name="years" label="Years" columns={4} placeholder="eg. 2018,2019-2021" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8">
|
||||||
|
<TitleSubtitle title="Quality" subtitle="Format, source, log etc." />
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<MultiSelect name="formats" options={FORMATS_OPTIONS} label="Format" columns={6} />
|
||||||
|
<MultiSelect name="quality" options={QUALITY_MUSIC_OPTIONS} label="Quality" columns={6} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<MultiSelect name="sources" options={SOURCES_MUSIC_OPTIONS} label="sources" columns={6} />
|
||||||
|
<MultiSelect name="match_release_types" options={RELEASE_TYPE_MUSIC_OPTIONS} label="Type" columns={6} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<NumberField name="log_score" label="Log score" placeholder="eg. 100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6 sm:space-y-5 divide-y divide-gray-200">
|
||||||
|
<div className="pt-6 sm:pt-5">
|
||||||
|
<div role="group" aria-labelledby="label-email">
|
||||||
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
|
||||||
|
{/* <div>
|
||||||
|
<div className="text-base font-medium text-gray-900 sm:text-sm sm:text-gray-700" id="label-email">
|
||||||
|
Extra
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg space-y-4">
|
||||||
|
<CheckboxField name="log" label="Log" sublabel="Must include Log" />
|
||||||
|
<CheckboxField name="cue" label="Cue" sublabel="Must include Cue"/>
|
||||||
|
<CheckboxField name="perfect_flac" label="Perfect FLAC" sublabel="Override all options about quality, source, format, and cue/log/log score"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Advanced() {
|
function Advanced() {
|
||||||
const [releasesIsOpen, toggleReleases] = useToggle(false)
|
const [releasesIsOpen, toggleReleases] = useToggle(false)
|
||||||
const [groupsIsOpen, toggleGroups] = useToggle(false)
|
const [groupsIsOpen, toggleGroups] = useToggle(false)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue