Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions FILENAME_FORMAT_EXAMPLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Custom Video Filename Formatting

The `download_videos()` method now supports an optional `filename_format` parameter to customize how video filenames are generated.

## Default Behavior (No Change)

If you don't provide `filename_format`, videos are named using the default slugified format:

```python
await blink.download_videos(path="/tmp/videos")
# Generates: /tmp/videos/front-door-2024-01-15t143022z.mp4
```

## Custom Format Function

Pass a callable that accepts `(created_at, camera_name, path)` and returns the full filepath:

```python
import datetime
import os
import pytz

def custom_format(created_at, camera_name, path):
"""Format: YYYYMMDD_HHMMSS_CameraName.mp4"""
dt = datetime.datetime.fromisoformat(created_at).astimezone(
pytz.timezone('US/Eastern')
)
clean_camera = camera_name.replace(' ', '')
filename = f"{dt:%Y%m%d_%H%M%S}_{clean_camera}.mp4"
return os.path.join(path, filename)

# Use it
await blink.download_videos(
path="/tmp/videos",
filename_format=custom_format
)
# Generates: /tmp/videos/20240115_143022_FrontDoor.mp4
```

## Another Example: ISO + Camera Name

```python
def iso_format(created_at, camera_name, path):
"""Format: YYYY-MM-DD_HH-MM-SS_CameraName.mp4"""
dt = datetime.datetime.fromisoformat(created_at)
clean_camera = camera_name.replace(' ', '_')
filename = f"{dt:%Y-%m-%d_%H-%M-%S}_{clean_camera}.mp4"
return os.path.join(path, filename)

await blink.download_videos(
path="/tmp/videos",
camera="Front Door",
filename_format=iso_format
)
# Generates: /tmp/videos/2024-01-15_14-30-22_Front_Door.mp4
```

## Notes

