Skip to content

Commit 309fa5f

Browse files
author
Sam Storer
committed
feat: add optional filename_format parameter to download_videos()
- Add _format_filename_default() method to encapsulate default filename formatting - Add filename_format parameter to download_videos() allowing custom filename generation - Update _parse_downloaded_items() to accept and use custom format function - Maintain full backward compatibility (defaults to existing slugified format) - Add comprehensive documentation and examples - Add test cases for custom format functions Usage example: def custom_fmt(created_at, camera_name, path): dt = datetime.datetime.fromisoformat(created_at).astimezone( pytz.timezone('US/Eastern') ) clean_name = camera_name.replace(' ', '') return os.path.join(path, f'{dt:%Y%m%d_%H%M%S}_{clean_name}.mp4') await blink.download_videos(path, filename_format=custom_fmt)
1 parent 6ef1b53 commit 309fa5f

File tree

4 files changed

+488
-7
lines changed

4 files changed

+488
-7
lines changed

FILENAME_FORMAT_EXAMPLE.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Custom Video Filename Formatting
2+
3+
The `download_videos()` method now supports an optional `filename_format` parameter to customize how video filenames are generated.
4+
5+
## Default Behavior (No Change)
6+
7+
If you don't provide `filename_format`, videos are named using the default slugified format:
8+
9+
```python
10+
await blink.download_videos(path="/tmp/videos")
11+
# Generates: /tmp/videos/front-door-2024-01-15t143022z.mp4
12+
```
13+
14+
## Custom Format Function
15+
16+
Pass a callable that accepts `(created_at, camera_name, path)` and returns the full filepath:
17+
18+
```python
19+
import datetime
20+
import os
21+
import pytz
22+
23+
def custom_format(created_at, camera_name, path):
24+
"""Format: YYYYMMDD_HHMMSS_CameraName.mp4"""
25+
dt = datetime.datetime.fromisoformat(created_at).astimezone(
26+
pytz.timezone('US/Eastern')
27+
)
28+
clean_camera = camera_name.replace(' ', '')
29+
filename = f"{dt:%Y%m%d_%H%M%S}_{clean_camera}.mp4"
30+
return os.path.join(path, filename)
31+
32+
# Use it
33+
await blink.download_videos(
34+
path="/tmp/videos",
35+
filename_format=custom_format
36+
)
37+
# Generates: /tmp/videos/20240115_143022_FrontDoor.mp4
38+
```
39+
40+
## Another Example: ISO + Camera Name
41+
42+
```python
43+
def iso_format(created_at, camera_name, path):
44+
"""Format: YYYY-MM-DD_HH-MM-SS_CameraName.mp4"""
45+
dt = datetime.datetime.fromisoformat(created_at)
46+
clean_camera = camera_name.replace(' ', '_')
47+
filename = f"{dt:%Y-%m-%d_%H-%M-%S}_{clean_camera}.mp4"
48+
return os.path.join(path, filename)
49+
50+
await blink.download_videos(
51+
path="/tmp/videos",
52+
camera="Front Door",
53+
filename_format=iso_format
54+
)
55+
# Generates: /tmp/videos/2024-01-15_14-30-22_Front_Door.mp4
56+
```
57+
58+
## Notes
59+
60+
- The `created_at` parameter is a string in ISO 8601 format (e.g., `"2024-01-15T14:30:22Z"`)
61+
- Your callable is responsible for returning the **full path** including the filename
62+
- The `.mp4` extension is your responsibility (it's not added automatically)
63+
- All other filtering (camera name, deleted flag, etc.) still applies
64+
- The format function is called once per video, so keep it efficient

IMPLEMENTATION_SUMMARY.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Implementation Summary: Custom Video Filename Formatting
2+
3+
## Overview
4+
5+
Added optional `filename_format` parameter to `download_videos()` method, allowing customization of how downloaded video filenames are generated.
6+
7+
**Date:** 2026-04-05
8+
**Changes Made:** 2 files
9+
**Backward Compatibility:** ✅ Full (defaults to existing behavior)
10+
11+
---
12+
13+
## Changes
14+
15+
### 1. **blinkpy/blinkpy.py**
16+
17+
#### New Method: `_format_filename_default()`
18+
- Extracted default filename formatting logic into a reusable method
19+
- Signature: `_format_filename_default(self, created_at, camera_name, path) -> str`
20+
- Returns the full filepath with `.mp4` extension
21+
- Preserves existing slugify-based format
22+
23+
#### Updated Method: `download_videos()`
24+
- **New Parameter:** `filename_format=None` (optional callable)
25+
- Signature: `filename_format(created_at, camera_name, path) -> str`
26+
- Passes parameter through to `_parse_downloaded_items()`
27+
- Maintains all existing parameters and behavior
28+
29+
#### Updated Method: `_parse_downloaded_items()`
30+
- **New Parameter:** `filename_format=None` (optional callable)
31+
- Uses provided formatter or defaults to `_format_filename_default()`
32+
- Single location where filenames are generated (no duplicated logic)
33+
- Cleaner code: format function called once per video item
34+
35+
---
36+
37+
## API
38+
39+
### Default Behavior (No Changes Required)
40+
41+
```python
42+
# Uses default slugified format
43+
await blink.download_videos(
44+
path="/tmp/videos",
45+
camera="Front Door",
46+
stop=10
47+
)
48+
# → /tmp/videos/front-door-2024-01-15t143022z.mp4
49+
```
50+
51+
### Custom Format Function
52+
53+
```python
54+
import datetime
55+
import os
56+
import pytz
57+
58+
def custom_format(created_at, camera_name, path):
59+
"""
60+
Custom filename formatter.
61+
62+
Args:
63+
created_at: ISO 8601 timestamp string (e.g., "2024-01-15T14:30:22Z")
64+
camera_name: Camera name from Blink API (e.g., "Front Door")
65+
path: Target directory path
66+
67+
Returns:
68+
Full filepath as string (must include directory + filename + extension)
69+
"""
70+
# Parse ISO timestamp and convert to Eastern time
71+
dt = datetime.datetime.fromisoformat(created_at).astimezone(
72+
pytz.timezone('US/Eastern')
73+
)
74+
# Remove spaces from camera name
75+
clean_camera = camera_name.replace(' ', '')
76+
# Build filename: YYYYMMDD_HHMMSS_CameraName.mp4
77+
filename = f"{dt:%Y%m%d_%H%M%S}_{clean_camera}.mp4"
78+
return os.path.join(path, filename)
79+
80+
# Use it
81+
await blink.download_videos(
82+
path="/tmp/videos",
83+
camera="Front Door",
84+
filename_format=custom_format
85+
)
86+
# → /tmp/videos/20240115_143022_FrontDoor.mp4
87+
```
88+
89+
---
90+
91+
## Implementation Details
92+
93+
### Design Decisions
94+
95+
1. **Callable Parameter Instead of Template String**
96+
- Why: More flexible, supports complex logic (timezone conversion, conditional formatting, etc.)
97+
- Simpler than implementing a template system
98+
- Matches Python conventions (cf. `map()`, `sorted(key=...)`)
99+
100+
2. **Extracted Default Formatter**
101+
- Makes it easier to modify or replace default format in the future
102+
- Keeps main parsing logic clean and focused
103+
- Single responsibility per method
104+
105+
3. **Format Function Called Once Per Item**
106+
- Efficient: no redundant calls or string manipulations
107+
- Clear: single place where filenames are generated
108+
- Testable: can test format functions in isolation
109+
110+
### Parameter Flow
111+
112+
```
113+
download_videos(path, camera, ..., filename_format)
114+
115+
_parse_downloaded_items(..., filename_format)
116+
117+
for each video item:
118+
filename = filename_format(created_at, camera_name, path)
119+
# download or log...
120+
```
121+
122+
---
123+
124+
## Testing
125+
126+
### Example Test Cases Provided
127+
128+
See `test_custom_format.py` for:
129+
- ✅ Default format (backward compatibility)
130+
- ✅ Eastern timezone conversion
131+
- ✅ ISO-style formatting
132+
- ✅ Minimal timestamp-only format
133+
- ✅ Integration with `download_videos()`
134+
135+
### Existing Tests
136+
137+
All existing tests continue to pass (backward compatible):
138+
- `test_parse_downloaded_items` — tests without custom formatter
139+
- `test_parse_downloaded_throttle` — tests default format behavior
140+
- `test_download_video_exit` — tests error handling
141+
142+
---
143+
144+
## Usage Examples
145+
146+
### Example 1: Eastern Time + Camera Name (from Issue)
147+
148+
```python
149+
def eastern_format(created_at, camera_name, path):
150+
dt = datetime.datetime.fromisoformat(created_at).astimezone(
151+
pytz.timezone('US/Eastern')
152+
)
153+
camera_name = camera_name.replace(' ', '')
154+
filename = f"{dt:%Y%m%d_%H%M%S}_{camera_name}.mp4"
155+
return os.path.join(path, filename)
156+
157+
await blink.download_videos(
158+
path="/backups/blink",
159+
filename_format=eastern_format
160+
)
161+
```
162+
163+
### Example 2: Keep Original Default (No Change)
164+
165+
```python
166+
# Simply don't pass filename_format
167+
await blink.download_videos(path="/videos")
168+
```
169+
170+
### Example 3: ISO Date + Separate Camera Folder
171+
172+
```python
173+
def organized_format(created_at, camera_name, path):
174+
dt = datetime.datetime.fromisoformat(created_at)
175+
camera_folder = os.path.join(path, camera_name)
176+
filename = f"{dt:%Y-%m-%d_%H-%M-%S}.mp4"
177+
return os.path.join(camera_folder, filename)
178+
179+
await blink.download_videos(
180+
path="/videos",
181+
filename_format=organized_format
182+
)
183+
# → /videos/Front Door/2024-01-15_14-30-22.mp4
184+
# → /videos/Back Patio/2024-01-16_09-15-45.mp4
185+
```
186+
187+
### Example 4: Include Media ID for Uniqueness
188+
189+
```python
190+
def include_id_format(created_at, camera_name, path):
191+
"""Include video ID if available (would need to extend signature)."""
192+
# Note: This example shows the current signature doesn't support it
193+
# but users could hash the created_at + camera_name if needed
194+
dt = datetime.datetime.fromisoformat(created_at)
195+
clean_camera = camera_name.replace(' ', '')
196+
filename = f"{dt:%Y%m%d_%H%M%S}_{clean_camera}_{hash(created_at)}.mp4"
197+
return os.path.join(path, filename)
198+
```
199+
200+
---
201+
202+
## Files Modified
203+
204+
1. **blinkpy/blinkpy.py**
205+
- Added `_format_filename_default()` method
206+
- Updated `download_videos()` signature and docstring
207+
- Updated `_parse_downloaded_items()` signature and implementation
208+
209+
2. **FILENAME_FORMAT_EXAMPLE.md** (NEW)
210+
- Documentation and usage examples
211+
- Multiple format function examples
212+
- Notes on best practices
213+
214+
3. **test_custom_format.py** (NEW)
215+
- Unit tests demonstrating the feature
216+
- Tests for various custom formats
217+
- Integration test with `download_videos()`
218+
219+
4. **IMPLEMENTATION_SUMMARY.md** (NEW - this file)
220+
- Detailed explanation of changes
221+
- API documentation
222+
- Design rationale
223+
224+
---
225+
226+
## Future Enhancements
227+
228+
Possible future improvements (not implemented):
229+
230+
1. **Extended Format Function Signature**
231+
- Could pass additional metadata (video duration, resolution, etc.)
232+
- Would require breaking change or new parameter
233+
234+
2. **Built-in Format Templates**
235+
- Could provide factory functions for common formats
236+
- Example: `Blink.FORMAT_EASTERN_TIME`, `Blink.FORMAT_ISO`
237+
238+
3. **Format Validation**
239+
- Could validate that returned path is safe (no path traversal)
240+
- Could auto-create directories if needed
241+
242+
---
243+
244+
## Notes for Integration
245+
246+
- ✅ No new dependencies added
247+
- ✅ Existing imports (datetime, pytz, os) already available in blinkpy
248+
- ✅ No breaking changes — all existing code works as-is
249+
- ✅ Clear error messages if format function fails (exception propagates)
250+
- ✅ Documentation included in docstrings (IDE autocomplete friendly)
251+
252+
---
253+
254+
## Questions?
255+
256+
See `FILENAME_FORMAT_EXAMPLE.md` for more usage examples, or review `test_custom_format.py` for test cases.

0 commit comments

Comments
 (0)