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