Skip to content

Commit 8b1c492

Browse files
committed
Refactoring
1 parent 19a3c44 commit 8b1c492

File tree

12 files changed

+122
-35
lines changed

12 files changed

+122
-35
lines changed

CLAUDE.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,33 @@ Audio Stream Server is a FastAPI application that streams audio from YouTube vid
4545

4646
### Path Handling
4747

48-
**CRITICAL: Always use .expanduser().resolve() when creating Path objects from user-provided or config paths.**
48+
**CRITICAL: Always use path_utils helpers for handling file paths.**
4949

50-
- ALWAYS call `.expanduser().resolve()` on Path objects to handle `~` (home directory) expansion and resolve symbolic links
51-
- This is required for paths that come from:
50+
- Use `expand_path()` and `expand_path_str()` from `services.path_utils` for all user/config paths
51+
- `expand_path(path)` - Returns Path object with ~ expansion and symbolic link resolution
52+
- `expand_path_str(path)` - Returns string (for external commands like ffmpeg, yt-dlp)
53+
- This is required for paths from:
5254
- Configuration files (environment variables, .env)
5355
- User input
5456
- Function parameters that represent file paths
55-
- Temporary file paths (from `tempfile.mkstemp()`, etc.) do NOT need `.expanduser().resolve()`
57+
- Temporary file paths (from `tempfile.mkstemp()`, etc.) do NOT need expansion
5658
- Example:
5759
```python
60+
from services.path_utils import expand_path, expand_path_str
61+
5862
# CORRECT - user/config paths
59-
audio_path = Path(config.temp_audio_dir).expanduser().resolve()
60-
file_size = Path(audio_path_param).expanduser().resolve().stat().st_size
63+
audio_path = expand_path(config.temp_audio_dir)
64+
file_size = expand_path(audio_path_param).stat().st_size
65+
66+
# CORRECT - external commands
67+
subprocess.run(["ffmpeg", "-i", expand_path_str(audio_path), ...])
6168

6269
# CORRECT - temporary file paths (no expansion needed)
6370
temp_fd, temp_path = tempfile.mkstemp()
64-
temp_file = Path(temp_path) # No .expanduser().resolve() needed
71+
temp_file = Path(temp_path) # No expansion needed
72+
73+
# WRONG - manual expansion (use helper instead)
74+
audio_path = Path(config.temp_audio_dir).expanduser().resolve() # ❌ Use expand_path()
6575

6676
# WRONG - missing expansion for user path
6777
audio_path = Path(config.temp_audio_dir) # ❌ May fail if path contains ~

migrate_add_queue_columns.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99
import sys
1010
from pathlib import Path
1111

12+
# Add parent directory to path for imports
13+
14+
sys.path.insert(0, str(Path(__file__).parent))
15+
16+
from services.path_utils import expand_path
17+
1218

1319
def migrate_queue_table() -> None:
1420
"""Add type and week_year columns to queue table if they don't exist."""
1521
db_path_str = os.getenv("DATABASE_PATH", "./audio_history.db")
16-
db_path = Path(db_path_str).expanduser().resolve()
22+
db_path = expand_path(db_path_str)
1723

1824
print(f"Migrating database: {db_path}")
1925

routes/stream.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
"""
44

55
import logging
6-
from pathlib import Path
76
import threading
87
from fastapi import APIRouter, HTTPException
98
from fastapi.responses import FileResponse, JSONResponse
109
from pydantic import BaseModel
1110
from config import get_config
1211
from services.background_tasks import get_transcription_queue, TranscriptionJob
1312
from services.database import add_to_history, get_history, clear_history
13+
from services.path_utils import expand_path
1414
from services.youtube import get_video_metadata, extract_video_id
1515

1616
logger = logging.getLogger(__name__)
@@ -162,14 +162,14 @@ def _audio_is_ready(video_id: str) -> bool:
162162
"""Check if the audio file exists and is not still being downloaded."""
163163
from services.streaming import is_download_in_progress
164164

165-
audio_path = Path(config.get_audio_path(video_id)).expanduser().resolve()
165+
audio_path = expand_path(config.get_audio_path(video_id))
166166
return audio_path.exists() and not is_download_in_progress(video_id)
167167

168168

169169
@router.get("/audio/{video_id}")
170170
def get_audio_file(video_id: str):
171171
"""Serve the actual MP3 file for the player with mobile-optimized headers."""
172-
audio_path = Path(config.get_audio_path(video_id)).expanduser().resolve()
172+
audio_path = expand_path(config.get_audio_path(video_id))
173173

174174
if _audio_is_ready(video_id):
175175
file_size = audio_path.stat().st_size
@@ -198,7 +198,7 @@ def get_audio_file(video_id: str):
198198
@router.head("/audio/{video_id}")
199199
def check_audio_file(video_id: str):
200200
"""Check if audio file exists and is ready (for polling). HEAD request."""
201-
audio_path = Path(config.get_audio_path(video_id)).expanduser().resolve()
201+
audio_path = expand_path(config.get_audio_path(video_id))
202202

203203
if _audio_is_ready(video_id):
204204
file_size = audio_path.stat().st_size

routes/weekly_summaries.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""API routes for weekly summaries."""
22

33
import logging
4-
from pathlib import Path
54
from fastapi import APIRouter, HTTPException
65
from fastapi.responses import FileResponse
76
from typing import List, Dict
@@ -11,6 +10,7 @@
1110
get_summary_by_week_year,
1211
add_summary_to_queue,
1312
)
13+
from services.path_utils import expand_path
1414

1515
logger = logging.getLogger(__name__)
1616

@@ -63,7 +63,7 @@ async def stream_summary_audio(week_year: str):
6363
)
6464

6565
# Check if file exists
66-
if not Path(audio_path).expanduser().resolve().exists():
66+
if not expand_path(audio_path).exists():
6767
logger.error(f"Audio file not found: {audio_path}")
6868
raise HTTPException(
6969
status_code=404, detail=f"Audio file not found: {audio_path}"

services/background_tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Background task processing for audio transcription."""
22

33
import logging
4-
from pathlib import Path
54
import threading
65
import time
76
import random
@@ -12,6 +11,7 @@
1211
from typing import Optional, Dict
1312

1413
from services.cache import get_transcript_cache, get_audio_cache
14+
from services.path_utils import expand_path
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -413,7 +413,7 @@ def _wait_for_file(
413413
start_time = time.time()
414414

415415
while time.time() - start_time < timeout:
416-
path = Path(audio_path).expanduser().resolve()
416+
path = expand_path(audio_path)
417417
file_exists = path.exists()
418418
still_downloading = is_download_in_progress(video_id)
419419

services/cache.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from datetime import datetime
1010

1111
from config import get_config
12+
from services.path_utils import expand_path
1213

1314
logger = logging.getLogger(__name__)
1415

@@ -22,7 +23,7 @@ class TranscriptionCache:
2223
"""Manages caching of transcripts and summaries."""
2324

2425
def __init__(self, cache_dir: str = "/tmp/transcription-cache"):
25-
self.cache_dir = Path(cache_dir).expanduser().resolve()
26+
self.cache_dir = expand_path(cache_dir)
2627
self.cache_dir.mkdir(parents=True, exist_ok=True)
2728
self._lock = threading.Lock()
2829
logger.info(f"Transcription cache initialized at {self.cache_dir}")
@@ -106,7 +107,7 @@ class AudioCache:
106107
def __init__(self, max_files: int = 10):
107108
self.max_files = max_files
108109
config = get_config()
109-
self.audio_dir = Path(config.temp_audio_dir).expanduser().resolve()
110+
self.audio_dir = expand_path(config.temp_audio_dir)
110111
self.audio_dir.mkdir(parents=True, exist_ok=True)
111112
logger.info(
112113
f"Audio cache initialized: max {max_files} files in {self.audio_dir}"

services/path_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Path utilities for handling file paths with proper expansion.
3+
4+
Provides helper functions for expanding user paths (~) and resolving
5+
symbolic links consistently across the codebase.
6+
"""
7+
8+
from pathlib import Path
9+
from typing import Union
10+
11+
12+
def expand_path(path: Union[str, Path]) -> Path:
13+
"""
14+
Expand user path (~) and resolve to absolute path.
15+
16+
This function handles:
17+
- Tilde (~) expansion to home directory
18+
- Resolving symbolic links
19+
- Converting to absolute path
20+
21+
Args:
22+
path: Path as string or Path object
23+
24+
Returns:
25+
Fully expanded and resolved Path object
26+
27+
Example:
28+
>>> expand_path("~/documents/file.txt")
29+
PosixPath('/home/user/documents/file.txt')
30+
"""
31+
return Path(path).expanduser().resolve()
32+
33+
34+
def expand_path_str(path: Union[str, Path]) -> str:
35+
"""
36+
Expand user path (~) and resolve to absolute path string.
37+
38+
Same as expand_path() but returns a string instead of Path object.
39+
Useful for external commands (ffmpeg, yt-dlp) that don't handle ~ paths.
40+
41+
Args:
42+
path: Path as string or Path object
43+
44+
Returns:
45+
Fully expanded and resolved path as string
46+
47+
Example:
48+
>>> expand_path_str("~/documents/file.txt")
49+
'/home/user/documents/file.txt'
50+
"""
51+
return str(expand_path(path))

services/streaming.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import os
99
import subprocess
1010

11-
from pathlib import Path
1211
from services.cache import get_audio_cache
12+
from services.path_utils import expand_path
1313
from config import get_config
1414

1515
logger = logging.getLogger(__name__)
@@ -23,7 +23,7 @@ def _get_download_marker(youtube_video_id: str) -> str:
2323

2424
def is_download_in_progress(youtube_video_id: str) -> bool:
2525
"""Check if a download is currently in progress for this video."""
26-
return Path(_get_download_marker(youtube_video_id)).expanduser().resolve().exists()
26+
return expand_path(_get_download_marker(youtube_video_id)).exists()
2727

2828

2929
def start_youtube_download(youtube_video_id: str):
@@ -41,7 +41,7 @@ def start_youtube_download(youtube_video_id: str):
4141
proc or None if already cached
4242
"""
4343
audio_cache = get_audio_cache()
44-
audio_path = Path(config.get_audio_path(youtube_video_id)).expanduser().resolve()
44+
audio_path = expand_path(config.get_audio_path(youtube_video_id))
4545

4646
if audio_cache.check_file_exists(youtube_video_id):
4747
logger.info(f"Audio file for video {youtube_video_id} already exists in cache")
@@ -51,7 +51,7 @@ def start_youtube_download(youtube_video_id: str):
5151
url = f"https://www.youtube.com/watch?v={youtube_video_id}"
5252

5353
# Create marker file so the /audio endpoint won't serve a partial file
54-
marker_path = Path(_get_download_marker(youtube_video_id)).expanduser().resolve()
54+
marker_path = expand_path(_get_download_marker(youtube_video_id))
5555
try:
5656
marker_path.touch(exist_ok=True)
5757
except Exception as e:
@@ -115,11 +115,9 @@ def finish_youtube_download(youtube_video_id: str, returncode: int):
115115
youtube_video_id: YouTube video ID
116116
returncode: Process exit code (0 = success)
117117
"""
118-
audio_path = Path(config.get_audio_path(youtube_video_id)).expanduser().resolve()
119-
marker_path = Path(_get_download_marker(youtube_video_id)).expanduser().resolve()
120-
stderr_path = (
121-
Path(config.get_audio_path(youtube_video_id) + ".err").expanduser().resolve()
122-
)
118+
audio_path = expand_path(config.get_audio_path(youtube_video_id))
119+
marker_path = expand_path(_get_download_marker(youtube_video_id))
120+
stderr_path = expand_path(config.get_audio_path(youtube_video_id) + ".err")
123121

124122
# Read stderr for error reporting
125123
error_output = ""

services/transcription.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from config import get_config
1212
from services.api_clients import get_openai_client
13+
from services.path_utils import expand_path, expand_path_str
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -37,7 +38,7 @@ def compress_audio_for_whisper(audio_path: str) -> str:
3738
Exception: If compression fails
3839
"""
3940
# Expand ~ in path for ffmpeg (ffmpeg doesn't handle ~ paths)
40-
expanded_audio_path = str(Path(audio_path).expanduser().resolve())
41+
expanded_audio_path = expand_path_str(audio_path)
4142

4243
logger.info(
4344
f"Compressing audio file {expanded_audio_path} for Whisper (1.5x speed, saves API costs)"
@@ -135,7 +136,7 @@ def transcribe_audio_openai(audio_path: str, retries: int = 3) -> str:
135136
client = get_openai_client()
136137

137138
# Always compress audio to save costs and reduce upload time
138-
file_size = Path(audio_path).expanduser().resolve().stat().st_size
139+
file_size = expand_path(audio_path).stat().st_size
139140
logger.info(
140141
f"Audio file size: {file_size / 1024 / 1024:.2f}MB - "
141142
f"compressing for Whisper (saves 33% API costs)"
@@ -222,7 +223,7 @@ def transcribe_audio_gemini(audio_path: str, retries: int = 3) -> str:
222223
raise ValueError("Gemini API key not configured")
223224

224225
# Expand ~ in path (Python's open() doesn't handle ~ paths)
225-
expanded_audio_path = str(Path(audio_path).expanduser().resolve())
226+
expanded_audio_path = expand_path_str(audio_path)
226227

227228
last_error = None
228229

services/tts.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
"""
66

77
import re
8-
from pathlib import Path
98
from typing import Optional
109
from elevenlabs.client import ElevenLabs
1110
from bs4 import BeautifulSoup
1211

12+
from services.path_utils import expand_path
13+
1314

1415
class TTSError(Exception):
1516
"""Base exception for TTS-related errors."""
@@ -178,7 +179,7 @@ def save_audio_file(audio_data: bytes, file_path: str) -> int:
178179
Raises:
179180
IOError: If file cannot be written
180181
"""
181-
path = Path(file_path).expanduser().resolve()
182+
path = expand_path(file_path)
182183

183184
# Create parent directory if needed
184185
path.parent.mkdir(parents=True, exist_ok=True)
@@ -205,7 +206,7 @@ def get_audio_duration(file_path: str) -> Optional[int]:
205206
Returns:
206207
Duration in seconds, or None if file doesn't exist
207208
"""
208-
path = Path(file_path).expanduser().resolve()
209+
path = expand_path(file_path)
209210
if not path.exists():
210211
return None
211212

0 commit comments

Comments
 (0)