- The `created_at` parameter is a string in ISO 8601 format (e.g., `"2024-01-15T14:30:22Z"`)
- Your callable is responsible for returning the **full path** including the filename
- The `.mp4` extension is your responsibility (it's not added automatically)
- All other filtering (camera name, deleted flag, etc.) still applies
- The format function is called once per video, so keep it efficient
256 changes: 256 additions & 0 deletions IMPLEMENTATION_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Implementation Summary: Custom Video Filename Formatting

## Overview

Added optional `filename_format` parameter to `download_videos()` method, allowing customization of how downloaded video filenames are generated.

**Date:** 2026-04-05
**Changes Made:** 2 files
**Backward Compatibility:** ✅ Full (defaults to existing behavior)

---

## Changes

### 1. **blinkpy/blinkpy.py**

#### New Method: `_format_filename_default()`
- Extracted default filename formatting logic into a reusable method
- Signature: `_format_filename_default(self, created_at, camera_name, path) -> str`
- Returns the full filepath with `.mp4` extension
- Preserves existing slugify-based format

#### Updated Method: `download_videos()`
- **New Parameter:** `filename_format=None` (optional callable)
- Signature: `filename_format(created_at, camera_name, path) -> str`
- Passes parameter through to `_parse_downloaded_items()`
- Maintains all existing parameters and behavior

#### Updated Method: `_parse_downloaded_items()`
- **New Parameter:** `filename_format=None` (optional callable)
- Uses provided formatter or defaults to `_format_filename_default()`
- Single location where filenames are generated (no duplicated logic)
- Cleaner code: format function called once per video item

---

## API

### Default Behavior (No Changes Required)

```python
# Uses default slugified format
await blink.download_videos(
path="/tmp/videos",
camera="Front Door",
stop=10
)
# → /tmp/videos/front-door-2024-01-15t143022z.mp4
```

### Custom Format Function

```python
import datetime
import os
import pytz

def custom_format(created_at, camera_name, path):
"""
Custom filename formatter.

Args:
created_at: ISO 8601 timestamp string (e.g., "2024-01-15T14:30:22Z")
camera_name: Camera name from Blink API (e.g., "Front Door")
path: Target directory path

Returns:
Full filepath as string (must include directory + filename + extension)
"""
# Parse ISO timestamp and convert to Eastern time
dt = datetime.datetime.fromisoformat(created_at).astimezone(
pytz.timezone('US/Eastern')
)
# Remove spaces from camera name
clean_camera = camera_name.replace(' ', '')
# Build filename: YYYYMMDD_HHMMSS_CameraName.mp4
filename = f"{dt:%Y%m%d_%H%M%S}_{clean_camera}.mp4"
return os.path.join(path, filename)

# Use it
await blink.download_videos(
path="/tmp/videos",
camera="Front Door",
filename_format=custom_format
)
# → /tmp/videos/20240115_143022_FrontDoor.mp4
```

---

## Implementation Details

### Design Decisions

1. **Callable Parameter Instead of Template String**
- Why: More flexible, supports complex logic (timezone conversion, conditional formatting, etc.)
- Simpler than implementing a template system
- Matches Python conventions (cf. `map()`, `sorted(key=...)`)

2. **Extracted Default Formatter**
- Makes it easier to modify or replace default format in the future
- Keeps main parsing logic clean and focused
- Single responsibility per method

3. **Format Function Called Once Per Item**
- Efficient: no redundant calls or string manipulations
- Clear: single place where filenames are generated
- Testable: can test format functions in isolation

### Parameter Flow

```
download_videos(path, camera, ..., filename_format)
_parse_downloaded_items(..., filename_format)
for each video item:
filename = filename_format(created_at, camera_name, path)
# download or log...
```

---

## Testing

### Example Test Cases Provided

See `test_custom_format.py` for:
- ✅ Default format (backward compatibility)
- ✅ Eastern timezone conversion
- ✅ ISO-style formatting
- ✅ Minimal timestamp-only format
- ✅ Integration with `download_videos()`

### Existing Tests

All existing tests continue to pass (backward compatible):
- `test_parse_downloaded_items` — tests without custom formatter
- `test_parse_downloaded_throttle` — tests default format behavior
- `test_download_video_exit` — tests error handling

---

## Usage Examples

### Example 1: Eastern Time + Camera Name (from Issue)

```python
def eastern_format(created_at, camera_name, path):
dt = datetime.datetime.fromisoformat(created_at).astimezone(
pytz.timezone('US/Eastern')
)
camera_name = camera_name.replace(' ', '')
filename = f"{dt:%Y%m%d_%H%M%S}_{camera_name}.mp4"
return os.path.join(path, filename)

await blink.download_videos(
path="/backups/blink",
filename_format=eastern_format
)
```

### Example 2: Keep Original Default (No Change)

```python
# Simply don't pass filename_format
await blink.download_videos(path="/videos")
```

### Example 3: ISO Date + Separate Camera Folder

```python
def organized_format(created_at, camera_name, path):
dt = datetime.datetime.fromisoformat(created_at)
camera_folder = os.path.join(path, camera_name)
filename = f"{dt:%Y-%m-%d_%H-%M-%S}.mp4"
return os.path.join(camera_folder, filename)

await blink.download_videos(
path="/videos",
filename_format=organized_format
)
# → /videos/Front Door/2024-01-15_14-30-22.mp4
# → /videos/Back Patio/2024-01-16_09-15-45.mp4
```

### Example 4: Include Media ID for Uniqueness

```python
def include_id_format(created_at, camera_name, path):
"""Include video ID if available (would need to extend signature)."""
# Note: This example shows the current signature doesn't support it
# but users could hash the created_at + camera_name if needed
dt = datetime.datetime.fromisoformat(created_at)
clean_camera = camera_name.replace(' ', '')
filename = f"{dt:%Y%m%d_%H%M%S}_{clean_camera}_{hash(created_at)}.mp4"
return os.path.join(path, filename)
```

---

## Files Modified

1. **blinkpy/blinkpy.py**
- Added `_format_filename_default()` method
- Updated `download_videos()` signature and docstring
- Updated `_parse_downloaded_items()` signature and implementation

2. **FILENAME_FORMAT_EXAMPLE.md** (NEW)
- Documentation and usage examples
- Multiple format function examples
- Notes on best practices

3. **test_custom_format.py** (NEW)
- Unit tests demonstrating the feature
- Tests for various custom formats
- Integration test with `download_videos()`

4. **IMPLEMENTATION_SUMMARY.md** (NEW - this file)
- Detailed explanation of changes
- API documentation
- Design rationale

---

## Future Enhancements

Possible future improvements (not implemented):

1. **Extended Format Function Signature**
- Could pass additional metadata (video duration, resolution, etc.)
- Would require breaking change or new parameter

2. **Built-in Format Templates**
- Could provide factory functions for common formats
- Example: `Blink.FORMAT_EASTERN_TIME`, `Blink.FORMAT_ISO`

3. **Format Validation**
- Could validate that returned path is safe (no path traversal)
- Could auto-create directories if needed

---

## Notes for Integration

- ✅ No new dependencies added
- ✅ Existing imports (datetime, pytz, os) already available in blinkpy
- ✅ No breaking changes — all existing code works as-is
- ✅ Clear error messages if format function fails (exception propagates)
- ✅ Documentation included in docstrings (IDE autocomplete friendly)

---

## Questions?

See `FILENAME_FORMAT_EXAMPLE.md` for more usage examples, or review `test_custom_format.py` for test cases.
Loading
Loading