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
use crate::prelude::*;
use std::cmp::Ordering;
use std::convert::Infallible;
use std::time::Duration;
/// URL of the RED indexer.
pub const RED_URL: &str = "https://redacted.sh";
/// URL of the previous RED indexer.
pub const RED_URL_CH: &str = "https://redacted.ch";
/// URL of the OPS indexer.
pub const OPS_URL: &str = "https://orpheus.network";
/// Tracker announce host for RED.
pub const RED_TRACKER_URL: &str = "https://flacsfor.me";
/// Tracker announce host for OPS.
pub const OPS_TRACKER_URL: &str = "https://home.opsfet.ch";
/// Indexer that a [`QueueItem`] belongs to.
///
/// - Deserializes case-insensitively from a string
/// - Serializes as a lowercase string
/// - Unknown values are normalized to lowercase and preserved in [`Indexer::Other`]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(from = "String", into = "String")]
pub enum Indexer {
#[default]
Red,
Pth,
Ops,
/// Any indexer not listed above.
///
/// The contained [`String`] must be lowercase. Constructing via [`From`] or
/// [`FromStr`] enforces this, so direct construction should be avoided.
Other(String),
}
impl Indexer {
/// Lowercase string form of the indexer.
#[must_use]
pub fn as_lowercase(&self) -> &str {
match self {
Indexer::Red => "red",
Indexer::Pth => "pth",
Indexer::Ops => "ops",
Indexer::Other(value) => value,
}
}
/// Uppercase string form of the indexer.
#[must_use]
pub fn to_uppercase(&self) -> String {
self.as_lowercase().to_uppercase()
}
/// Gazelle API rate limit for this indexer as `(requests, window)`.
///
/// - RED allows 10 per 10s with API key auth; we use 8 to leave headroom
/// - OPS applies per-action limits with `browse` capped at 5 per 10s; we use 4 to leave headroom
/// - Other Gazelle-based trackers fall back to a conservative default
#[must_use]
pub fn gazelle_rate_limit(&self) -> (usize, Duration) {
match self {
Indexer::Red => (8, Duration::from_secs(10)),
Indexer::Ops => (4, Duration::from_secs(10)),
_ => (5, Duration::from_secs(10)),
}
}
/// Delays between retry attempts when the API returns `TooManyRequests`.
///
/// - OPS enforces a longer cooldown so uses `10s, 20s`
/// - Other Gazelle-based indexers use `5s, 10s`
/// - Up to 3 attempts total before the error propagates
#[must_use]
pub fn gazelle_retry_delays(&self) -> Vec<Duration> {
match self {
Indexer::Ops => vec![Duration::from_secs(10), Duration::from_secs(20)],
_ => vec![Duration::from_secs(5), Duration::from_secs(10)],
}
}
/// Check if `other` is the same indexer as this one, allowing known alternatives.
///
/// - Asymmetric: if this is `Red`, `Pth` is accepted as an alternative
/// - The reverse does not hold: `Pth` does not match `Red`
pub fn match_with_alts(&self, other: &Indexer) -> bool {
self == other || (self == &Indexer::Red && other == &Indexer::Pth)
}
}
impl From<&str> for Indexer {
fn from(value: &str) -> Self {
let lowercase = value.to_lowercase();
match lowercase.as_str() {
"red" => Indexer::Red,
"pth" => Indexer::Pth,
"ops" => Indexer::Ops,
_ => Indexer::Other(lowercase),
}
}
}
impl From<String> for Indexer {
fn from(value: String) -> Self {
Indexer::from(value.as_str())
}
}
impl From<Indexer> for String {
fn from(value: Indexer) -> Self {
value.as_lowercase().to_owned()
}
}
impl FromStr for Indexer {
type Err = Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Indexer::from(value))
}
}
impl Ord for Indexer {
fn cmp(&self, other: &Self) -> Ordering {
self.as_lowercase().cmp(other.as_lowercase())
}
}
impl PartialOrd for Indexer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Display for Indexer {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(&self.to_uppercase(), f)
}
}