Repository navigation
Expand file tree
/
Copy pathsource.go
More file actions
138 lines (124 loc) · 3.79 KB
/
Copy pathsource.go
File metadata and controls
138 lines (124 loc) · 3.79 KB
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Package subapi searches subtitle sites and downloads what they have.
//
// Two sources: SubDL (subdl.com) and OpenSubtitles (opensubtitles.com). Both
// implement Source, so callers can try them in order and treat the results
// the same way. Downloads are deferred to Candidate.Fetch because that's the
// metered half on opensubtitles; only pay for the ones you try.
//
// sources := []subapi.Source{
// &subapi.SubDL{APIKey: subdlKey},
// &subapi.OpenSubtitles{APIKey: osKey, Username: u, Password: p},
// }
// q := subapi.Query{TMDBID: 1396, Season: 1, Episode: 1, Lang: "ar"}
// for _, src := range sources {
// cands, err := src.Find(ctx, q)
// if err != nil {
// continue
// }
// for _, c := range cands {
// srt, err := c.Fetch(ctx)
// // ...
// }
// }
//
// Season packs are unpacked for you: Fetch on an episode query returns that
// episode's .srt, not the archive.
//
// SubDL's AI endpoints (translate, transcribe, filename search) are methods on
// SubDL. See ai.go for their state.
package subapi
import (
"context"
"path"
"regexp"
"strconv"
"strings"
)
// Query says what we want a subtitle for.
type Query struct {
TMDBID int // series id for TV, movie id for film
IMDBID string // "tt1234567", used when TMDBID is 0
Title string // free text fallback when no id is known
Year int
Season int // 0 for a movie
Episode int // 0 for a movie
// Lang is the language wanted, e.g. "en". Empty means any.
Lang string
}
func (q Query) isEpisode() bool { return q.Season > 0 && q.Episode > 0 }
// Candidate is one subtitle a Source found. Fetch is deferred because
// downloading is the rate limited half; only pay for the ones we try.
type Candidate struct {
Source string
Release string
// The source's own id for this subtitle, if it has one. SubDL fills it
// with the nId its translation endpoint takes.
ID string
Fetch func(context.Context) ([]byte, error)
}
// Source is one subtitle site.
type Source interface {
Name() string
Find(context.Context, Query) ([]Candidate, error)
}
// S01E02, 1x02
var episodeRe = regexp.MustCompile(`(?i)(?:s(\d{1,2})[\s._-]*e(\d{1,3})|(\d{1,2})x(\d{1,3}))`)
var numberRe = regexp.MustCompile(`\d+`)
// pickEpisode returns the index of the archive member holding the episode we
// want, or -1.
//
// Season packs are the norm on subdl and uploaders name the members however
// they like: "Show.S01E02.srt" but just as often "show <arabic title> 2.srt".
func pickEpisode(names []string, season, episode int) int {
for i, n := range names {
if matchesEpisode(n, season, episode) {
return i
}
}
// Fall back to a bare number, but only where it can't mislead. If any
// member has an explicit marker then this pack does label its episodes
// and ours just isn't here; guessing at loose digits would pick a
// neighbour. Same if a number matches two members ("season 1 ep 5" vs
// "ep 1"). A wrong subtitle is worse than none.
for _, n := range names {
if episodeRe.MatchString(n) {
return -1
}
}
found := -1
for i, n := range names {
if !hasNumber(n, episode) {
continue
}
if found >= 0 {
return -1
}
found = i
}
return found
}
func matchesEpisode(name string, season, episode int) bool {
for _, m := range episodeRe.FindAllStringSubmatch(name, -1) {
s, e := m[1], m[2]
if s == "" {
s, e = m[3], m[4]
}
si, _ := strconv.Atoi(s)
ei, _ := strconv.Atoi(e)
if si == season && ei == episode {
return true
}
}
return false
}
// hasNumber reports whether any run of digits in the name equals n. Whole
// runs only, so "11" never matches 1.
func hasNumber(name string, n int) bool {
name = strings.TrimSuffix(name, path.Ext(name))
for _, d := range numberRe.FindAllString(name, -1) {
if v, err := strconv.Atoi(d); err == nil && v == n {
return true
}
}
return false
}