Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ func main() {
// group: NOiR
}
```
## Build standalone cli
```
go build ./cmd/rls
```

## Run tests
```
go test ./...
```
66 changes: 66 additions & 0 deletions cmd/rls/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"strings"

"github.com/moistari/rls"
)

var pretty = flag.Bool("pretty", false, "pretty-print JSON output")

func processLine(line string) error {
line = strings.TrimSpace(line)
if line == "" {
return nil
}

// Just parse the release directly
r := rls.ParseString(line)

var b []byte
var err error

if *pretty {
b, err = json.MarshalIndent(r, "", " ")
} else {
b, err = json.Marshal(r)
}

if err != nil {
return err
}
fmt.Print(string(b))
return nil
}

func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
// read lines from stdin
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
if err := processLine(scanner.Text()); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error reading stdin:", err)
os.Exit(1)
}
return
}
// treat each arg as a separate release name
for _, a := range args {
if err := processLine(a); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
}
16 changes: 8 additions & 8 deletions lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,25 @@ func DefaultLexers() []Lexer {
NewRegexpSourceLexer(TagTypeCollection, true),
NewSeriesLexer(
// s02, S01E01
`(?i)^s(?P<s>[0-8]?\d)[\-\._ ]?(?:e(?P<e>\d{1,5}))?\b`,
`(?i)^s(?P<s>\d{1,4})[\-\._ ]?(?:e(?P<e>\d{1,5}))?\b`,
// S01E02E03, S01E02-E03, S01E03.E04.E05
`(?i)^s(?P<s>[0-8]?\d)(?P<m>(?:[\-\._ ]?e\d{1,5}){1,5})\b`,
`(?i)^s(?P<s>\d{1,4})(?P<m>(?:[\-\._ ]?e\d{1,5}){1,5})\b`,
// S01S02S03
`(?i)^(?P<S>(?:s[0-8]?\d){2,4})\b`,
`(?i)^(?P<S>(?:s\d{1,4}){2,4})\b`,
// 2x1, 1x01
`(?i)^(?P<s>[0-8]?\d)x(?P<e>\d{1,3})\b`,
`(?i)^(?P<s>\d{1,4})x(?P<e>\d{1,5})\b`,
// S01 - 02v3, S07-06, s03-5v.9
`(?i)^s(?P<s>[0-8]?\d)[\-\._ ]{1,3}(?P<e>\d{1,5})(?:[\-\._ ]{1,3}(?P<v>v\d+(?:\.\d+){0,2}))?\b`,
`(?i)^s(?P<s>\d{1,4})[\-\._ ]{1,3}(?P<e>\d{1,5})(?:[\-\._ ]{1,3}(?P<v>v\d+(?:\.\d+){0,2}))?\b`,
// Season.01.Episode.02, Series.01.Ep.02, Series.01, Season.01
`(?i)^(?:series|season|s)[\-\._ ]?(?P<s>[0-8]?\d)(?:[\-\._ ]?(?:episode|ep)(?P<e>\d{1,5}))?\b`,
`(?i)^(?:series|season|s)[\-\._ ]?(?P<s>\d{1,2})(?:[\-\._ ]?(?:episode|ep)(?P<e>\d{1,5}))?\b`,
// Vol.1.No.2, vol1no2
`(?i)^vol(?:ume)?[\-\._ ]?(?P<s>\d{1,3})(?:[\-\._ ]?(?:number|no)[\-\._ ]?(?P<e>\d{1,5}))\b`,
// Episode 15, E009, Ep. 007, Ep.05-07
`(?i)^e(?:p(?:isode)?[\-\._ ]{1,3})?(?P<e>\d{1,5})(?:[\-\._ ]{1,3}\d{1,3})?\b`,
// 10v1.7, 13v2
`(?i)^(?P<e>\d{1,5})(?P<v>v[\-\._ ]?\d+(?:\.\d){0,2})\b`,
// S01.Disc02, s01D3, Series.01.Disc.02, S02DVD3
`(?i)^(?:series|season|s)[\-\._ ]?(?P<s>[0-8]?\d)[\-\._ ]?(?P<d>(?:disc|disk|dvd|d)[\-\._ ]?(?:\d{1,3}))\b`,
`(?i)^(?:series|season|s)[\-\._ ]?(?P<s>\d{1,4})[\-\._ ]?(?P<d>(?:disc|disk|dvd|d)[\-\._ ]?(?:\d{1,3}))\b`,
// s1957e01
`(?i)^s(?P<s>19\d\d)e(?P<e>\d{2,4})\b`,
),
Expand Down Expand Up @@ -231,7 +231,7 @@ func NewDateLexer(strs ...string) Lexer {
func NewSeriesLexer(strs ...string) Lexer {
var sourcef taginfo.FindFunc
lexer := NamedCaptureLexer(strs...)
mlt := regexp.MustCompile(`(?i)s(\d?\d)`)
mlt := regexp.MustCompile(`(?i)s(\d{1,4})`)
mny := regexp.MustCompile(`(?i)[\-\._ ]?e(\d{1,5})`)
dsc := regexp.MustCompile(`(?i)^disc|disk|dvd|d`)
return TagLexer{
Expand Down
59 changes: 51 additions & 8 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,17 @@ func (b *TagBuilder) fixMusic(r *Release) {

// collect collects tags into the release.
func (b *TagBuilder) collect(r *Release) {
// determine pivot and the text end prior to pivot — only consider series
// tags that occur before this 'end' (the first text prior to the pivot).
// Note: do not include TagTypeSeries when computing the pivot here —
// including it causes the pivot to point to series tags themselves and
// leads to misclassification when series tags appear early in the name.
// compute pivot based on source/resolution/version so that series tags
// following a title or year (TagTypeDate) are still accepted. Excluding
// TagTypeDate prevents dates from moving the pivot before legitimate
// series tokens.
_, seriesTextEnd := b.pivots(r, TagTypeSource, TagTypeResolution, TagTypeVersion)

for i := 0; i < len(r.tags); i++ {
switch r.tags[i].typ {
case TagTypeText:
Expand Down Expand Up @@ -534,12 +545,26 @@ func (b *TagBuilder) collect(r *Release) {
case TagTypeDate:
r.Year, r.Month, r.Day = r.tags[i].Date()
case TagTypeSeries:
series, episode := r.tags[i].Series()
// only accept series tags from the initial part of the release
// name (typically after the title or year). series tokens found
// after the pivot (eg. in codec/version sections) are ignored.
if i >= seriesTextEnd {
// treat as text (leave for unused/group heuristics)
r.tags[i] = r.tags[i].As(TagTypeText, nil)
break
}
// inspect raw tag captures so we can detect explicit episode captures
tag := r.tags[i]
series, episode := tag.Series()
if r.Series == 0 {
r.Series = series
}
if r.Episode == 0 {
r.Episode = episode
// if the tag actually captured an episode (even "0"), set episode
if len(tag.v) > 2 && tag.v[2] != "" {
// prefer first explicit episode capture
if r.Episode == 0 && (episode != 0 || tag.v[2] == "0") {
r.Episode = episode
}
}
case TagTypeVersion:
if r.Version == "" {
Expand Down Expand Up @@ -627,6 +652,14 @@ func (b *TagBuilder) inspect(r *Release, initial bool) Type {
return r.Type
}
n := len(r.tags)
// detect explicit episode captures in tags (including "0")
episodePresent := false
for _, t := range r.tags {
if t.Is(TagTypeSeries) && len(t.v) > 2 && t.v[2] != "" {
episodePresent = true
break
}
}
// inspect types
var app, series, movie bool
for i := n; i > 0; i-- {
Expand All @@ -642,12 +675,12 @@ func (b *TagBuilder) inspect(r *Release, initial bool) Type {
}
return typ
case Series, Episode:
if r.Episode != 0 || (r.Series == 0 && r.Episode == 0) && !contains(r.Other, "BOXSET") {
if episodePresent || (r.Series == 0 && !episodePresent) && !contains(r.Other, "BOXSET") {
return Episode
}
return Series
case Education:
if r.Series == 0 && r.Episode == 0 {
if r.Series == 0 && !episodePresent {
return Education
}
case Music:
Expand All @@ -664,7 +697,7 @@ func (b *TagBuilder) inspect(r *Release, initial bool) Type {
// exclusive tag not superseded by version/episode/date
if r.tags[i-1].InfoExcl() &&
r.Version == "" &&
r.Series == 0 && r.Episode == 0 &&
r.Series == 0 && !episodePresent &&
r.Day == 0 && r.Month == 0 {
return typ
}
Expand All @@ -690,7 +723,7 @@ func (b *TagBuilder) inspect(r *Release, initial bool) Type {
}
// defaults
switch {
case r.Episode != 0 || (r.Year != 0 && r.Month != 0 && r.Day != 0):
case episodePresent || (r.Year != 0 && r.Month != 0 && r.Day != 0):
return Episode
case r.Series != 0 || series:
return Series
Expand Down Expand Up @@ -1180,7 +1213,17 @@ func (b *TagBuilder) unused(r *Release, i int) {
case r.Sum == "" && b.sum.MatchString(s) && strings.ContainsAny(s, "0123456789"):
r.Sum, r.unused = s, r.unused[:n-1]
case r.Group == "" && !b.digits.MatchString(s):
r.Group, r.unused = s, r.unused[:n-1]
// if the unused tag was originally a series token, reconstruct
// a normalized series-like group (eg. "S97") using the numeric
// capture rather than the raw captured text (which may include
// punctuation such as a trailing dot).
if r.tags[r.unused[n-1]].Was(TagTypeSeries) {
series, _ := r.tags[r.unused[n-1]].Series()
r.Group = fmt.Sprintf("S%v", series)
} else {
r.Group = s
}
r.unused = r.unused[:n-1]
}
}
}
Expand Down
1 change: 1 addition & 0 deletions reutil/reutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func Join(quote bool, strs ...string) string {
// Build builds a regexp for strings.
//
// Config options:
//
// i - ignore case
// ^ - add ^ start anchor
// a - add \b start anchor
Expand Down
18 changes: 14 additions & 4 deletions rls.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rls

import (
"bytes"
"encoding/json"
"fmt"
"math"
"regexp"
Expand Down Expand Up @@ -261,8 +262,8 @@ func (tag Tag) Info() *taginfo.Taginfo {
// SingleEp returns true when the
func (tag Tag) SingleEp() bool {
if tag.typ.Is(TagTypeSeries) {
s, e := tag.Series()
return s == 0 && e != 0 && tag.v[1] == "" && tag.v[0] == tag.v[2]
s, _ := tag.Series()
return s == 0 && len(tag.v) > 2 && tag.v[2] != "" && tag.v[1] == "" && tag.v[0] == tag.v[2]
}
return false
}
Expand Down Expand Up @@ -346,7 +347,7 @@ func (tag Tag) Normalize() string {
return strconv.Itoa(year)
case TagTypeSeries:
series, episode := tag.Series()
if episode != 0 {
if len(tag.v) > 2 && tag.v[2] != "" {
return fmt.Sprintf("S%02dE%02d", series, episode)
}
return fmt.Sprintf("S%02d", series)
Expand Down Expand Up @@ -521,7 +522,10 @@ func (tag Tag) Series() (int, int) {
func (tag Tag) Episodes() []int {
var v []int
for _, b := range tag.v[2:] {
if episode, _ := strconv.Atoi(b); episode != 0 {
if b == "" {
continue
}
if episode, err := strconv.Atoi(b); err == nil {
v = append(v, episode)
}
}
Expand Down Expand Up @@ -703,6 +707,12 @@ const (
Series
)

// MarshalJSON ensures that when this type is converted to JSON,
// it uses its string representation instead of the integer value.
func (t Type) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}

// ParseType parses a type from s.
func ParseType(s string) Type {
switch s {
Expand Down
Loading