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:
Ludvig Lundgren 2022-01-19 18:50:04 +01:00 committed by GitHub
parent 30c11d4ef1
commit 00bc8298ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1053 additions and 52 deletions

View file

@ -58,7 +58,7 @@ func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
//r.db.lock.RLock()
//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
@ -66,11 +66,11 @@ func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
return nil, err
}
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
var useRegex, scene, freeleech sql.NullBool
var delay sql.NullInt32
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, hasLog, hasCue, perfectFlac sql.NullBool
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)
return nil, err
}
@ -87,6 +87,12 @@ func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
f.Seasons = seasons.String
f.Episodes = episodes.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.ExceptCategories = exceptCategories.String
f.MatchUploaders = matchUploaders.String
@ -131,6 +137,15 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
f.match_hdr,
f.except_hdr,
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.except_categories,
f.match_uploaders,
@ -155,11 +170,11 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
for rows.Next() {
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 useRegex, scene, freeleech sql.NullBool
var delay sql.NullInt32
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, hasLog, hasCue, perfectFlac sql.NullBool
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")
return nil, err
}
@ -176,6 +191,12 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
f.Seasons = seasons.String
f.Episodes = episodes.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.ExceptCategories = exceptCategories.String
f.MatchUploaders = matchUploaders.String
@ -234,9 +255,18 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
match_uploaders,
except_uploaders,
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.Enabled,
filter.MinSize,
@ -266,6 +296,15 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
filter.ExceptUploaders,
filter.Tags,
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 {
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 = ?,
tags = ?,
except_tags = ?,
artists = ?,
albums = ?,
release_types_match = ?,
formats = ?,
quality = ?,
log_score = ?,
has_log = ?,
has_cue = ?,
perfect_flac = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
filter.Name,
@ -348,6 +396,15 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.
filter.ExceptUploaders,
filter.Tags,
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,
)
if err != nil {

View file

@ -84,6 +84,16 @@ CREATE TABLE filter
match_hdr TEXT [] DEFAULT '{}',
except_hdr TEXT [] DEFAULT '{}',
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,
except_categories TEXT,
match_uploaders TEXT,
@ -178,7 +188,7 @@ CREATE TABLE "release"
artists TEXT [] DEFAULT '{}' NOT NULL,
type TEXT,
format TEXT,
bitrate TEXT,
quality TEXT,
log_score INTEGER,
has_log BOOLEAN,
has_cue BOOLEAN,
@ -289,6 +299,40 @@ var migrations = []string{
ALTER TABLE "filter"
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 {

View file

@ -24,8 +24,8 @@ func (repo *ReleaseRepo) Store(ctx context.Context, r *domain.Release) (*domain.
query, args, err := sq.
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").
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).
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.Quality, r.LogScore, r.HasLog, r.HasCue, r.IsScene, r.Origin, pq.Array(r.Tags), r.Freeleech, r.FreeleechPercent, r.Uploader, r.PreTime).
ToSql()
res, err := repo.db.handler.ExecContext(ctx, query, args...)

View file

@ -44,7 +44,7 @@ func (db *SqliteDB) Open() error {
// Set busy timeout
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

View file

@ -53,22 +53,23 @@ type Filter struct {
Years string `json:"years"`
Artists string `json:"artists"`
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"`
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
Media []string `json:"media"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
Cue bool `json:"cue"`
Log bool `json:"log"`
LogScores string `json:"log_scores"`
MatchCategories string `json:"match_categories"`
ExceptCategories string `json:"except_categories"`
MatchUploaders string `json:"match_uploaders"`
ExceptUploaders string `json:"except_uploaders"`
Tags string `json:"tags"`
ExceptTags string `json:"except_tags"`
TagsAny string `json:"tags_any"`
ExceptTagsAny string `json:"except_tags_any"`
Actions []Action `json:"actions"`
Indexers []Indexer `json:"indexers"`
Formats []string `json:"formats"` // MP3, FLAC, Ogg, AAC, AC3, DTS
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
PerfectFlac bool `json:"perfect_flac"`
Cue bool `json:"cue"`
Log bool `json:"log"`
LogScore int `json:"log_score"`
MatchCategories string `json:"match_categories"`
ExceptCategories string `json:"except_categories"`
MatchUploaders string `json:"match_uploaders"`
ExceptUploaders string `json:"except_uploaders"`
Tags string `json:"tags"`
ExceptTags string `json:"except_tags"`
TagsAny string `json:"tags_any"`
ExceptTagsAny string `json:"except_tags_any"`
Actions []Action `json:"actions"`
Indexers []Indexer `json:"indexers"`
}

View file

@ -73,7 +73,7 @@ type Release struct {
Artists []string `json:"artists"`
Type string `json:"type"` // Album,Single,EP
Format string `json:"format"` // music only
Bitrate string `json:"bitrate"` // bitrate
Quality string `json:"quality"` // quality
LogScore int `json:"log_score"`
HasLog bool `json:"has_log"`
HasCue bool `json:"has_cue"`
@ -151,6 +151,10 @@ func (r *Release) Parse() 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`)
if err != nil {
return err
@ -285,7 +289,7 @@ func (r *Release) extractHDR() 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 {
return err
}
@ -299,7 +303,7 @@ func (r *Release) extractAudioFromTags(tag string) error {
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 {
return err
}
@ -308,6 +312,20 @@ func (r *Release) extractAudioFromTags(tag string) error {
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 {
// v, err := findLast(tag, `Cue`)
// if err != nil {
@ -477,14 +495,14 @@ func (r *Release) extractLogScoreFromTags(tag string) error {
return nil
}
func (r *Release) extractBitrateFromTags(tag string) error {
if r.Bitrate != "" {
func (r *Release) extractQualityFromTags(tag string) error {
if r.Quality != "" {
return nil
}
// 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 {
return err
//return errors.Wrapf(err, "invalid regex: %s", value)
@ -496,7 +514,7 @@ func (r *Release) extractBitrateFromTags(tag string) error {
if len(matches) >= 1 {
last := matches[len(matches)-1]
r.Bitrate = last
r.Quality = last
return nil
}
}
@ -516,13 +534,14 @@ func (r *Release) extractReleaseTags() error {
var err error
err = r.extractAudioFromTags(t)
err = r.extractFormatsFromTags(t)
err = r.extractResolutionFromTags(t)
err = r.extractCodecFromTags(t)
err = r.extractContainerFromTags(t)
err = r.extractSourceFromTags(t)
err = r.extractFreeleechFromTags(t)
err = r.extractLogScoreFromTags(t)
err = r.extractBitrateFromTags(t)
err = r.extractQualityFromTags(t)
err = r.extractAnimeGroupFromTags(t)
if err != nil {
@ -781,6 +800,11 @@ func (r *Release) CheckFilter(filter Filter) bool {
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) {
return false
}
@ -795,6 +819,49 @@ func (r *Release) CheckFilter(filter Filter) bool {
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
}

View file

@ -158,6 +158,8 @@ func TestRelease_Parse(t *testing.T) {
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
Group: "",
Audio: "FLAC",
Format: "FLAC",
Quality: "Lossless",
Source: "CD",
HasCue: 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"},
ReleaseTags: "MP3 / 320 / Cassette",
Group: "",
Audio: "MP3",
Format: "MP3",
Source: "Cassette",
Bitrate: "320",
Quality: "320",
},
wantErr: false,
},
@ -195,7 +197,8 @@ func TestRelease_Parse(t *testing.T) {
Clean: "The artist (ザ・フリーダムユニティ) Long album name",
ReleaseTags: "MP3 / V0 (VBR) / CD",
Group: "",
Audio: "MP3",
Format: "MP3",
Quality: "V0 (VBR)",
Source: "CD",
},
wantErr: false,
@ -212,6 +215,29 @@ func TestRelease_Parse(t *testing.T) {
ReleaseTags: "FLAC / Lossless / Log / 100% / Cue / CD",
Group: "",
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",
HasCue: true,
HasLog: true,
@ -1031,6 +1057,192 @@ func TestRelease_CheckFilter(t *testing.T) {
},
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 {
t.Run(tt.name, func(t *testing.T) {

View file

@ -211,8 +211,8 @@ func (s *service) FindAndCheckFilters(release *domain.Release) (bool, *domain.Fi
if release.AdditionalSizeCheckRequired {
log.Debug().Msgf("filter-service.find_and_check_filters: (%v) additional size check required", f.Name)
// check if indexer = btn,ptp (ggn,red later)
if release.Indexer == "ptp" || release.Indexer == "btn" || release.Indexer == "ggn" {
// check if indexer = btn, ptp, ggn or red
if release.Indexer == "ptp" || release.Indexer == "btn" || release.Indexer == "ggn" || release.Indexer == "redacted" {
// fetch torrent info from api
// save outside of loop to check multiple filters with only one fetch
if torrentInfo == nil {

View file

@ -9,6 +9,7 @@ import (
"github.com/autobrr/autobrr/pkg/btn"
"github.com/autobrr/autobrr/pkg/ggn"
"github.com/autobrr/autobrr/pkg/ptp"
"github.com/autobrr/autobrr/pkg/red"
)
type APIService interface {
@ -104,6 +105,13 @@ func (s *apiService) AddClient(indexer string, settings map[string]string) error
}
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:
return fmt.Errorf("api_service: could not initialize client: unsupported indexer '%v'", indexer)

View file

@ -9,6 +9,7 @@ urls:
privacy: private
protocol: torrent
supports:
- api
- irc
- rss
source: gazelle
@ -21,6 +22,22 @@ settings:
type: secret
label: 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:
network: Scratch-Network
@ -55,12 +72,13 @@ parse:
- 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"
- "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:
- torrentName
- year
- category
- releaseTags
- torrentId
- baseUrl
- tags

212
pkg/red/red.go Normal file
View 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
View 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
View 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
View 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
}
}
}

View file

@ -0,0 +1 @@
{"status":"failure","error":"bad id parameter"}

View file

@ -14,4 +14,30 @@ const ErrorField: React.FC<ErrorFieldProps> = ({ name, classNames }) => (
}
</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 }

View file

@ -1,4 +1,4 @@
export { ErrorField } from "./common";
export { ErrorField, CheckboxField } from "./common";
export { TextField, NumberField, PasswordField } from "./input";
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
export { RadioFieldsetWide } from "./radio";

View file

@ -53,7 +53,16 @@ export const sources = [
"HDTV",
"Mixed",
"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}));
@ -80,6 +89,68 @@ export const hdr = [
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 {
label: string;
description: string;

View file

@ -83,8 +83,17 @@ export interface Filter {
sources: string[];
codecs: string[];
containers: string[];
match_release_types: string[];
quality: string[];
formats: string[];
match_hdr: string[];
except_hdr: string[];
log_score: number;
log: boolean;
cue: boolean;
perfect_flac: boolean;
artists: string;
albums: string;
seasons: string;
episodes: string;
match_releases: string;

View file

@ -16,7 +16,7 @@ import { Action, ActionType, DownloadClient, Filter, Indexer } from "../../domai
import { useToggle } from "../../hooks/hooks";
import { useMutation, useQuery } from "react-query";
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 { TitleSubtitle } from "../../components/headings";
@ -30,11 +30,12 @@ import Toast from '../../components/notifications/Toast';
import { Field, FieldArray, Form, Formik } from "formik";
import { AlertWarning } from "../../components/alerts";
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 = [
{ name: 'General', href: '', current: true },
{ name: 'Movies and TV', href: 'movies-tv', current: false },
{ name: 'Music', href: 'music', current: false },
// { name: 'P2P', href: 'p2p', current: false },
{ name: 'Advanced', href: 'advanced', current: false },
{ name: 'Actions', href: 'actions', current: false },
@ -251,7 +252,16 @@ export default function FilterDetails() {
freeleech_percent: data.freeleech_percent,
indexers: data.indexers || [],
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}
>
{({ values, dirty, resetForm }) => (
@ -265,6 +275,10 @@ export default function FilterDetails() {
<MoviesTv />
</Route>
<Route path={`${url}/music`}>
<Music />
</Route>
<Route path={`${url}/advanced`}>
<Advanced />
</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() {
const [releasesIsOpen, toggleReleases] = useToggle(false)
const [groupsIsOpen, toggleGroups] = useToggle(false)