lushapp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ # LUSH - Network Bandwidth Monitor
2
+ import time
3
+
4
+ def get_net_bytes():
5
+ rx = 0
6
+ tx = 0
7
+ try:
8
+ with open('/proc/net/dev') as f:
9
+ for line in f.readlines()[2:]:
10
+ parts = line.split(':')
11
+ if len(parts) == 2:
12
+ iface = parts[0].strip()
13
+ if iface != 'lo':
14
+ cols = parts[1].split()
15
+ rx += int(cols[0])
16
+ tx += int(cols[8])
17
+ except Exception:
18
+ pass
19
+ return rx, tx
20
+
21
+ def format_speed(bps: float) -> str:
22
+ if bps < 1024:
23
+ return f"{bps:.0f} B/s"
24
+ elif bps < 1024 * 1024:
25
+ return f"{bps/1024:.1f} KB/s"
26
+ else:
27
+ return f"{bps/(1024*1024):.2f} MB/s"
@@ -0,0 +1,154 @@
1
+ # LUSH - Cross-Platform Desktop Track Notifications
2
+ import platform
3
+ import subprocess
4
+ import threading
5
+ import shutil
6
+ import time
7
+ import re
8
+ from typing import Optional
9
+
10
+ class NotificationManager:
11
+ """
12
+ Lightweight, thread-safe desktop notification manager supporting
13
+ Linux (notify-send), macOS (osascript), and Windows (PowerShell Toast).
14
+
15
+ Includes smart filtering to ignore soundscapes, ambient noise, station bumpers,
16
+ and duplicate metadata events.
17
+ """
18
+
19
+ def __init__(self, enabled: bool = True):
20
+ self.enabled = enabled
21
+ self._last_track = ""
22
+ self._last_time = 0.0
23
+ self._lock = threading.Lock()
24
+ self._system = platform.system()
25
+
26
+ def set_enabled(self, enabled: bool):
27
+ with self._lock:
28
+ self.enabled = enabled
29
+
30
+ def is_enabled(self) -> bool:
31
+ with self._lock:
32
+ return self.enabled
33
+
34
+ @staticmethod
35
+ def should_suppress(track_title: str, station_name: str, genre: str = "", category: str = "") -> bool:
36
+ """
37
+ Smart filter heuristic to determine if a notification should be suppressed.
38
+ Mutes soundscapes, ambient overlays, station identifiers, and promotional bumpers.
39
+ """
40
+ if not track_title or not track_title.strip():
41
+ return True
42
+
43
+ clean_track = track_title.strip().lower()
44
+ clean_station = station_name.strip().lower() if station_name else ""
45
+ clean_genre = genre.strip().lower() if genre else ""
46
+ clean_cat = category.strip().lower() if category else ""
47
+
48
+ # 1. Ignore pure soundscapes and ambient FX
49
+ if clean_cat in ("soundscape", "ambient", "fx", "nature"):
50
+ return True
51
+ if any(amb in clean_genre for amb in ["ambient", "soundscape", "rain", "spa", "drone", "whitenoise", "sleep"]):
52
+ return True
53
+
54
+ # 2. Ignore when track title is identical to station name (common on radio startup)
55
+ if clean_station and (clean_track == clean_station or clean_track in clean_station):
56
+ return True
57
+
58
+ # 3. Ignore common radio bumpers and commercial placeholders
59
+ bumper_phrases = [
60
+ "station will continue",
61
+ "this break",
62
+ "commercial break",
63
+ "after this",
64
+ "stream offline",
65
+ "connecting",
66
+ "buffering",
67
+ "continuous music",
68
+ "music mix",
69
+ "various artists"
70
+ ]
71
+ if any(phrase in clean_track for phrase in bumper_phrases):
72
+ return True
73
+
74
+ # 4. Ignore pure soundscape track names
75
+ soundscape_names = ["deep rain", "pink rain", "ocean waves", "campfire", "space drone", "soma drone"]
76
+ if any(s_name in clean_track for s_name in soundscape_names):
77
+ return True
78
+
79
+ return False
80
+
81
+ def send_track_notification(self, track_title: str, station_name: str, genre: str = "", category: str = ""):
82
+ """Dispatches a non-blocking desktop notification in a daemon thread."""
83
+ with self._lock:
84
+ if not self.enabled:
85
+ return
86
+
87
+ if self.should_suppress(track_title, station_name, genre, category):
88
+ return
89
+
90
+ now = time.time()
91
+ clean = track_title.strip()
92
+ # Debounce duplicate notifications within 20 seconds
93
+ if clean == self._last_track and (now - self._last_time < 20.0):
94
+ return
95
+
96
+ self._last_track = clean
97
+ self._last_time = now
98
+
99
+ threading.Thread(
100
+ target=self._dispatch,
101
+ args=(clean, station_name.strip() if station_name else "", genre.strip() if genre else ""),
102
+ daemon=True
103
+ ).start()
104
+
105
+ def _dispatch(self, track: str, station: str, genre: str):
106
+ """Worker thread to run native OS notification command with strict timeout."""
107
+ app_title = "LUSH ♫"
108
+ body = f"{track}"
109
+ if station:
110
+ body += f"\n[{station}]"
111
+
112
+ try:
113
+ if self._system == "Linux":
114
+ self._dispatch_linux(app_title, body)
115
+ elif self._system == "Darwin":
116
+ self._dispatch_macos(app_title, track, station)
117
+ elif self._system == "Windows":
118
+ self._dispatch_windows(app_title, body)
119
+ except Exception:
120
+ pass
121
+
122
+ @staticmethod
123
+ def _dispatch_linux(title: str, body: str):
124
+ if shutil.which("notify-send"):
125
+ subprocess.run(
126
+ ["notify-send", "-a", "LUSH", "-i", "audio-headphones", title, body],
127
+ capture_output=True,
128
+ timeout=2.0
129
+ )
130
+
131
+ @staticmethod
132
+ def _dispatch_macos(title: str, track: str, station: str):
133
+ subtitle = f"[{station}]" if station else "Now Playing"
134
+ # Escape quotes for AppleScript
135
+ safe_track = track.replace('"', '\\"')
136
+ safe_sub = subtitle.replace('"', '\\"')
137
+ script = f'display notification "{safe_track}" with title "{title}" subtitle "{safe_sub}"'
138
+ subprocess.run(["osascript", "-e", script], capture_output=True, timeout=2.0)
139
+
140
+ @staticmethod
141
+ def _dispatch_windows(title: str, body: str):
142
+ safe_title = title.replace('"', '`"')
143
+ safe_body = body.replace('"', '`"')
144
+ ps_script = f'''
145
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
146
+ $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
147
+ $textNodes = $template.GetElementsByTagName("text")
148
+ $textNodes.Item(0).AppendChild($template.CreateTextNode("{safe_title} - Now Playing")) > $null
149
+ $textNodes.Item(1).AppendChild($template.CreateTextNode("{safe_body}")) > $null
150
+ $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("LUSH")
151
+ $notification = [Windows.UI.Notifications.ToastNotification]::new($template)
152
+ $notifier.Show($notification)
153
+ '''
154
+ subprocess.run(["powershell", "-NoProfile", "-Command", ps_script], capture_output=True, timeout=3.0)
@@ -0,0 +1,185 @@
1
+ # LUSH - In-Memory Fuzzy Search & Relevance Ranking Engine
2
+ import re
3
+ import difflib
4
+ from typing import List, Tuple, Dict, Any
5
+
6
+ STOPWORDS = {
7
+ 'the', 'a', 'an', 'and', 'or', 'in', 'on', 'of', 'one', 'that', 'this',
8
+ 'to', 'for', 'with', 'by', 'at', 'from', 'is', 'it', 'station', 'radio'
9
+ }
10
+
11
+ def _extract_tokens(text: str) -> List[str]:
12
+ """
13
+ Extracts search tokens from text, handling whitespace, punctuation,
14
+ CamelCase word splitting, and compound 2-word combinations (e.g. 'Anne-Marie' -> 'annemarie').
15
+ """
16
+ raw_words = re.findall(r'[a-zA-Z0-9]+', text)
17
+ tokens = set()
18
+ for w in raw_words:
19
+ tokens.add(w.lower())
20
+ camel_parts = re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\W|$)|\d+', w)
21
+ if len(camel_parts) > 1:
22
+ for cp in camel_parts:
23
+ tokens.add(cp.lower())
24
+
25
+ for i in range(len(raw_words) - 1):
26
+ combo = (raw_words[i] + raw_words[i+1]).lower()
27
+ if len(combo) <= 20:
28
+ tokens.add(combo)
29
+
30
+ return list(tokens)
31
+
32
+
33
+ def _token_similarity(q_tok: str, cand_tok: str) -> float:
34
+ """
35
+ Computes token-level similarity between query word and candidate word.
36
+ Handles exact, prefix, substring, SequenceMatcher ratio, sub-sequence acronyms,
37
+ and conversational synonyms (e.g. 'sister' <-> 'sis').
38
+ """
39
+ if not q_tok or not cand_tok:
40
+ return 0.0
41
+ if q_tok == cand_tok:
42
+ return 1.0
43
+ if cand_tok.startswith(q_tok):
44
+ return 0.95
45
+ if q_tok in cand_tok:
46
+ return 0.88
47
+ if len(cand_tok) >= 3 and cand_tok in q_tok:
48
+ return 0.50 + 0.35 * (len(cand_tok) / len(q_tok))
49
+
50
+ # Common colloquial abbreviations
51
+ if (q_tok == 'sister' and cand_tok == 'sis') or (q_tok == 'sis' and cand_tok == 'sister'):
52
+ return 0.95
53
+
54
+ r = difflib.SequenceMatcher(None, q_tok, cand_tok).ratio()
55
+
56
+ # If first letter differs on short words, apply typo penalty
57
+ if q_tok[0] != cand_tok[0]:
58
+ if len(q_tok) <= 5 or len(cand_tok) <= 5:
59
+ r = r * 0.75
60
+
61
+ # Subsequence ratio within token (e.g. 'swft' in 'swift', 'sntra' in 'sinatra')
62
+ if len(q_tok) >= 3 and len(cand_tok) >= len(q_tok):
63
+ sub_idx = 0
64
+ for ch in cand_tok:
65
+ if ch == q_tok[sub_idx]:
66
+ sub_idx += 1
67
+ if sub_idx == len(q_tok):
68
+ sub_r = len(q_tok) / len(cand_tok)
69
+ if q_tok[0] == cand_tok[0]:
70
+ return max(r, 0.75 + 0.25 * sub_r)
71
+ else:
72
+ return max(r, 0.60 + 0.20 * sub_r)
73
+ return r
74
+
75
+
76
+ def score_station_match(query: str, station: Dict[str, Any]) -> float:
77
+ """
78
+ Computes an in-memory fuzzy relevance score (0.0 to 100.0) between a user query
79
+ and a station's metadata (name, genre, category, description).
80
+ """
81
+ if not query:
82
+ return 1.0
83
+
84
+ q = query.strip().lower()
85
+ if not q:
86
+ return 1.0
87
+
88
+ name = station.get("name", "").lower()
89
+ genre = station.get("genre", "").lower()
90
+ cat = station.get("category", "").lower()
91
+ full_text = f"{name} {genre} {cat}"
92
+
93
+ # 1. Exact string match
94
+ if q == name:
95
+ return 100.0
96
+
97
+ # 2. Exact prefix match on name
98
+ if name.startswith(q):
99
+ return 95.0 - (len(name) - len(q)) * 0.02
100
+
101
+ # 3. Whole query substring in name
102
+ if q in name:
103
+ return 90.0
104
+
105
+ # 4. Token-to-Token Multi-Word Alignment
106
+ q_all_words = [w.lower() for w in re.findall(r'[a-zA-Z0-9]+', q)]
107
+ q_words = [w for w in q_all_words if w not in STOPWORDS]
108
+ if not q_words:
109
+ q_words = q_all_words
110
+
111
+ name_tokens = _extract_tokens(station.get("name", ""))
112
+ full_tokens = _extract_tokens(full_text)
113
+
114
+ if q_words:
115
+ # Match against name tokens
116
+ token_scores = []
117
+ for qw in q_words:
118
+ best_t = 0.0
119
+ for nt in name_tokens:
120
+ s = _token_similarity(qw, nt)
121
+ if s > best_t:
122
+ best_t = s
123
+ token_scores.append(best_t)
124
+
125
+ avg_score = sum(token_scores) / len(token_scores)
126
+ min_score = min(token_scores)
127
+
128
+ if min_score >= 0.55 or (len(q_words) > 1 and avg_score >= 0.70 and min_score >= 0.40):
129
+ return 60.0 + avg_score * 30.0
130
+
131
+ # Match against full metadata (name + genre + category)
132
+ full_token_scores = []
133
+ for qw in q_words:
134
+ best_ft = 0.0
135
+ for ft in full_tokens:
136
+ s = _token_similarity(qw, ft)
137
+ if s > best_ft:
138
+ best_ft = s
139
+ full_token_scores.append(best_ft)
140
+
141
+ avg_full = sum(full_token_scores) / len(full_token_scores)
142
+ min_full = min(full_token_scores)
143
+ if min_full >= 0.55 or (len(q_words) > 1 and avg_full >= 0.70 and min_full >= 0.40):
144
+ return 45.0 + avg_full * 25.0
145
+
146
+ # 5. Full-string character stream alignment (handles squished names)
147
+ clean_q = "".join(q_words)
148
+ clean_name = "".join(name_tokens)
149
+ if clean_q and clean_name:
150
+ if clean_q == clean_name:
151
+ return 93.0
152
+ if clean_name.startswith(clean_q):
153
+ return 88.0
154
+ if clean_q in clean_name:
155
+ return 83.0
156
+ full_r = difflib.SequenceMatcher(None, clean_q, clean_name).ratio()
157
+ if full_r >= 0.65:
158
+ return 50.0 + full_r * 30.0
159
+
160
+ # 6. Acronym / Initialism
161
+ if len(q_words) == 1 and len(q) >= 2:
162
+ initials = "".join(w[0] for w in name_tokens if w)
163
+ if q == initials or q in initials:
164
+ return 75.0
165
+
166
+ return 0.0
167
+
168
+
169
+ def fuzzy_filter_stations(indexed_stations: List[Tuple[int, Dict[str, Any]]], query: str) -> List[Tuple[int, Dict[str, Any]]]:
170
+ """
171
+ Filters and sorts a list of (global_index, station_dict) tuples using
172
+ in-memory fuzzy scoring. Preserves original order if query is empty.
173
+ """
174
+ if not query or not query.strip():
175
+ return indexed_stations
176
+
177
+ scored_items = []
178
+ for orig_idx, st in indexed_stations:
179
+ score = score_station_match(query, st)
180
+ if score > 0.0:
181
+ scored_items.append((score, orig_idx, st))
182
+
183
+ # Sort primarily by score (descending), secondarily by original order
184
+ scored_items.sort(key=lambda item: (-item[0], item[1]))
185
+ return [(orig_idx, st) for _, orig_idx, st in scored_items]