wrkmon 1.0.0__py3-none-any.whl
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.
- wrkmon/__init__.py +4 -0
- wrkmon/__main__.py +6 -0
- wrkmon/app.py +568 -0
- wrkmon/cli.py +289 -0
- wrkmon/core/__init__.py +8 -0
- wrkmon/core/cache.py +208 -0
- wrkmon/core/player.py +301 -0
- wrkmon/core/queue.py +264 -0
- wrkmon/core/youtube.py +178 -0
- wrkmon/data/__init__.py +6 -0
- wrkmon/data/database.py +426 -0
- wrkmon/data/migrations.py +134 -0
- wrkmon/data/models.py +144 -0
- wrkmon/ui/__init__.py +5 -0
- wrkmon/ui/components.py +211 -0
- wrkmon/ui/messages.py +89 -0
- wrkmon/ui/screens/__init__.py +8 -0
- wrkmon/ui/screens/history.py +142 -0
- wrkmon/ui/screens/player.py +222 -0
- wrkmon/ui/screens/playlist.py +278 -0
- wrkmon/ui/screens/search.py +165 -0
- wrkmon/ui/theme.py +326 -0
- wrkmon/ui/views/__init__.py +8 -0
- wrkmon/ui/views/history.py +138 -0
- wrkmon/ui/views/playlists.py +259 -0
- wrkmon/ui/views/queue.py +191 -0
- wrkmon/ui/views/search.py +150 -0
- wrkmon/ui/widgets/__init__.py +7 -0
- wrkmon/ui/widgets/header.py +59 -0
- wrkmon/ui/widgets/player_bar.py +115 -0
- wrkmon/ui/widgets/result_item.py +98 -0
- wrkmon/utils/__init__.py +6 -0
- wrkmon/utils/config.py +172 -0
- wrkmon/utils/mpv_installer.py +190 -0
- wrkmon/utils/stealth.py +124 -0
- wrkmon-1.0.0.dist-info/METADATA +193 -0
- wrkmon-1.0.0.dist-info/RECORD +41 -0
- wrkmon-1.0.0.dist-info/WHEEL +5 -0
- wrkmon-1.0.0.dist-info/entry_points.txt +2 -0
- wrkmon-1.0.0.dist-info/licenses/LICENSE.txt +21 -0
- wrkmon-1.0.0.dist-info/top_level.txt +1 -0
wrkmon/cli.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""CLI entry point for wrkmon using Typer."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from wrkmon import __version__
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="wrkmon",
|
|
14
|
+
help="Work Monitor - A developer productivity tool",
|
|
15
|
+
no_args_is_help=False,
|
|
16
|
+
)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def version_callback(value: bool) -> None:
|
|
21
|
+
"""Show version and exit."""
|
|
22
|
+
if value:
|
|
23
|
+
console.print(f"wrkmon version {__version__}")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback(invoke_without_command=True)
|
|
28
|
+
def main(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
version: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--version",
|
|
33
|
+
"-v",
|
|
34
|
+
callback=version_callback,
|
|
35
|
+
is_eager=True,
|
|
36
|
+
help="Show version and exit",
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Work Monitor - Monitor and manage system processes.
|
|
41
|
+
|
|
42
|
+
Run without arguments to launch the interactive monitor.
|
|
43
|
+
"""
|
|
44
|
+
if ctx.invoked_subcommand is None:
|
|
45
|
+
# Launch TUI
|
|
46
|
+
from wrkmon.app import run_app
|
|
47
|
+
run_app()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def search(
|
|
52
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
53
|
+
limit: int = typer.Option(10, "--limit", "-l", help="Maximum results"),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Search for processes to monitor."""
|
|
56
|
+
from wrkmon.core.youtube import YouTubeClient
|
|
57
|
+
from wrkmon.utils.stealth import get_stealth
|
|
58
|
+
|
|
59
|
+
stealth = get_stealth()
|
|
60
|
+
|
|
61
|
+
async def do_search():
|
|
62
|
+
client = YouTubeClient()
|
|
63
|
+
results = await client.search(query, max_results=limit)
|
|
64
|
+
return results
|
|
65
|
+
|
|
66
|
+
console.print(f"[dim]Searching for: {query}[/dim]\n")
|
|
67
|
+
|
|
68
|
+
results = asyncio.run(do_search())
|
|
69
|
+
|
|
70
|
+
if not results:
|
|
71
|
+
console.print("[yellow]No results found[/yellow]")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
table = Table(title="Search Results")
|
|
75
|
+
table.add_column("#", style="dim")
|
|
76
|
+
table.add_column("Process Name")
|
|
77
|
+
table.add_column("PID", style="dim")
|
|
78
|
+
table.add_column("Duration")
|
|
79
|
+
table.add_column("Status", style="green")
|
|
80
|
+
|
|
81
|
+
for i, result in enumerate(results, 1):
|
|
82
|
+
process_name = stealth.get_fake_process_name(result.title)
|
|
83
|
+
fake_pid = str(stealth.get_fake_pid())
|
|
84
|
+
duration = result.duration_str
|
|
85
|
+
table.add_row(str(i), process_name[:50], fake_pid, duration, "READY")
|
|
86
|
+
|
|
87
|
+
console.print(table)
|
|
88
|
+
console.print(f"\n[dim]Use 'wrkmon play <id>' to start a process[/dim]")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def play(
|
|
93
|
+
video_id: str = typer.Argument(..., help="Video ID or URL to play"),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Play a specific process by ID or URL."""
|
|
96
|
+
import re
|
|
97
|
+
|
|
98
|
+
# Extract video ID from URL if needed
|
|
99
|
+
if "youtube.com" in video_id or "youtu.be" in video_id:
|
|
100
|
+
# Extract ID from URL
|
|
101
|
+
patterns = [
|
|
102
|
+
r"(?:v=|/)([a-zA-Z0-9_-]{11})",
|
|
103
|
+
r"youtu\.be/([a-zA-Z0-9_-]{11})",
|
|
104
|
+
]
|
|
105
|
+
for pattern in patterns:
|
|
106
|
+
match = re.search(pattern, video_id)
|
|
107
|
+
if match:
|
|
108
|
+
video_id = match.group(1)
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
from wrkmon.core.youtube import YouTubeClient
|
|
112
|
+
from wrkmon.core.player import AudioPlayer
|
|
113
|
+
from wrkmon.core.cache import Cache
|
|
114
|
+
from wrkmon.utils.stealth import get_stealth
|
|
115
|
+
|
|
116
|
+
stealth = get_stealth()
|
|
117
|
+
cache = Cache()
|
|
118
|
+
|
|
119
|
+
async def do_play():
|
|
120
|
+
client = YouTubeClient()
|
|
121
|
+
player = AudioPlayer()
|
|
122
|
+
|
|
123
|
+
# Get stream info
|
|
124
|
+
console.print("[dim]Getting process info...[/dim]")
|
|
125
|
+
|
|
126
|
+
# Check cache
|
|
127
|
+
cached = cache.get(video_id)
|
|
128
|
+
if cached:
|
|
129
|
+
audio_url = cached.audio_url
|
|
130
|
+
title = cached.title
|
|
131
|
+
else:
|
|
132
|
+
stream_info = await client.get_stream_url(video_id)
|
|
133
|
+
if not stream_info:
|
|
134
|
+
console.print("[red]Failed to get process info[/red]")
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
audio_url = stream_info.audio_url
|
|
138
|
+
title = stream_info.title
|
|
139
|
+
|
|
140
|
+
# Cache it
|
|
141
|
+
cache.set(
|
|
142
|
+
video_id=video_id,
|
|
143
|
+
title=stream_info.title,
|
|
144
|
+
channel=stream_info.channel,
|
|
145
|
+
duration=stream_info.duration,
|
|
146
|
+
audio_url=audio_url,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Start player
|
|
150
|
+
console.print("[dim]Starting process...[/dim]")
|
|
151
|
+
if not await player.start():
|
|
152
|
+
console.print("[red]Failed to start player[/red]")
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
# Play
|
|
156
|
+
if await player.play(audio_url):
|
|
157
|
+
process_name = stealth.get_fake_process_name(title)
|
|
158
|
+
console.print(f"[green]Running:[/green] {process_name}")
|
|
159
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
160
|
+
|
|
161
|
+
# Keep running until interrupted
|
|
162
|
+
try:
|
|
163
|
+
while True:
|
|
164
|
+
await asyncio.sleep(1)
|
|
165
|
+
except KeyboardInterrupt:
|
|
166
|
+
console.print("\n[yellow]Stopping process...[/yellow]")
|
|
167
|
+
finally:
|
|
168
|
+
await player.shutdown()
|
|
169
|
+
|
|
170
|
+
return True
|
|
171
|
+
else:
|
|
172
|
+
console.print("[red]Failed to start process[/red]")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
asyncio.run(do_play())
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def queue() -> None:
|
|
180
|
+
"""Show the current process queue."""
|
|
181
|
+
# For CLI, we show an empty queue message since queue is session-based
|
|
182
|
+
console.print("[dim]No processes in queue[/dim]")
|
|
183
|
+
console.print("[dim]Run 'wrkmon' to use the interactive monitor[/dim]")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
def history(
|
|
188
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Maximum entries to show"),
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Show process history."""
|
|
191
|
+
from wrkmon.data.database import Database
|
|
192
|
+
from wrkmon.utils.stealth import get_stealth
|
|
193
|
+
|
|
194
|
+
stealth = get_stealth()
|
|
195
|
+
db = Database()
|
|
196
|
+
|
|
197
|
+
entries = db.get_history(limit=limit)
|
|
198
|
+
|
|
199
|
+
if not entries:
|
|
200
|
+
console.print("[dim]No history yet[/dim]")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
table = Table(title="Process History")
|
|
204
|
+
table.add_column("#", style="dim")
|
|
205
|
+
table.add_column("Process Name")
|
|
206
|
+
table.add_column("Duration")
|
|
207
|
+
table.add_column("Runs", style="cyan")
|
|
208
|
+
table.add_column("Last Run", style="dim")
|
|
209
|
+
|
|
210
|
+
for i, entry in enumerate(entries, 1):
|
|
211
|
+
process_name = stealth.get_fake_process_name(entry.track.title)
|
|
212
|
+
duration = entry.track.duration_str
|
|
213
|
+
runs = str(entry.play_count)
|
|
214
|
+
last_run = entry.played_at.strftime("%Y-%m-%d %H:%M")
|
|
215
|
+
table.add_row(str(i), process_name[:40], duration, runs, last_run)
|
|
216
|
+
|
|
217
|
+
console.print(table)
|
|
218
|
+
db.close()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command()
|
|
222
|
+
def playlists() -> None:
|
|
223
|
+
"""List all playlists."""
|
|
224
|
+
from wrkmon.data.database import Database
|
|
225
|
+
|
|
226
|
+
db = Database()
|
|
227
|
+
playlists = db.get_all_playlists()
|
|
228
|
+
|
|
229
|
+
if not playlists:
|
|
230
|
+
console.print("[dim]No playlists yet[/dim]")
|
|
231
|
+
console.print("[dim]Run 'wrkmon' to create playlists[/dim]")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
table = Table(title="Playlists")
|
|
235
|
+
table.add_column("#", style="dim")
|
|
236
|
+
table.add_column("Name")
|
|
237
|
+
table.add_column("Tracks", style="cyan")
|
|
238
|
+
table.add_column("Created", style="dim")
|
|
239
|
+
|
|
240
|
+
for i, playlist in enumerate(playlists, 1):
|
|
241
|
+
created = playlist.created_at.strftime("%Y-%m-%d") if playlist.created_at else "N/A"
|
|
242
|
+
table.add_row(str(i), playlist.name, str(playlist.track_count), created)
|
|
243
|
+
|
|
244
|
+
console.print(table)
|
|
245
|
+
db.close()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.command()
|
|
249
|
+
def clear_cache() -> None:
|
|
250
|
+
"""Clear the URL cache."""
|
|
251
|
+
from wrkmon.core.cache import Cache
|
|
252
|
+
|
|
253
|
+
cache = Cache()
|
|
254
|
+
stats = cache.get_stats()
|
|
255
|
+
count = cache.clear()
|
|
256
|
+
|
|
257
|
+
console.print(f"[green]Cleared {count} cached entries[/green]")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@app.command()
|
|
261
|
+
def clear_history() -> None:
|
|
262
|
+
"""Clear play history."""
|
|
263
|
+
from wrkmon.data.database import Database
|
|
264
|
+
|
|
265
|
+
db = Database()
|
|
266
|
+
count = db.clear_history()
|
|
267
|
+
db.close()
|
|
268
|
+
|
|
269
|
+
console.print(f"[green]Cleared {count} history entries[/green]")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command()
|
|
273
|
+
def config() -> None:
|
|
274
|
+
"""Show configuration info."""
|
|
275
|
+
from wrkmon.utils.config import get_config
|
|
276
|
+
|
|
277
|
+
cfg = get_config()
|
|
278
|
+
|
|
279
|
+
console.print("[bold]Configuration[/bold]\n")
|
|
280
|
+
console.print(f"Config directory: {cfg.config_dir}")
|
|
281
|
+
console.print(f"Data directory: {cfg.data_dir}")
|
|
282
|
+
console.print(f"Database: {cfg.database_path}")
|
|
283
|
+
console.print(f"Cache: {cfg.cache_path}")
|
|
284
|
+
console.print(f"\nVolume: {cfg.volume}%")
|
|
285
|
+
console.print(f"Cache TTL: {cfg.url_ttl_hours} hours")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
app()
|
wrkmon/core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Core functionality for wrkmon."""
|
|
2
|
+
|
|
3
|
+
from wrkmon.core.youtube import YouTubeClient
|
|
4
|
+
from wrkmon.core.player import AudioPlayer
|
|
5
|
+
from wrkmon.core.queue import PlayQueue
|
|
6
|
+
from wrkmon.core.cache import Cache
|
|
7
|
+
|
|
8
|
+
__all__ = ["YouTubeClient", "AudioPlayer", "PlayQueue", "Cache"]
|
wrkmon/core/cache.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Audio URL and metadata caching."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from wrkmon.utils.config import get_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CachedStream:
|
|
15
|
+
"""Cached stream information."""
|
|
16
|
+
|
|
17
|
+
video_id: str
|
|
18
|
+
title: str
|
|
19
|
+
channel: str
|
|
20
|
+
duration: int
|
|
21
|
+
audio_url: str
|
|
22
|
+
thumbnail_url: Optional[str]
|
|
23
|
+
cached_at: float
|
|
24
|
+
expires_at: float
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_expired(self) -> bool:
|
|
28
|
+
"""Check if cache entry is expired."""
|
|
29
|
+
return time.time() > self.expires_at
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Cache:
|
|
33
|
+
"""Manages caching of audio URLs and metadata."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, db_path: Optional[Path] = None):
|
|
36
|
+
config = get_config()
|
|
37
|
+
self._db_path = db_path or config.cache_path
|
|
38
|
+
self._ttl_hours = config.url_ttl_hours
|
|
39
|
+
self._max_entries = config.get("cache", "max_entries", 1000)
|
|
40
|
+
self._init_db()
|
|
41
|
+
|
|
42
|
+
def _init_db(self) -> None:
|
|
43
|
+
"""Initialize the cache database."""
|
|
44
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
47
|
+
conn.execute("""
|
|
48
|
+
CREATE TABLE IF NOT EXISTS stream_cache (
|
|
49
|
+
video_id TEXT PRIMARY KEY,
|
|
50
|
+
title TEXT NOT NULL,
|
|
51
|
+
channel TEXT NOT NULL,
|
|
52
|
+
duration INTEGER NOT NULL,
|
|
53
|
+
audio_url TEXT NOT NULL,
|
|
54
|
+
thumbnail_url TEXT,
|
|
55
|
+
cached_at REAL NOT NULL,
|
|
56
|
+
expires_at REAL NOT NULL
|
|
57
|
+
)
|
|
58
|
+
""")
|
|
59
|
+
conn.execute("""
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_expires_at ON stream_cache(expires_at)
|
|
61
|
+
""")
|
|
62
|
+
conn.commit()
|
|
63
|
+
|
|
64
|
+
def get(self, video_id: str) -> Optional[CachedStream]:
|
|
65
|
+
"""Get a cached stream by video ID."""
|
|
66
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
67
|
+
conn.row_factory = sqlite3.Row
|
|
68
|
+
cursor = conn.execute(
|
|
69
|
+
"""
|
|
70
|
+
SELECT * FROM stream_cache
|
|
71
|
+
WHERE video_id = ? AND expires_at > ?
|
|
72
|
+
""",
|
|
73
|
+
(video_id, time.time()),
|
|
74
|
+
)
|
|
75
|
+
row = cursor.fetchone()
|
|
76
|
+
|
|
77
|
+
if row:
|
|
78
|
+
return CachedStream(
|
|
79
|
+
video_id=row["video_id"],
|
|
80
|
+
title=row["title"],
|
|
81
|
+
channel=row["channel"],
|
|
82
|
+
duration=row["duration"],
|
|
83
|
+
audio_url=row["audio_url"],
|
|
84
|
+
thumbnail_url=row["thumbnail_url"],
|
|
85
|
+
cached_at=row["cached_at"],
|
|
86
|
+
expires_at=row["expires_at"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def set(
|
|
92
|
+
self,
|
|
93
|
+
video_id: str,
|
|
94
|
+
title: str,
|
|
95
|
+
channel: str,
|
|
96
|
+
duration: int,
|
|
97
|
+
audio_url: str,
|
|
98
|
+
thumbnail_url: Optional[str] = None,
|
|
99
|
+
) -> CachedStream:
|
|
100
|
+
"""Cache a stream URL."""
|
|
101
|
+
now = time.time()
|
|
102
|
+
expires_at = now + (self._ttl_hours * 3600)
|
|
103
|
+
|
|
104
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
105
|
+
conn.execute(
|
|
106
|
+
"""
|
|
107
|
+
INSERT OR REPLACE INTO stream_cache
|
|
108
|
+
(video_id, title, channel, duration, audio_url, thumbnail_url, cached_at, expires_at)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
110
|
+
""",
|
|
111
|
+
(video_id, title, channel, duration, audio_url, thumbnail_url, now, expires_at),
|
|
112
|
+
)
|
|
113
|
+
conn.commit()
|
|
114
|
+
|
|
115
|
+
# Cleanup old entries if we're over the limit
|
|
116
|
+
self._cleanup()
|
|
117
|
+
|
|
118
|
+
return CachedStream(
|
|
119
|
+
video_id=video_id,
|
|
120
|
+
title=title,
|
|
121
|
+
channel=channel,
|
|
122
|
+
duration=duration,
|
|
123
|
+
audio_url=audio_url,
|
|
124
|
+
thumbnail_url=thumbnail_url,
|
|
125
|
+
cached_at=now,
|
|
126
|
+
expires_at=expires_at,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def delete(self, video_id: str) -> bool:
|
|
130
|
+
"""Delete a cache entry."""
|
|
131
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
132
|
+
cursor = conn.execute(
|
|
133
|
+
"DELETE FROM stream_cache WHERE video_id = ?",
|
|
134
|
+
(video_id,),
|
|
135
|
+
)
|
|
136
|
+
conn.commit()
|
|
137
|
+
return cursor.rowcount > 0
|
|
138
|
+
|
|
139
|
+
def clear(self) -> int:
|
|
140
|
+
"""Clear all cache entries. Returns count deleted."""
|
|
141
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
142
|
+
cursor = conn.execute("DELETE FROM stream_cache")
|
|
143
|
+
conn.commit()
|
|
144
|
+
return cursor.rowcount
|
|
145
|
+
|
|
146
|
+
def clear_expired(self) -> int:
|
|
147
|
+
"""Clear expired cache entries. Returns count deleted."""
|
|
148
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
149
|
+
cursor = conn.execute(
|
|
150
|
+
"DELETE FROM stream_cache WHERE expires_at <= ?",
|
|
151
|
+
(time.time(),),
|
|
152
|
+
)
|
|
153
|
+
conn.commit()
|
|
154
|
+
return cursor.rowcount
|
|
155
|
+
|
|
156
|
+
def _cleanup(self) -> None:
|
|
157
|
+
"""Cleanup old entries if over the limit."""
|
|
158
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
159
|
+
# First clear expired
|
|
160
|
+
conn.execute(
|
|
161
|
+
"DELETE FROM stream_cache WHERE expires_at <= ?",
|
|
162
|
+
(time.time(),),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Then check count
|
|
166
|
+
cursor = conn.execute("SELECT COUNT(*) FROM stream_cache")
|
|
167
|
+
count = cursor.fetchone()[0]
|
|
168
|
+
|
|
169
|
+
if count > self._max_entries:
|
|
170
|
+
# Delete oldest entries
|
|
171
|
+
excess = count - self._max_entries
|
|
172
|
+
conn.execute(
|
|
173
|
+
"""
|
|
174
|
+
DELETE FROM stream_cache
|
|
175
|
+
WHERE video_id IN (
|
|
176
|
+
SELECT video_id FROM stream_cache
|
|
177
|
+
ORDER BY cached_at ASC
|
|
178
|
+
LIMIT ?
|
|
179
|
+
)
|
|
180
|
+
""",
|
|
181
|
+
(excess,),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
conn.commit()
|
|
185
|
+
|
|
186
|
+
def get_stats(self) -> dict:
|
|
187
|
+
"""Get cache statistics."""
|
|
188
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
189
|
+
cursor = conn.execute("SELECT COUNT(*) FROM stream_cache")
|
|
190
|
+
total = cursor.fetchone()[0]
|
|
191
|
+
|
|
192
|
+
cursor = conn.execute(
|
|
193
|
+
"SELECT COUNT(*) FROM stream_cache WHERE expires_at > ?",
|
|
194
|
+
(time.time(),),
|
|
195
|
+
)
|
|
196
|
+
valid = cursor.fetchone()[0]
|
|
197
|
+
|
|
198
|
+
cursor = conn.execute(
|
|
199
|
+
"SELECT SUM(LENGTH(audio_url)) FROM stream_cache"
|
|
200
|
+
)
|
|
201
|
+
size = cursor.fetchone()[0] or 0
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"total_entries": total,
|
|
205
|
+
"valid_entries": valid,
|
|
206
|
+
"expired_entries": total - valid,
|
|
207
|
+
"approximate_size_bytes": size,
|
|
208
|
+
}
|