commit 490a182f727c65dc5b7c78112ed0a78c2efa7c16 Author: sudacode Date: Tue Aug 19 00:09:13 2025 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9748ff9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.git +data +.mypy_cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2494ad --- /dev/null +++ b/README.md @@ -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 diff --git a/analyze_data.py b/analyze_data.py new file mode 100644 index 0000000..204c7b8 --- /dev/null +++ b/analyze_data.py @@ -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() diff --git a/immersion-tracker.conf b/immersion-tracker.conf new file mode 100644 index 0000000..90bed0d --- /dev/null +++ b/immersion-tracker.conf @@ -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 diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..0021a4a --- /dev/null +++ b/main.lua @@ -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()