Skip to content

Commit ff481b2

Browse files
committed
add streaming setting to page and support binstall disc
1 parent 78e4eca commit ff481b2

File tree

11 files changed

+90
-81
lines changed

11 files changed

+90
-81
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
name = "ferristream"
33
version = "0.0.10"
44
edition = "2024"
5+
repository = "https://github.com/van-sprundel/ferristream"
6+
7+
[package.metadata.binstall]
8+
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.{ archive-format }"
9+
pkg-fmt = "tgz"
10+
11+
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
12+
pkg-fmt = "zip"
513

614
[dependencies]
715
librqbit = { version = "8.1.1", default-features = false, features = ["http-api", "tracing-subscriber-utils", "rust-tls"] }

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ Sometimes I just want to try out a movie/series without having to download their
88

99
## Installation
1010

11-
Download the latest binary from [Releases](https://github.com/van-sprundel/ferristream/releases), or build from source:
11+
Download the latest binary from [Releases](https://github.com/van-sprundel/ferristream/releases).
12+
13+
Or install with [cargo-binstall](https://github.com/cargo-bins/cargo-binstall) (recommended, downloads pre-built binary):
14+
15+
```bash
16+
cargo binstall ferristream
17+
```
18+
19+
Or build from source:
1220

1321
```bash
1422
cargo install --git https://github.com/van-sprundel/ferristream

src/extensions/mod.rs

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,49 +36,41 @@ pub fn parse_episode_info(filename: &str) -> (Option<u32>, Option<u32>) {
3636

3737
// S01E02, S1E2 format (most common)
3838
let sxex_re = Regex::new(r"(?i)[Ss](\d{1,2})[Ee](\d{1,3})").unwrap();
39-
if let Some(caps) = sxex_re.captures(filename) {
40-
if let (Some(s), Some(e)) = (caps.get(1), caps.get(2)) {
41-
if let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
39+
if let Some(caps) = sxex_re.captures(filename)
40+
&& let (Some(s), Some(e)) = (caps.get(1), caps.get(2))
41+
&& let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
4242
return (Some(season), Some(episode));
4343
}
44-
}
45-
}
4644

4745
// 1x02, 01x02 format
4846
let x_re = Regex::new(r"(?i)(\d{1,2})x(\d{1,3})").unwrap();
49-
if let Some(caps) = x_re.captures(filename) {
50-
if let (Some(s), Some(e)) = (caps.get(1), caps.get(2)) {
51-
if let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
47+
if let Some(caps) = x_re.captures(filename)
48+
&& let (Some(s), Some(e)) = (caps.get(1), caps.get(2))
49+
&& let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
5250
return (Some(season), Some(episode));
5351
}
54-
}
55-
}
5652

5753
// Season 1 Episode 2 format (also handles dots instead of spaces)
5854
let full_re = Regex::new(r"(?i)season[.\s]*(\d{1,2}).*episode[.\s]*(\d{1,3})").unwrap();
59-
if let Some(caps) = full_re.captures(filename) {
60-
if let (Some(s), Some(e)) = (caps.get(1), caps.get(2)) {
61-
if let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
55+
if let Some(caps) = full_re.captures(filename)
56+
&& let (Some(s), Some(e)) = (caps.get(1), caps.get(2))
57+
&& let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
6258
return (Some(season), Some(episode));
6359
}
64-
}
65-
}
6660

6761
// .102. or .1002. format (season 1, episode 02 or season 10, episode 02)
6862
// Must be surrounded by dots/spaces to avoid matching years
6963
let compact_re = Regex::new(r"[.\s](\d)(\d{2})[.\s]").unwrap();
70-
if let Some(caps) = compact_re.captures(filename) {
71-
if let (Some(s), Some(e)) = (caps.get(1), caps.get(2)) {
72-
if let (Ok(season), Ok(episode)) =
64+
if let Some(caps) = compact_re.captures(filename)
65+
&& let (Some(s), Some(e)) = (caps.get(1), caps.get(2))
66+
&& let (Ok(season), Ok(episode)) =
7367
(s.as_str().parse::<u32>(), e.as_str().parse::<u32>())
7468
{
7569
// Only valid if episode isn't too high (avoid matching years like 1999)
7670
if (1..=99).contains(&season) && (1..=99).contains(&episode) {
7771
return (Some(season), Some(episode));
7872
}
7973
}
80-
}
81-
}
8274

8375
(None, None)
8476
}

src/history.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,11 @@ impl WatchHistory {
6060
};
6161

6262
// Ensure parent directory exists
63-
if let Some(parent) = path.parent() {
64-
if let Err(e) = std::fs::create_dir_all(parent) {
63+
if let Some(parent) = path.parent()
64+
&& let Err(e) = std::fs::create_dir_all(parent) {
6565
error!("failed to create history directory: {}", e);
6666
return;
6767
}
68-
}
6968

7069
match serde_json::to_string_pretty(self) {
7170
Ok(contents) => {

src/streaming.rs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ impl StreamingSession {
224224
match result {
225225
Ok(info) => {
226226
// Validate the filename if validation is provided
227-
if let Some(ref v) = validation {
228-
if !v.matches(&info.selected_file.name) {
227+
if let Some(ref v) = validation
228+
&& !v.matches(&info.selected_file.name) {
229229
info!(
230230
idx,
231231
name = %info.selected_file.name,
@@ -243,7 +243,6 @@ impl StreamingSession {
243243
}
244244
continue;
245245
}
246-
}
247246
info!(idx, name = %info.selected_file.name, "torrent won the race");
248247
return Ok((idx, info));
249248
}
@@ -437,8 +436,8 @@ impl StreamingSession {
437436
.map_err(|e| StreamError::TorrentError(e.to_string()))?;
438437

439438
// Check if we have file info
440-
if let Some(files) = details.get("files").and_then(|f| f.as_array()) {
441-
if !files.is_empty() {
439+
if let Some(files) = details.get("files").and_then(|f| f.as_array())
440+
&& !files.is_empty() {
442441
info!(files = files.len(), "metadata received");
443442

444443
// Find all video files
@@ -517,7 +516,6 @@ impl StreamingSession {
517516
subtitle_files,
518517
});
519518
}
520-
}
521519

522520
debug!(
523521
elapsed_secs = start.elapsed().as_secs(),
@@ -861,23 +859,19 @@ impl VideoFile {
861859

862860
// S01E02 format
863861
let sxex_re = Regex::new(r"(?i)[Ss](\d{1,2})[Ee](\d{1,3})").unwrap();
864-
if let Some(caps) = sxex_re.captures(&self.name) {
865-
if let (Some(s), Some(e)) = (caps.get(1), caps.get(2)) {
866-
if let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
862+
if let Some(caps) = sxex_re.captures(&self.name)
863+
&& let (Some(s), Some(e)) = (caps.get(1), caps.get(2))
864+
&& let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
867865
return (season, episode);
868866
}
869-
}
870-
}
871867

872868
// 1x02 format
873869
let x_re = Regex::new(r"(?i)(\d{1,2})x(\d{1,3})").unwrap();
874-
if let Some(caps) = x_re.captures(&self.name) {
875-
if let (Some(s), Some(e)) = (caps.get(1), caps.get(2)) {
876-
if let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
870+
if let Some(caps) = x_re.captures(&self.name)
871+
&& let (Some(s), Some(e)) = (caps.get(1), caps.get(2))
872+
&& let (Ok(season), Ok(episode)) = (s.as_str().parse(), e.as_str().parse()) {
877873
return (season, episode);
878874
}
879-
}
880-
}
881875

882876
// If no episode pattern found, use large values to sort at end
883877
(u32::MAX, u32::MAX)
@@ -970,11 +964,10 @@ pub async fn launch_player(
970964
}
971965

972966
// For VLC, subtitles are handled differently
973-
if command.contains("vlc") {
974-
if let Some(sub_url) = subtitle_url {
967+
if command.contains("vlc")
968+
&& let Some(sub_url) = subtitle_url {
975969
cmd.arg(format!("--sub-file={}", sub_url));
976970
}
977-
}
978971

979972
cmd.args(args);
980973
cmd.arg(stream_url);

src/tmdb.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,11 +301,10 @@ pub fn parse_torrent_title(torrent_name: &str) -> (String, Option<u16>) {
301301
.and_then(|m| m.as_str().parse().ok());
302302

303303
// Remove everything after the year (usually quality info)
304-
if let Some(y) = year {
305-
if let Some(idx) = name.find(&y.to_string()) {
304+
if let Some(y) = year
305+
&& let Some(idx) = name.find(&y.to_string()) {
306306
name = name[..idx].to_string();
307307
}
308-
}
309308

310309
// Remove quality patterns
311310
for pattern in quality_patterns {

src/torznab.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,10 @@ impl TorrentResult {
5050
}
5151

5252
// check if link is a magnet url
53-
if let Some(ref link) = self.link {
54-
if link.starts_with("magnet:") {
53+
if let Some(ref link) = self.link
54+
&& link.starts_with("magnet:") {
5555
return Some(link.clone());
5656
}
57-
}
5857

5958
// construct magnet from infohash if available
6059
if let Some(ref hash) = self.infohash {
@@ -173,8 +172,8 @@ impl TorznabClient {
173172
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
174173

175174
// handle <torznab:attr name="X" value="Y" /> elements
176-
if name == "torznab:attr" || name == "attr" {
177-
if let Some(ref mut item) = current_item {
175+
if (name == "torznab:attr" || name == "attr")
176+
&& let Some(ref mut item) = current_item {
178177
let mut attr_name = String::new();
179178
let mut attr_value = String::new();
180179

@@ -198,7 +197,6 @@ impl TorznabClient {
198197
_ => {}
199198
}
200199
}
201-
}
202200
}
203201
Ok(Event::Text(ref e)) => {
204202
if let Some(ref mut item) = current_item {
@@ -219,13 +217,11 @@ impl TorznabClient {
219217
Ok(Event::End(ref e)) => {
220218
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
221219

222-
if name == "item" {
223-
if let Some(item) = current_item.take() {
224-
if !item.title.is_empty() {
220+
if name == "item"
221+
&& let Some(item) = current_item.take()
222+
&& !item.title.is_empty() {
225223
results.push(item);
226224
}
227-
}
228-
}
229225
}
230226
Ok(Event::Eof) => break,
231227
Err(e) => return Err(TorznabError::XmlError(e)),

src/tui/app.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub enum SettingsSection {
7272
Prowlarr,
7373
Tmdb,
7474
Player,
75+
Streaming,
7576
Subtitles,
7677
Discord,
7778
Trakt,
@@ -82,7 +83,8 @@ impl SettingsSection {
8283
match self {
8384
SettingsSection::Prowlarr => SettingsSection::Tmdb,
8485
SettingsSection::Tmdb => SettingsSection::Player,
85-
SettingsSection::Player => SettingsSection::Subtitles,
86+
SettingsSection::Player => SettingsSection::Streaming,
87+
SettingsSection::Streaming => SettingsSection::Subtitles,
8688
SettingsSection::Subtitles => SettingsSection::Discord,
8789
SettingsSection::Discord => SettingsSection::Trakt,
8890
SettingsSection::Trakt => SettingsSection::Prowlarr,
@@ -94,7 +96,8 @@ impl SettingsSection {
9496
SettingsSection::Prowlarr => SettingsSection::Trakt,
9597
SettingsSection::Tmdb => SettingsSection::Prowlarr,
9698
SettingsSection::Player => SettingsSection::Tmdb,
97-
SettingsSection::Subtitles => SettingsSection::Player,
99+
SettingsSection::Streaming => SettingsSection::Player,
100+
SettingsSection::Subtitles => SettingsSection::Streaming,
98101
SettingsSection::Discord => SettingsSection::Subtitles,
99102
SettingsSection::Trakt => SettingsSection::Discord,
100103
}
@@ -105,6 +108,7 @@ impl SettingsSection {
105108
SettingsSection::Prowlarr => "Prowlarr",
106109
SettingsSection::Tmdb => "TMDB",
107110
SettingsSection::Player => "Player",
111+
SettingsSection::Streaming => "Streaming",
108112
SettingsSection::Subtitles => "Subtitles",
109113
SettingsSection::Discord => "Discord",
110114
SettingsSection::Trakt => "Trakt",
@@ -117,6 +121,7 @@ impl SettingsSection {
117121
SettingsSection::Prowlarr => 2, // url, apikey
118122
SettingsSection::Tmdb => 1, // apikey
119123
SettingsSection::Player => 2, // command, args
124+
SettingsSection::Streaming => 1, // auto_race
120125
SettingsSection::Subtitles => 3, // enabled, language, api_key
121126
SettingsSection::Discord => 2, // enabled, app_id
122127
SettingsSection::Trakt => 3, // enabled, client_id, access_token
@@ -127,6 +132,7 @@ impl SettingsSection {
127132
SettingsSection::Prowlarr,
128133
SettingsSection::Tmdb,
129134
SettingsSection::Player,
135+
SettingsSection::Streaming,
130136
SettingsSection::Subtitles,
131137
SettingsSection::Discord,
132138
SettingsSection::Trakt,

0 commit comments

Comments
 (0)