Add download all recordings as ZIP feature
- Add /download_all endpoint to create in-memory ZIP archive - Include zipfile and io imports for ZIP creation - Add "Download All as ZIP" button in web interface (only shows when recordings exist) - ZIP filename includes timestamp: wedding_recordings_YYYYMMDD_HHMMSS.zip - Only includes .wav files from recordings directory - Update API documentation in README.md - Document feature in CLAUDE.md Allows users to quickly backup all guest recordings in a single download. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -178,6 +178,14 @@ Edit the `RotaryPhone` class methods:
|
|||||||
- Backup called after recording saves and greeting uploads
|
- Backup called after recording saves and greeting uploads
|
||||||
- Test with `/api/backup/test` endpoint
|
- Test with `/api/backup/test` endpoint
|
||||||
|
|
||||||
|
### Download All Recordings
|
||||||
|
The `/download_all` endpoint creates a ZIP archive of all recordings in memory:
|
||||||
|
- Uses `zipfile` and `io.BytesIO()` for in-memory ZIP creation
|
||||||
|
- Filename includes timestamp: `wedding_recordings_YYYYMMDD_HHMMSS.zip`
|
||||||
|
- Only includes `.wav` files from recordings directory
|
||||||
|
- Returns 404 if no recordings exist
|
||||||
|
- Web UI shows "Download All as ZIP" button when recordings are present
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### Audio Testing
|
### Audio Testing
|
||||||
|
|||||||
@@ -300,7 +300,8 @@ The web interface provides four main sections:
|
|||||||
|
|
||||||
#### 4. Recordings
|
#### 4. Recordings
|
||||||
- **Play**: Listen to recordings directly in the browser
|
- **Play**: Listen to recordings directly in the browser
|
||||||
- **Download**: Save recordings to your computer
|
- **Download**: Save individual recordings to your computer
|
||||||
|
- **Download All**: Download all recordings as a single ZIP file with timestamp
|
||||||
- **Delete**: Remove unwanted recordings
|
- **Delete**: Remove unwanted recordings
|
||||||
- **Statistics**: View total recordings, storage used, and total duration
|
- **Statistics**: View total recordings, storage used, and total duration
|
||||||
|
|
||||||
@@ -619,6 +620,7 @@ The system provides REST API endpoints:
|
|||||||
- `POST /delete_greeting/<filename>` - Delete greeting
|
- `POST /delete_greeting/<filename>` - Delete greeting
|
||||||
- `GET /play_audio/<type>/<filename>` - Stream audio file
|
- `GET /play_audio/<type>/<filename>` - Stream audio file
|
||||||
- `GET /download/<filename>` - Download recording
|
- `GET /download/<filename>` - Download recording
|
||||||
|
- `GET /download_all` - Download all recordings as ZIP
|
||||||
- `POST /delete/<filename>` - Delete recording
|
- `POST /delete/<filename>` - Delete recording
|
||||||
- `POST /restore_default_sound` - Generate default dial tone
|
- `POST /restore_default_sound` - Generate default dial tone
|
||||||
- `GET /api/backup/status` - Get USB backup drive status
|
- `GET /api/backup/status` - Get USB backup drive status
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
import shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
|
||||||
# Load configuration
|
# Load configuration
|
||||||
def load_system_config():
|
def load_system_config():
|
||||||
@@ -798,6 +800,42 @@ def download_recording(filename):
|
|||||||
return send_file(filepath, as_attachment=True)
|
return send_file(filepath, as_attachment=True)
|
||||||
return "File not found", 404
|
return "File not found", 404
|
||||||
|
|
||||||
|
@app.route('/download_all')
|
||||||
|
def download_all_recordings():
|
||||||
|
"""Download all recordings as a ZIP file"""
|
||||||
|
if not os.path.exists(OUTPUT_DIR):
|
||||||
|
return "No recordings found", 404
|
||||||
|
|
||||||
|
# Get all recording files
|
||||||
|
recordings = [f for f in os.listdir(OUTPUT_DIR)
|
||||||
|
if f.endswith('.wav') and os.path.isfile(os.path.join(OUTPUT_DIR, f))]
|
||||||
|
|
||||||
|
if not recordings:
|
||||||
|
return "No recordings found", 404
|
||||||
|
|
||||||
|
# Create ZIP file in memory
|
||||||
|
memory_file = io.BytesIO()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for filename in recordings:
|
||||||
|
filepath = os.path.join(OUTPUT_DIR, filename)
|
||||||
|
# Add file to ZIP with just the filename (no path)
|
||||||
|
zf.write(filepath, arcname=filename)
|
||||||
|
|
||||||
|
# Seek to beginning of file
|
||||||
|
memory_file.seek(0)
|
||||||
|
|
||||||
|
# Generate filename with timestamp
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
zip_filename = f"wedding_recordings_{timestamp}.zip"
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
memory_file,
|
||||||
|
mimetype='application/zip',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=zip_filename
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/delete/<filename>', methods=['POST'])
|
@app.route('/delete/<filename>', methods=['POST'])
|
||||||
def delete_recording(filename):
|
def delete_recording(filename):
|
||||||
"""Delete a recording"""
|
"""Delete a recording"""
|
||||||
@@ -1499,8 +1537,15 @@ def main():
|
|||||||
|
|
||||||
<!-- Recordings Card -->
|
<!-- Recordings Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>🎙️ Recordings</h2>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
|
<h2 style="margin: 0;">🎙️ Recordings</h2>
|
||||||
|
{% if recordings %}
|
||||||
|
<button class="btn btn-primary" onclick="downloadAllRecordings()" style="font-size: 0.9em;">
|
||||||
|
📦 Download All as ZIP
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if recordings %}
|
{% if recordings %}
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
@@ -1715,7 +1760,11 @@ def main():
|
|||||||
function downloadRecording(filename) {
|
function downloadRecording(filename) {
|
||||||
window.location.href = '/download/' + filename;
|
window.location.href = '/download/' + filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadAllRecordings() {
|
||||||
|
window.location.href = '/download_all';
|
||||||
|
}
|
||||||
|
|
||||||
function deleteRecording(filename, index) {
|
function deleteRecording(filename, index) {
|
||||||
if (confirm('Delete this recording?')) {
|
if (confirm('Delete this recording?')) {
|
||||||
fetch('/delete/' + filename, { method: 'POST' })
|
fetch('/delete/' + filename, { method: 'POST' })
|
||||||
|
|||||||
Reference in New Issue
Block a user