initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.git
|
||||||
|
data
|
||||||
|
.mypy_cache
|
||||||
286
README.md
Normal file
286
README.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# MPV Immersion Tracker for Language Learning
|
||||||
|
|
||||||
|
A Lua script for MPV that helps you track your language learning immersion sessions. The script uses a manual keybinding to start/stop tracking, giving you full control over when to log your sessions.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Manual Session Control**: Press `Ctrl+L` to start/stop immersion tracking
|
||||||
|
- **Session Tracking**: Records start/end times, duration, and progress
|
||||||
|
- **Comprehensive Logging**: Tracks video metadata, formats, resolution, and more
|
||||||
|
- **Resume Support**: Continues tracking if you stop and resume watching later
|
||||||
|
- **CSV Export**: Saves all session data to a CSV file for analysis
|
||||||
|
- **Real-time Progress**: Monitors watch progress and saves periodically
|
||||||
|
- **MPV Integration**: Uses only MPV's built-in functions - no external dependencies
|
||||||
|
- **MPV Options**: Configure everything through MPV's standard configuration system
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Locate your MPV scripts directory**:
|
||||||
|
- **Linux/macOS**: `~/.config/mpv/scripts/`
|
||||||
|
- **Windows**: `%APPDATA%\mpv\scripts\`
|
||||||
|
|
||||||
|
2. **Copy the script file**:
|
||||||
|
```bash
|
||||||
|
cp immersion-tracker.lua ~/.config/mpv/scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Restart MPV** or reload scripts with `Ctrl+Shift+r`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The script uses MPV's built-in options system. Add configuration to your `~/.config/mpv/mpv.conf` file:
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Change the keybinding (default: ctrl+l)
|
||||||
|
immersion-tracker-start_tracking_key=ctrl+k
|
||||||
|
|
||||||
|
# Change save frequency (default: 10 seconds)
|
||||||
|
immersion-tracker-save_interval=30
|
||||||
|
|
||||||
|
# Customize session naming
|
||||||
|
immersion-tracker-custom_prefix=[My Study]
|
||||||
|
immersion-tracker-max_title_length=80
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Enable debug logging
|
||||||
|
immersion-tracker-enable_debug_logging=true
|
||||||
|
|
||||||
|
# Show progress milestones
|
||||||
|
immersion-tracker-show_progress_milestones=true
|
||||||
|
immersion-tracker-milestone_percentages=10,25,50,75,90
|
||||||
|
|
||||||
|
# Customize notifications
|
||||||
|
immersion-tracker-show_session_start=false
|
||||||
|
immersion-tracker-show_session_end=true
|
||||||
|
|
||||||
|
# Export settings
|
||||||
|
immersion-tracker-export_csv=true
|
||||||
|
immersion-tracker-backup_csv=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Configuration Reference
|
||||||
|
|
||||||
|
See `mpv.conf.example` for all available options and example configurations.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
1. **Start MPV** with any video file
|
||||||
|
2. **Press `Ctrl+L`** to start immersion tracking
|
||||||
|
3. **Watch normally** - the script tracks everything in the background
|
||||||
|
4. **Press `Ctrl+L` again** to stop tracking
|
||||||
|
5. **Check the console** for tracking messages (press `~` to toggle console)
|
||||||
|
|
||||||
|
### Keybindings
|
||||||
|
|
||||||
|
- **`Ctrl+L`**: Start/stop immersion tracking (configurable)
|
||||||
|
- **`~`**: Toggle console to see tracking messages
|
||||||
|
|
||||||
|
### Manual Control
|
||||||
|
|
||||||
|
- **Start tracking**: Press `Ctrl+L` anytime during playback
|
||||||
|
- **Stop tracking**: Press `Ctrl+L` again to end the session
|
||||||
|
- **Check status**: Look for `[Immersion Tracker]` messages in the console
|
||||||
|
- **View data**: Check the generated CSV file in the `data/` directory
|
||||||
|
|
||||||
|
## Data Output
|
||||||
|
|
||||||
|
### CSV Format
|
||||||
|
|
||||||
|
The script generates a CSV file with the following columns:
|
||||||
|
|
||||||
|
| Column | Description |
|
||||||
|
| ------------ | ---------------------------------- |
|
||||||
|
| Session ID | Unique identifier for each session |
|
||||||
|
| Title | Video title or filename |
|
||||||
|
| Filename | Original filename |
|
||||||
|
| Path | Full file path |
|
||||||
|
| Duration | Total video duration in seconds |
|
||||||
|
| Start Time | Session start timestamp |
|
||||||
|
| End Time | Session end timestamp |
|
||||||
|
| Watch Time | Total time spent watching |
|
||||||
|
| Progress | Percentage of video watched |
|
||||||
|
| Video Format | Video codec |
|
||||||
|
| Audio Format | Audio codec |
|
||||||
|
| Resolution | Video resolution |
|
||||||
|
| FPS | Frame rate |
|
||||||
|
| Bitrate | Video bitrate |
|
||||||
|
|
||||||
|
### Session Files
|
||||||
|
|
||||||
|
- **Current session**: `data/current_session.json` (for resuming)
|
||||||
|
- **Backup files**: Previous sessions are backed up automatically
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
### Starting a Session
|
||||||
|
|
||||||
|
- Press `Ctrl+L` to start tracking
|
||||||
|
- The script will gather current video information
|
||||||
|
- Session data is saved immediately
|
||||||
|
- On-screen message confirms tracking has started (if enabled)
|
||||||
|
|
||||||
|
### Stopping a Session
|
||||||
|
|
||||||
|
- Press `Ctrl+L` again to stop tracking
|
||||||
|
- End time and total watch time are calculated
|
||||||
|
- Data is saved to CSV
|
||||||
|
- On-screen message shows completion percentage (if enabled)
|
||||||
|
|
||||||
|
### Resume Support
|
||||||
|
|
||||||
|
- If you stop watching and restart later, the script can resume tracking
|
||||||
|
- Progress is maintained across sessions
|
||||||
|
- Previous session data is preserved
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Auto-save
|
||||||
|
|
||||||
|
- Session data is saved every 10 seconds (configurable)
|
||||||
|
- Ensures data isn't lost if MPV crashes
|
||||||
|
- Graceful shutdown handling
|
||||||
|
|
||||||
|
### Progress Tracking
|
||||||
|
|
||||||
|
- Real-time watch progress monitoring
|
||||||
|
- Seek detection and position tracking
|
||||||
|
- Duration and completion percentage
|
||||||
|
- Configurable progress milestones
|
||||||
|
|
||||||
|
### On-screen Messages
|
||||||
|
|
||||||
|
- Configurable confirmation when tracking starts/stops
|
||||||
|
- Progress information displayed
|
||||||
|
- Milestone notifications (optional)
|
||||||
|
|
||||||
|
### Session Naming
|
||||||
|
|
||||||
|
- Use media title or filename
|
||||||
|
- Custom prefixes
|
||||||
|
- Length limits
|
||||||
|
- Automatic truncation
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Keybinding Settings
|
||||||
|
- `start_tracking_key`: Key to start/stop tracking (default: `ctrl+l`)
|
||||||
|
|
||||||
|
### File Paths
|
||||||
|
- `data_dir`: Data directory (default: `data`)
|
||||||
|
- `csv_file`: CSV output file (default: `data/immersion_log.csv`)
|
||||||
|
- `session_file`: Session file (default: `data/current_session.json`)
|
||||||
|
|
||||||
|
### Tracking Settings
|
||||||
|
- `save_interval`: Auto-save frequency in seconds (default: `10`)
|
||||||
|
- `min_session_duration`: Minimum session duration (default: `30`)
|
||||||
|
|
||||||
|
### Session Naming
|
||||||
|
- `use_title`: Use media title (default: `true`)
|
||||||
|
- `use_filename`: Use filename instead (default: `false`)
|
||||||
|
- `custom_prefix`: Custom prefix (default: `[Immersion] `)
|
||||||
|
- `max_title_length`: Maximum title length (default: `100`)
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- `show_session_start`: Show start message (default: `true`)
|
||||||
|
- `show_session_end`: Show end message (default: `true`)
|
||||||
|
- `show_progress_milestones`: Show milestones (default: `false`)
|
||||||
|
- `milestone_percentages`: Milestone percentages (default: `25,50,75,90`)
|
||||||
|
|
||||||
|
### Export Settings
|
||||||
|
- `export_csv`: Export to CSV (default: `true`)
|
||||||
|
- `backup_csv`: Create backups (default: `true`)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Script not loading**:
|
||||||
|
- Check MPV scripts directory path
|
||||||
|
- Verify file permissions
|
||||||
|
- Check console for error messages
|
||||||
|
|
||||||
|
2. **No tracking data**:
|
||||||
|
- Ensure you've pressed `Ctrl+L` to start tracking
|
||||||
|
- Check console for tracking messages
|
||||||
|
- Verify data directory exists
|
||||||
|
|
||||||
|
3. **Permission errors**:
|
||||||
|
- Ensure write access to scripts directory
|
||||||
|
- Check data directory permissions
|
||||||
|
|
||||||
|
4. **Configuration not working**:
|
||||||
|
- Verify options are in `~/.config/mpv/mpv.conf`
|
||||||
|
- Check option names (use `immersion-tracker-` prefix)
|
||||||
|
- Restart MPV or reload scripts
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging in your `mpv.conf`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
immersion-tracker-enable_debug_logging=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Console Messages
|
||||||
|
|
||||||
|
Look for these messages in the MPV console:
|
||||||
|
|
||||||
|
- `[Immersion Tracker] Script initialized`
|
||||||
|
- `[Immersion Tracker] Configuration loaded:`
|
||||||
|
- `[Immersion Tracker] Press ctrl+l to start/stop immersion tracking`
|
||||||
|
- `[Immersion Tracker] New immersion session started`
|
||||||
|
- `[Immersion Tracker] Session ended`
|
||||||
|
|
||||||
|
## Data Analysis
|
||||||
|
|
||||||
|
### CSV Analysis Tools
|
||||||
|
|
||||||
|
- **Spreadsheets**: Open in Excel, Google Sheets, or LibreOffice
|
||||||
|
- **Python**: Use pandas for data analysis
|
||||||
|
- **R**: Import and analyze with RStudio
|
||||||
|
- **Command line**: Use `awk`, `sed`, or `csvkit`
|
||||||
|
|
||||||
|
### Example Queries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Total watch time
|
||||||
|
awk -F',' 'NR>1 {sum+=$8} END {print "Total watch time:", sum, "seconds"}'
|
||||||
|
|
||||||
|
# Most watched content
|
||||||
|
awk -F',' 'NR>1 {print $2}' | sort | uniq -c | sort -nr
|
||||||
|
|
||||||
|
# Daily progress
|
||||||
|
awk -F',' 'NR>1 {print $6}' | cut -d' ' -f1 | sort | uniq -c
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- **Database support**: MySQL/PostgreSQL integration
|
||||||
|
- **Web dashboard**: View statistics in a browser
|
||||||
|
- **Export formats**: JSON, XML, or custom formats
|
||||||
|
- **Advanced analytics**: Watch patterns, learning goals
|
||||||
|
- **Cloud sync**: Backup data to cloud storage
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Feel free to submit issues, feature requests, or pull requests to improve the script.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This script is provided as-is for educational and personal use.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the console for error messages
|
||||||
|
2. Verify your MPV configuration
|
||||||
|
3. Check file permissions and paths
|
||||||
|
4. Review the troubleshooting section above
|
||||||
|
5. Check `mpv.conf.example` for configuration examples
|
||||||
298
analyze_data.py
Normal file
298
analyze_data.py
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MPV Immersion Tracker Data Analyzer
|
||||||
|
Analyzes the CSV data generated by the immersion tracker script
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class ImmersionAnalyzer:
|
||||||
|
def __init__(self, csv_file):
|
||||||
|
self.csv_file = csv_file
|
||||||
|
self.data = []
|
||||||
|
self.load_data()
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
"""Load data from CSV file"""
|
||||||
|
if not os.path.exists(self.csv_file):
|
||||||
|
print(f"Error: CSV file '{self.csv_file}' not found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.csv_file, "r", encoding="utf-8") as file:
|
||||||
|
reader = csv.DictReader(file)
|
||||||
|
for row in reader:
|
||||||
|
# Convert numeric fields
|
||||||
|
try:
|
||||||
|
row["Duration"] = int(row["Duration"])
|
||||||
|
row["Watch Time"] = int(row["Watch Time"])
|
||||||
|
row["Progress"] = float(row["Progress"])
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse timestamps
|
||||||
|
try:
|
||||||
|
row["Start Time"] = datetime.strptime(
|
||||||
|
row["Start Time"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
row["End Time"] = datetime.strptime(
|
||||||
|
row["End Time"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
print(f"Loaded {len(self.data)} sessions from {self.csv_file}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading CSV file: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def total_watch_time(self):
|
||||||
|
"""Calculate total watch time"""
|
||||||
|
total_seconds = sum(row["Watch Time"] for row in self.data)
|
||||||
|
hours = total_seconds // 3600
|
||||||
|
minutes = (total_seconds % 3600) // 60
|
||||||
|
return hours, minutes, total_seconds
|
||||||
|
|
||||||
|
def total_content_duration(self):
|
||||||
|
"""Calculate total content duration"""
|
||||||
|
total_seconds = sum(row["Duration"] for row in self.data)
|
||||||
|
hours = total_seconds // 3600
|
||||||
|
minutes = (total_seconds % 3600) // 60
|
||||||
|
return hours, minutes, total_seconds
|
||||||
|
|
||||||
|
def average_progress(self):
|
||||||
|
"""Calculate average completion percentage"""
|
||||||
|
if not self.data:
|
||||||
|
return 0
|
||||||
|
return sum(row["Progress"] for row in self.data) / len(self.data)
|
||||||
|
|
||||||
|
def most_watched_content(self, limit=10):
|
||||||
|
"""Find most watched content"""
|
||||||
|
content_watch_time = defaultdict(int)
|
||||||
|
for row in self.data:
|
||||||
|
content_watch_time[row["Title"]] += row["Watch Time"]
|
||||||
|
|
||||||
|
return sorted(content_watch_time.items(), key=lambda x: x[1], reverse=True)[
|
||||||
|
:limit
|
||||||
|
]
|
||||||
|
|
||||||
|
def daily_progress(self):
|
||||||
|
"""Calculate daily watch time"""
|
||||||
|
daily_watch = defaultdict(int)
|
||||||
|
for row in self.data:
|
||||||
|
date = row["Start Time"].date()
|
||||||
|
daily_watch[date] += row["Watch Time"]
|
||||||
|
|
||||||
|
return sorted(daily_watch.items())
|
||||||
|
|
||||||
|
def weekly_progress(self):
|
||||||
|
"""Calculate weekly watch time"""
|
||||||
|
weekly_watch = defaultdict(int)
|
||||||
|
for row in self.data:
|
||||||
|
# Get the week start (Monday)
|
||||||
|
date = row["Start Time"].date()
|
||||||
|
week_start = date - timedelta(days=date.weekday())
|
||||||
|
weekly_watch[week_start] += row["Watch Time"]
|
||||||
|
|
||||||
|
return sorted(weekly_watch.items())
|
||||||
|
|
||||||
|
def monthly_progress(self):
|
||||||
|
"""Calculate monthly watch time"""
|
||||||
|
monthly_watch = defaultdict(int)
|
||||||
|
for row in self.data:
|
||||||
|
month_start = row["Start Time"].replace(day=1).date()
|
||||||
|
monthly_watch[month_start] += row["Watch Time"]
|
||||||
|
|
||||||
|
return sorted(monthly_watch.items())
|
||||||
|
|
||||||
|
def format_duration(self, seconds):
|
||||||
|
"""Format duration in human-readable format"""
|
||||||
|
hours = seconds // 3600
|
||||||
|
minutes = (seconds % 3600) // 60
|
||||||
|
if hours > 0:
|
||||||
|
return f"{hours}h {minutes}m"
|
||||||
|
else:
|
||||||
|
return f"{minutes}m"
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
|
"""Print a comprehensive summary"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("MPV IMMERSION TRACKER - DATA SUMMARY")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if not self.data:
|
||||||
|
print("No data found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Basic stats
|
||||||
|
total_hours, total_minutes, total_seconds = self.total_watch_time()
|
||||||
|
content_hours, content_minutes, content_seconds = self.total_content_duration()
|
||||||
|
avg_progress = self.average_progress()
|
||||||
|
|
||||||
|
print(f"\n📊 BASIC STATISTICS:")
|
||||||
|
print(f" Total sessions: {len(self.data)}")
|
||||||
|
print(
|
||||||
|
f" Total watch time: {total_hours}h {total_minutes}m ({total_seconds:,} seconds)"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" Total content duration: {content_hours}h {content_minutes}m ({content_seconds:,} seconds)"
|
||||||
|
)
|
||||||
|
print(f" Average completion: {avg_progress:.1f}%")
|
||||||
|
|
||||||
|
# Time analysis
|
||||||
|
print(f"\n⏰ TIME ANALYSIS:")
|
||||||
|
daily_data = self.daily_progress()
|
||||||
|
if daily_data:
|
||||||
|
total_days = len(daily_data)
|
||||||
|
avg_daily = total_seconds / total_days
|
||||||
|
avg_daily_h, avg_daily_m = avg_daily // 3600, (avg_daily % 3600) // 60
|
||||||
|
|
||||||
|
print(f" Active days: {total_days}")
|
||||||
|
print(f" Average daily watch time: {avg_daily_h}h {avg_daily_m}m")
|
||||||
|
|
||||||
|
# Most active day
|
||||||
|
most_active_day, most_active_time = max(daily_data, key=lambda x: x[1])
|
||||||
|
print(
|
||||||
|
f" Most active day: {most_active_day} ({self.format_duration(most_active_time)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Content analysis
|
||||||
|
print(f"\n🎬 CONTENT ANALYSIS:")
|
||||||
|
most_watched = self.most_watched_content(5)
|
||||||
|
for i, (title, watch_time) in enumerate(most_watched, 1):
|
||||||
|
print(f" {i}. {title[:50]}{'...' if len(title) > 50 else ''}")
|
||||||
|
print(f" Watch time: {self.format_duration(watch_time)}")
|
||||||
|
|
||||||
|
# Recent activity
|
||||||
|
print(f"\n📅 RECENT ACTIVITY:")
|
||||||
|
recent_sessions = sorted(
|
||||||
|
self.data, key=lambda x: x["Start Time"], reverse=True
|
||||||
|
)[:5]
|
||||||
|
for session in recent_sessions:
|
||||||
|
date_str = session["Start Time"].strftime("%Y-%m-%d %H:%M")
|
||||||
|
print(
|
||||||
|
f" {date_str}: {session['Title'][:40]}{'...' if len(session['Title']) > 40 else ''}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" Progress: {session['Progress']:.1f}% ({self.format_duration(session['Watch Time'])})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def print_detailed_analysis(self):
|
||||||
|
"""Print detailed analysis with charts"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("DETAILED ANALYSIS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Weekly progress chart
|
||||||
|
print(f"\n📈 WEEKLY PROGRESS (Last 8 weeks):")
|
||||||
|
weekly_data = self.weekly_progress()
|
||||||
|
recent_weeks = weekly_data[-8:] if len(weekly_data) >= 8 else weekly_data
|
||||||
|
|
||||||
|
for week_start, watch_time in recent_weeks:
|
||||||
|
week_end = week_start + timedelta(days=6)
|
||||||
|
week_range = (
|
||||||
|
f"{week_start.strftime('%m/%d')} - {week_end.strftime('%m/%d')}"
|
||||||
|
)
|
||||||
|
duration_str = self.format_duration(watch_time)
|
||||||
|
print(f" Week of {week_range}: {duration_str}")
|
||||||
|
|
||||||
|
# Monthly progress chart
|
||||||
|
print(f"\n📊 MONTHLY PROGRESS:")
|
||||||
|
monthly_data = self.monthly_progress()
|
||||||
|
for month_start, watch_time in monthly_data:
|
||||||
|
month_name = month_start.strftime("%B %Y")
|
||||||
|
duration_str = self.format_duration(watch_time)
|
||||||
|
print(f" {month_name}: {duration_str}")
|
||||||
|
|
||||||
|
# Progress distribution
|
||||||
|
print(f"\n🎯 PROGRESS DISTRIBUTION:")
|
||||||
|
progress_ranges = {"0-25%": 0, "26-50%": 0, "51-75%": 0, "76-99%": 0, "100%": 0}
|
||||||
|
|
||||||
|
for row in self.data:
|
||||||
|
progress = row["Progress"]
|
||||||
|
if progress == 100:
|
||||||
|
progress_ranges["100%"] += 1
|
||||||
|
elif progress >= 76:
|
||||||
|
progress_ranges["76-99%"] += 1
|
||||||
|
elif progress >= 51:
|
||||||
|
progress_ranges["51-75%"] += 1
|
||||||
|
elif progress >= 26:
|
||||||
|
progress_ranges["26-50%"] += 1
|
||||||
|
else:
|
||||||
|
progress_ranges["0-25%"] += 1
|
||||||
|
|
||||||
|
for range_name, count in progress_ranges.items():
|
||||||
|
percentage = (count / len(self.data)) * 100 if self.data else 0
|
||||||
|
print(f" {range_name}: {count} sessions ({percentage:.1f}%)")
|
||||||
|
|
||||||
|
def export_summary(self, output_file):
|
||||||
|
"""Export summary to a text file"""
|
||||||
|
try:
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
# Redirect stdout to file
|
||||||
|
import io
|
||||||
|
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
sys.stdout = io.StringIO()
|
||||||
|
|
||||||
|
self.print_summary()
|
||||||
|
self.print_detailed_analysis()
|
||||||
|
|
||||||
|
output = sys.stdout.getvalue()
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
|
f.write(output)
|
||||||
|
print(f"Summary exported to: {output_file}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error exporting summary: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Analyze MPV Immersion Tracker data")
|
||||||
|
parser.add_argument(
|
||||||
|
"csv_file",
|
||||||
|
nargs="?",
|
||||||
|
default="data/immersion_log.csv",
|
||||||
|
help="Path to the CSV file (default: data/immersion_log.csv)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--export", "-e", metavar="FILE", help="Export summary to specified file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--detailed", "-d", action="store_true", help="Show detailed analysis"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Check if CSV file exists
|
||||||
|
if not os.path.exists(args.csv_file):
|
||||||
|
print(f"Error: CSV file '{args.csv_file}' not found!")
|
||||||
|
print("Make sure you have run the MPV immersion tracker first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create analyzer and run analysis
|
||||||
|
analyzer = ImmersionAnalyzer(args.csv_file)
|
||||||
|
|
||||||
|
if args.export:
|
||||||
|
analyzer.export_summary(args.export)
|
||||||
|
else:
|
||||||
|
analyzer.print_summary()
|
||||||
|
if args.detailed:
|
||||||
|
analyzer.print_detailed_analysis()
|
||||||
|
|
||||||
|
print(f"\n💡 Tip: Use --export FILENAME to save the analysis to a file")
|
||||||
|
print(f"💡 Tip: Use --detailed for more comprehensive analysis")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
21
immersion-tracker.conf
Normal file
21
immersion-tracker.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
start_tracking_key=ctrl+t
|
||||||
|
data_dir=~/.config/mpv/scripts/immersion-tracker/data
|
||||||
|
csv_file=~/.config/mpv/scripts/immersion-tracker/data/immersion_log.csv
|
||||||
|
session_file=~/.config/mpv/scripts/immersion-tracker/data/current_session.json
|
||||||
|
min_session_duration=30
|
||||||
|
save_interval=10
|
||||||
|
enable_debug_logging=no
|
||||||
|
backup_sessions=no
|
||||||
|
max_backup_files=10
|
||||||
|
use_title=yes
|
||||||
|
use_filename=no
|
||||||
|
custom_prefix=[Immersion]
|
||||||
|
max_title_length = 100
|
||||||
|
export_csv=yes
|
||||||
|
export_json=no
|
||||||
|
export_html=no
|
||||||
|
backup_csv=yes
|
||||||
|
show_session_start=yes
|
||||||
|
show_session_end=yes
|
||||||
|
show_progress_milestones=no
|
||||||
|
milestone_percentages=25507590
|
||||||
424
main.lua
Normal file
424
main.lua
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
-- MPV Immersion Tracker for Language Learning
|
||||||
|
-- This script tracks watching sessions for language learning content
|
||||||
|
-- Author: Generated for language learning immersion tracking
|
||||||
|
|
||||||
|
local mp = require("mp")
|
||||||
|
local utils = require("mp.utils")
|
||||||
|
|
||||||
|
local script_dir = mp.get_script_directory()
|
||||||
|
|
||||||
|
-- Configuration using MPV options
|
||||||
|
mp.options = require("mp.options")
|
||||||
|
local options = {
|
||||||
|
-- Keybinding settings
|
||||||
|
start_tracking_key = "ctrl+t",
|
||||||
|
|
||||||
|
-- File paths (relative to script directory)
|
||||||
|
data_dir = script_dir .. "/data",
|
||||||
|
csv_file = script_dir .. "/data/immersion_log.csv",
|
||||||
|
session_file = script_dir .. "/data/current_session.json",
|
||||||
|
|
||||||
|
-- Tracking settings
|
||||||
|
min_session_duration = 30, -- seconds
|
||||||
|
save_interval = 10, -- seconds
|
||||||
|
|
||||||
|
-- Advanced settings
|
||||||
|
enable_debug_logging = false,
|
||||||
|
backup_sessions = true,
|
||||||
|
max_backup_files = 10,
|
||||||
|
|
||||||
|
-- Session naming preferences
|
||||||
|
use_title = true, -- Use media title for session names
|
||||||
|
use_filename = false, -- Use filename instead
|
||||||
|
custom_prefix = "[Immersion] ", -- Add custom prefix to session names
|
||||||
|
max_title_length = 100, -- Maximum title length
|
||||||
|
|
||||||
|
-- Export settings
|
||||||
|
export_csv = true, -- Export to CSV
|
||||||
|
export_json = false, -- Export to JSON
|
||||||
|
export_html = false, -- Export to HTML report
|
||||||
|
backup_csv = true, -- Create backup CSV files
|
||||||
|
|
||||||
|
-- Notification settings
|
||||||
|
show_session_start = true, -- Show OSD message when session starts
|
||||||
|
show_session_end = true, -- Show OSD message when session ends
|
||||||
|
show_progress_milestones = false, -- Show progress milestone messages
|
||||||
|
milestone_percentages = "25,50,75,90", -- Comma-separated percentages
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.options.read_options(options, "immersion-tracker")
|
||||||
|
|
||||||
|
-- Parse milestone percentages from string to table
|
||||||
|
local function parse_milestone_percentages()
|
||||||
|
local percentages = {}
|
||||||
|
for percentage in options.milestone_percentages:gmatch("([^,]+)") do
|
||||||
|
local num = tonumber(percentage:match("^%s*(.-)%s*$"))
|
||||||
|
if num and num >= 0 and num <= 100 then
|
||||||
|
table.insert(percentages, num)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return percentages
|
||||||
|
end
|
||||||
|
|
||||||
|
options.milestone_percentages = parse_milestone_percentages()
|
||||||
|
|
||||||
|
-- Global variables
|
||||||
|
local current_session = nil
|
||||||
|
local session_start_time = nil
|
||||||
|
local last_save_time = 0
|
||||||
|
local video_info = {}
|
||||||
|
local is_tracking = false
|
||||||
|
|
||||||
|
-- Utility functions
|
||||||
|
local function log(message)
|
||||||
|
if options.enable_debug_logging then
|
||||||
|
mp.msg.info("[Immersion Tracker] " .. message)
|
||||||
|
else
|
||||||
|
mp.msg.info("[Immersion Tracker] " .. message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_data_directory()
|
||||||
|
local dir = options.data_dir
|
||||||
|
local result = utils.subprocess({
|
||||||
|
args = { "mkdir", "-p", dir },
|
||||||
|
cancellable = false,
|
||||||
|
})
|
||||||
|
if result.status ~= 0 then
|
||||||
|
log("Failed to create data directory: " .. dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_current_timestamp()
|
||||||
|
return os.time()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function format_timestamp(timestamp)
|
||||||
|
return os.date("%Y-%m-%d %H:%M:%S", timestamp)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_duration_string(seconds)
|
||||||
|
local hours = math.floor(seconds / 3600)
|
||||||
|
local minutes = math.floor((seconds % 3600) / 60)
|
||||||
|
local secs = seconds % 60
|
||||||
|
return string.format("%02d:%02d:%02d", hours, minutes, secs)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- File operations
|
||||||
|
local function save_session_to_file()
|
||||||
|
if not current_session then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local session_file = io.open(options.session_file, "w")
|
||||||
|
if session_file then
|
||||||
|
session_file:write(utils.format_json(current_session))
|
||||||
|
session_file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function load_existing_session()
|
||||||
|
local session_file = io.open(options.session_file, "r")
|
||||||
|
if session_file then
|
||||||
|
local content = session_file:read("*all")
|
||||||
|
session_file:close()
|
||||||
|
|
||||||
|
if content and content ~= "" then
|
||||||
|
local success, session = pcall(utils.parse_json, content)
|
||||||
|
if success and session then
|
||||||
|
current_session = session
|
||||||
|
session_start_time = session.start_time
|
||||||
|
is_tracking = true
|
||||||
|
log("Resumed existing session: " .. session.title)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function save_session_to_csv()
|
||||||
|
if not current_session then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local csv_path = options.csv_file
|
||||||
|
log("Saving session to CSV: " .. csv_path)
|
||||||
|
|
||||||
|
-- Create CSV header if file doesn't exist
|
||||||
|
local file_exists = io.open(csv_path, "r")
|
||||||
|
local need_header = not file_exists
|
||||||
|
if file_exists then
|
||||||
|
file_exists:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local csv_file = io.open(csv_path, "a")
|
||||||
|
if not csv_file then
|
||||||
|
log("Failed to open CSV file for writing")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write header if needed
|
||||||
|
if need_header then
|
||||||
|
csv_file:write(
|
||||||
|
"Session ID,Title,Filename,Path,Duration,Start Time,End Time,Watch Time,Progress,Video Format,Audio Format,Resolution,FPS,Bitrate\n"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write session data
|
||||||
|
local csv_line = string.format(
|
||||||
|
'"%s","%s","%s","%s",%d,"%s","%s",%d,%.2f,"%s","%s","%s","%s","%s"\n',
|
||||||
|
current_session.id,
|
||||||
|
current_session.title:gsub('"', '""'),
|
||||||
|
current_session.filename:gsub('"', '""'),
|
||||||
|
current_session.path:gsub('"', '""'),
|
||||||
|
current_session.duration,
|
||||||
|
current_session.start_timestamp,
|
||||||
|
current_session.end_timestamp,
|
||||||
|
current_session.total_watch_time,
|
||||||
|
current_session.watch_progress,
|
||||||
|
current_session.video_format,
|
||||||
|
current_session.audio_format,
|
||||||
|
current_session.resolution,
|
||||||
|
current_session.fps,
|
||||||
|
current_session.bitrate
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_file:write(csv_line)
|
||||||
|
csv_file:close()
|
||||||
|
|
||||||
|
log("Session saved to CSV: " .. current_session.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Video information gathering
|
||||||
|
local function gather_video_info()
|
||||||
|
video_info = {
|
||||||
|
filename = mp.get_property("filename") or "unknown",
|
||||||
|
path = mp.get_property("path") or "unknown",
|
||||||
|
title = mp.get_property("media-title") or mp.get_property("filename") or "unknown",
|
||||||
|
duration = mp.get_property_number("duration") or 0,
|
||||||
|
video_format = mp.get_property("video-codec") or "unknown",
|
||||||
|
audio_format = mp.get_property("audio-codec") or "unknown",
|
||||||
|
resolution = mp.get_property("video-params/width")
|
||||||
|
and mp.get_property("video-params/height")
|
||||||
|
and mp.get_property("video-params/width") .. "x" .. mp.get_property("video-params/height")
|
||||||
|
or "unknown",
|
||||||
|
fps = mp.get_property("video-params/fps") or "unknown",
|
||||||
|
bitrate = mp.get_property("video-bitrate") or "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Video info gathered: " .. video_info.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Session management
|
||||||
|
local function start_new_session()
|
||||||
|
if current_session then
|
||||||
|
log("Session already in progress, ending previous session first")
|
||||||
|
end_current_session()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gather current video info
|
||||||
|
gather_video_info()
|
||||||
|
|
||||||
|
-- Determine session title based on options
|
||||||
|
local session_title = video_info.title
|
||||||
|
if options.use_filename then
|
||||||
|
session_title = video_info.filename
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Apply custom prefix and length limit
|
||||||
|
if options.custom_prefix then
|
||||||
|
session_title = options.custom_prefix .. session_title
|
||||||
|
end
|
||||||
|
|
||||||
|
if #session_title > options.max_title_length then
|
||||||
|
session_title = session_title:sub(1, options.max_title_length) .. "..."
|
||||||
|
end
|
||||||
|
|
||||||
|
current_session = {
|
||||||
|
id = os.time() .. "_" .. math.random(1000, 9999),
|
||||||
|
filename = video_info.filename,
|
||||||
|
path = video_info.path,
|
||||||
|
title = session_title,
|
||||||
|
duration = video_info.duration,
|
||||||
|
start_time = get_current_timestamp(),
|
||||||
|
start_timestamp = format_timestamp(get_current_timestamp()),
|
||||||
|
video_format = video_info.video_format,
|
||||||
|
audio_format = video_info.audio_format,
|
||||||
|
resolution = video_info.resolution,
|
||||||
|
fps = video_info.fps,
|
||||||
|
bitrate = video_info.bitrate,
|
||||||
|
watch_progress = 0,
|
||||||
|
last_position = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
session_start_time = get_current_timestamp()
|
||||||
|
is_tracking = true
|
||||||
|
save_session_to_file()
|
||||||
|
log("New immersion session started: " .. current_session.title)
|
||||||
|
|
||||||
|
-- Show on-screen message if enabled
|
||||||
|
if options.show_session_start then
|
||||||
|
mp.osd_message("Immersion tracking started: " .. current_session.title, 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_session_progress()
|
||||||
|
if not current_session or not is_tracking then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local current_pos = mp.get_property_number("time-pos") or 0
|
||||||
|
local duration = mp.get_property_number("duration") or 0
|
||||||
|
|
||||||
|
if duration > 0 then
|
||||||
|
current_session.watch_progress = (current_pos / duration) * 100
|
||||||
|
current_session.last_position = current_pos
|
||||||
|
|
||||||
|
-- Check for milestone notifications
|
||||||
|
if options.show_progress_milestones then
|
||||||
|
for _, milestone in ipairs(options.milestone_percentages) do
|
||||||
|
if
|
||||||
|
current_session.watch_progress >= milestone
|
||||||
|
and (not current_session.milestones_reached or not current_session.milestones_reached[milestone])
|
||||||
|
then
|
||||||
|
if not current_session.milestones_reached then
|
||||||
|
current_session.milestones_reached = {}
|
||||||
|
end
|
||||||
|
current_session.milestones_reached[milestone] = true
|
||||||
|
mp.osd_message(string.format("Progress milestone: %d%%", milestone), 2)
|
||||||
|
log("Progress milestone reached: " .. milestone .. "%")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Auto-save if enough time has passed
|
||||||
|
local current_time = get_current_timestamp()
|
||||||
|
if current_time - last_save_time >= options.save_interval then
|
||||||
|
save_session_to_file()
|
||||||
|
last_save_time = current_time
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function end_current_session()
|
||||||
|
if not current_session or not is_tracking then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local end_time = get_current_timestamp()
|
||||||
|
current_session.end_time = end_time
|
||||||
|
current_session.end_timestamp = format_timestamp(end_time)
|
||||||
|
current_session.total_watch_time = end_time - session_start_time
|
||||||
|
current_session.watch_progress = (current_session.last_position / current_session.duration) * 100
|
||||||
|
|
||||||
|
-- Save to CSV if enabled
|
||||||
|
if options.export_csv then
|
||||||
|
save_session_to_csv()
|
||||||
|
end
|
||||||
|
|
||||||
|
log(
|
||||||
|
"Session ended: "
|
||||||
|
.. current_session.title
|
||||||
|
.. " (Progress: "
|
||||||
|
.. string.format("%.1f", current_session.watch_progress)
|
||||||
|
.. "%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Show on-screen message if enabled
|
||||||
|
if options.show_session_end then
|
||||||
|
mp.osd_message(
|
||||||
|
"Immersion tracking ended: " .. string.format("%.1f", current_session.watch_progress) .. "% completed",
|
||||||
|
3
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
current_session = nil
|
||||||
|
session_start_time = nil
|
||||||
|
is_tracking = false
|
||||||
|
|
||||||
|
-- Clean up session file
|
||||||
|
local session_file = io.open(options.session_file, "w")
|
||||||
|
if session_file then
|
||||||
|
session_file:write("")
|
||||||
|
session_file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Keybinding handler
|
||||||
|
local function toggle_tracking()
|
||||||
|
if is_tracking then
|
||||||
|
log("Stopping immersion tracking...")
|
||||||
|
end_current_session()
|
||||||
|
else
|
||||||
|
log("Starting immersion tracking...")
|
||||||
|
start_new_session()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Event handlers
|
||||||
|
local function on_file_loaded()
|
||||||
|
log("File loaded, ready for manual tracking")
|
||||||
|
|
||||||
|
-- Try to load existing session if available
|
||||||
|
load_existing_session()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_file_end()
|
||||||
|
if current_session and is_tracking then
|
||||||
|
log("File ended, completing session...")
|
||||||
|
end_current_session()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_shutdown()
|
||||||
|
if current_session and is_tracking then
|
||||||
|
log("MPV shutting down, saving session...")
|
||||||
|
end_current_session()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_seek()
|
||||||
|
if current_session and is_tracking then
|
||||||
|
update_session_progress()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_time_update()
|
||||||
|
if current_session and is_tracking then
|
||||||
|
update_session_progress()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize script
|
||||||
|
local function init()
|
||||||
|
log("Immersion Tracker initialized")
|
||||||
|
log("Configuration loaded:")
|
||||||
|
log(" Keybinding: " .. options.start_tracking_key)
|
||||||
|
log(" Data directory: " .. options.data_dir)
|
||||||
|
log(" Save interval: " .. options.save_interval .. " seconds")
|
||||||
|
log(" Debug logging: " .. (options.enable_debug_logging and "enabled" or "disabled"))
|
||||||
|
|
||||||
|
ensure_data_directory()
|
||||||
|
|
||||||
|
-- Register keybinding
|
||||||
|
mp.remove_key_binding("toggle-clipboard-insertion")
|
||||||
|
mp.add_key_binding(options.start_tracking_key, "immersion_tracking", toggle_tracking)
|
||||||
|
|
||||||
|
-- Register event handlers
|
||||||
|
mp.register_event("file-loaded", on_file_loaded)
|
||||||
|
mp.register_event("end-file", on_file_end)
|
||||||
|
mp.register_event("shutdown", on_shutdown)
|
||||||
|
|
||||||
|
-- Register property change handlers
|
||||||
|
mp.observe_property("time-pos", "number", on_time_update)
|
||||||
|
|
||||||
|
-- Register seek event
|
||||||
|
mp.register_event("seek", on_seek)
|
||||||
|
|
||||||
|
log("Event handlers registered successfully")
|
||||||
|
log("Press " .. options.start_tracking_key .. " to start/stop immersion tracking")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Start the script
|
||||||
|
init()
|
||||||
Reference in New Issue
Block a user