diff --git a/internal/database/filter.go b/internal/database/filter.go index cc069d3..549a78f 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -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 { diff --git a/internal/database/migrate.go b/internal/database/migrate.go index 9d31b28..74ff18d 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -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 { diff --git a/internal/database/release.go b/internal/database/release.go index 07d7625..74a3b7c 100644 --- a/internal/database/release.go +++ b/internal/database/release.go @@ -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...) diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 6589acf..d082833 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -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 diff --git a/internal/domain/filter.go b/internal/domain/filter.go index a260385..9061304 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -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"` } diff --git a/internal/domain/release.go b/internal/domain/release.go index f909772..0d6c233 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -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 } diff --git a/internal/domain/release_test.go b/internal/domain/release_test.go index e1a7f01..472a182 100644 --- a/internal/domain/release_test.go +++ b/internal/domain/release_test.go @@ -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) { diff --git a/internal/filter/service.go b/internal/filter/service.go index c98e42f..1154693 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -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 { diff --git a/internal/indexer/api.go b/internal/indexer/api.go index 61ce2fc..0e1427b 100644 --- a/internal/indexer/api.go +++ b/internal/indexer/api.go @@ -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) diff --git a/internal/indexer/definitions/red.yaml b/internal/indexer/definitions/red.yaml index 9bafb1f..242f8af 100644 --- a/internal/indexer/definitions/red.yaml +++ b/internal/indexer/definitions/red.yaml @@ -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 diff --git a/pkg/red/red.go b/pkg/red/red.go new file mode 100644 index 0000000..1c23172 --- /dev/null +++ b/pkg/red/red.go @@ -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 +} diff --git a/pkg/red/red_test.go b/pkg/red/red_test.go new file mode 100644 index 0000000..2c7e591 --- /dev/null +++ b/pkg/red/red_test.go @@ -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) + }) + } +} diff --git a/pkg/red/testdata/get_index.json b/pkg/red/testdata/get_index.json new file mode 100644 index 0000000..fac4e0e --- /dev/null +++ b/pkg/red/testdata/get_index.json @@ -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" + } + } +} \ No newline at end of file diff --git a/pkg/red/testdata/get_torrent_by_id.json b/pkg/red/testdata/get_torrent_by_id.json new file mode 100644 index 0000000..f6af2db --- /dev/null +++ b/pkg/red/testdata/get_torrent_by_id.json @@ -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 + } + } +} \ No newline at end of file diff --git a/pkg/red/testdata/get_torrent_by_id_not_found.json b/pkg/red/testdata/get_torrent_by_id_not_found.json new file mode 100644 index 0000000..4e7ea48 --- /dev/null +++ b/pkg/red/testdata/get_torrent_by_id_not_found.json @@ -0,0 +1 @@ +{"status":"failure","error":"bad id parameter"} \ No newline at end of file diff --git a/web/src/components/inputs/common.tsx b/web/src/components/inputs/common.tsx index b4e9edd..df9b992 100644 --- a/web/src/components/inputs/common.tsx +++ b/web/src/components/inputs/common.tsx @@ -14,4 +14,30 @@ const ErrorField: React.FC = ({ name, classNames }) => ( } ); -export { ErrorField } \ No newline at end of file + +interface CheckboxFieldProps { + name: string; + label: string; + sublabel?: string; +} + +const CheckboxField: React.FC = ({ name, label, sublabel }) => ( +
+
+ +
+
+ +

{sublabel}

+
+
+) + +export { ErrorField, CheckboxField } \ No newline at end of file diff --git a/web/src/components/inputs/index.ts b/web/src/components/inputs/index.ts index 28a9403..5471b05 100644 --- a/web/src/components/inputs/index.ts +++ b/web/src/components/inputs/index.ts @@ -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"; diff --git a/web/src/domain/constants.ts b/web/src/domain/constants.ts index 5624755..4578efd 100644 --- a/web/src/domain/constants.ts +++ b/web/src/domain/constants.ts @@ -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; diff --git a/web/src/domain/interfaces.ts b/web/src/domain/interfaces.ts index f903407..3be84d3 100644 --- a/web/src/domain/interfaces.ts +++ b/web/src/domain/interfaces.ts @@ -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; diff --git a/web/src/screens/filters/details.tsx b/web/src/screens/filters/details.tsx index bc74d9f..675464f 100644 --- a/web/src/screens/filters/details.tsx +++ b/web/src/screens/filters/details.tsx @@ -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() { + + + + @@ -405,6 +419,58 @@ function MoviesTv() { ) } +function Music() { + return ( +
+
+ + + +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
+
+ {/*
+
+ Extra +
+
*/} +
+
+ + + +
+
+
+
+
+
+
+ ) +} + function Advanced() { const [releasesIsOpen, toggleReleases] = useToggle(false) const [groupsIsOpen, toggleGroups] = useToggle(false)