-- 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 video_info_refresh_delay = 1.0, -- seconds to wait before refreshing video info -- 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 max_title_length = 100, -- Maximum title length -- Export settings export_csv = true, -- Export to CSV -- Notification settings show_session_start = true, -- Show OSD message when session starts show_session_end = true, -- Show OSD message when session ends } mp.options.read_options(options, "immersion-tracker") -- Global variables local current_session = nil local session_start_time = nil 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 -- Parse CSV line to extract session ID from first column local function parse_csv_session_id(csv_line) -- Handle quoted fields properly if csv_line:match('^"') then -- Find the closing quote for the first field local end_quote = csv_line:find('"', 2) if end_quote then return csv_line:sub(2, end_quote - 1) end end -- Fallback: split by comma and take first field return csv_line:match("^([^,]+)") 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 -- 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) -- Read existing CSV content to check for existing session local existing_lines = {} local session_exists = false local existing_file = io.open(csv_path, "r") if existing_file then for line in existing_file:lines() do table.insert(existing_lines, line) end existing_file:close() end -- Check if session already exists in CSV local header_line = nil local updated_lines = {} if #existing_lines > 0 then header_line = existing_lines[1] table.insert(updated_lines, header_line) -- Check remaining lines for existing session for i = 2, #existing_lines do local line = existing_lines[i] -- Parse CSV line to extract session ID (first column) local session_id = parse_csv_session_id(line) if session_id == current_session.id then -- Update existing session line local updated_line = string.format( '"%s","%s","%s","%s",%d,"%s","%s",%d,%.2f,"%s","%s","%s","%s","%s"', 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 or current_session.start_timestamp, current_session.total_watch_time or (get_current_timestamp() - session_start_time), current_session.watch_progress, current_session.video_format, current_session.audio_format, current_session.resolution, current_session.fps, current_session.bitrate ) table.insert(updated_lines, updated_line) session_exists = true log("Updated existing session in CSV: " .. current_session.id) else -- Keep other sessions unchanged table.insert(updated_lines, line) end end end -- If session doesn't exist, add header and new session if not session_exists then if not header_line then header_line = "Session ID,Title,Filename,Path,Duration,Start Time,End Time," .. "Watch Time,Progress,Video Format,Audio Format,Resolution,FPS,Bitrate" table.insert(updated_lines, header_line) end mp.msg.info("current_session: " .. utils.format_json(current_session)) -- Add new session line local new_line = string.format( '"%s","%s","%s","%s",%d,"%s","%s",%d,%.2f,"%s","%s","%s","%s","%s"', current_session.id or "", (current_session.title or ""):gsub('"', '""'), (current_session.filename or ""):gsub('"', '""'), (current_session.path or ""):gsub('"', '""'), tonumber(current_session.duration) or 0, current_session.start_timestamp or "", current_session.end_timestamp or current_session.start_timestamp or "", tonumber(current_session.total_watch_time) or ( get_current_timestamp and session_start_time and (get_current_timestamp() - session_start_time) or 0 ), tonumber(current_session.watch_progress) or 0, current_session.video_format or "", current_session.audio_format or "", current_session.resolution or "", current_session.fps or "", current_session.bitrate or "" ) table.insert(updated_lines, new_line) log("Added new session to CSV: " .. current_session.id) end -- Write updated content back to file local csv_file = io.open(csv_path, "w") if not csv_file then log("Failed to open CSV file for writing") return end for _, line in ipairs(updated_lines) do csv_file:write(line .. "\n") end csv_file:close() log("Session saved to CSV: " .. current_session.title) end -- Video information gathering local function gather_video_info() -- Get video dimensions - try multiple property names local width = mp.get_property_number("video-params/w") or mp.get_property_number("video-params/width") local height = mp.get_property_number("video-params/h") or mp.get_property_number("video-params/height") local resolution = "unknown" if width and height and width > 0 and height > 0 then resolution = width .. "x" .. height end -- Get FPS - try multiple property names local fps = mp.get_property_number("video-params/fps") or mp.get_property_number("fps") local fps_str = "unknown" if fps and fps > 0 then fps_str = string.format("%.2f", fps) end -- Get bitrate - try multiple property names local bitrate = mp.get_property_number("video-bitrate") or mp.get_property_number("bitrate") local bitrate_str = "unknown" if bitrate and bitrate > 0 then bitrate_str = string.format("%.0f", bitrate) end -- Get video codec - try multiple property names local video_codec = mp.get_property("video-codec") or mp.get_property("video-params/codec") or "unknown" -- Get audio codec - try multiple property names local audio_codec = mp.get_property("audio-codec") or mp.get_property("audio-params/codec") or "unknown" 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 = video_codec, audio_format = audio_codec, resolution = resolution, fps = fps_str, bitrate = bitrate_str, } log("Video info gathered: " .. video_info.title) log("Resolution: " .. resolution .. ", FPS: " .. fps_str .. ", Bitrate: " .. bitrate_str) -- Debug: log all available video properties if options.enable_debug_logging then log("Debug - Available properties:") log(" video-params/w: " .. tostring(mp.get_property("video-params/w"))) log(" video-params/h: " .. tostring(mp.get_property("video-params/h"))) log(" video-params/width: " .. tostring(mp.get_property("video-params/width"))) log(" video-params/height: " .. tostring(mp.get_property("video-params/height"))) log(" video-params/fps: " .. tostring(mp.get_property("video-params/fps"))) log(" fps: " .. tostring(mp.get_property("fps"))) log(" video-bitrate: " .. tostring(mp.get_property("video-bitrate"))) log(" bitrate: " .. tostring(mp.get_property("bitrate"))) log(" video-codec: " .. tostring(mp.get_property("video-codec"))) log(" video-params/codec: " .. tostring(mp.get_property("video-params/codec"))) log(" audio-codec: " .. tostring(mp.get_property("audio-codec"))) log(" audio-params/codec: " .. tostring(mp.get_property("audio-params/codec"))) end end -- Manual refresh function for video info local function refresh_video_info() if current_session and is_tracking then log("Manually refreshing video info...") gather_video_info() -- Update session with new info if video_info.resolution ~= "unknown" then current_session.resolution = video_info.resolution end if video_info.fps ~= "unknown" then current_session.fps = video_info.fps end if video_info.bitrate ~= "unknown" then current_session.bitrate = video_info.bitrate end -- Save updated session save_session_to_file() if options.export_csv then save_session_to_csv() end log("Video info refreshed and session updated") 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 -- 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() -- Also gather again after a delay to ensure properties are loaded mp.add_timeout(options.video_info_refresh_delay, function() if current_session and is_tracking then gather_video_info() log("Video info updated after delay") end end) -- Determine session title based on options local session_title = video_info.title if options.use_filename then session_title = video_info.filename 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() -- Save to CSV when starting session if options.export_csv then save_session_to_csv() end 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 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() -- If we have an active session, refresh video info if current_session and is_tracking then mp.add_timeout(options.video_info_refresh_delay, function() if current_session and is_tracking then gather_video_info() log("Video info refreshed after file load") end end) end end 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 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 -- 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(" Video info refresh delay: " .. options.video_info_refresh_delay .. " seconds") log(" Debug logging: " .. (options.enable_debug_logging and "enabled" or "disabled")) ensure_data_directory() -- Register keybindings mp.remove_key_binding("toggle-clipboard-insertion") mp.add_key_binding(options.start_tracking_key, "immersion_tracking", toggle_tracking) mp.add_key_binding("ctrl+r", "refresh_video_info", refresh_video_info) -- 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 seek event for milestone notifications mp.register_event("seek", on_seek) log("Event handlers registered successfully") log("Press " .. options.start_tracking_key .. " to start/stop immersion tracking") log("Press Ctrl+R to manually refresh video info") end -- Start the script init()