initial commit

This commit is contained in:
2025-08-19 00:09:13 -07:00
commit 490a182f72
5 changed files with 1032 additions and 0 deletions

424
main.lua Normal file
View 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()