diff --git a/README.md b/README.md index 600f98c..1a8386f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ An XAPI integrated VLC player ### Setup +#### Linux/MacOS/WSL 0) (optional) `make configure` will setup the configuration files for the plugin. to override defaults, run the command with the appropriate overrides. Example being: `make configure THRESHOLD=0.87`. The following is a list of variables and what they mean. - `THRESHOLD` - Decimal value between 0 and 1 that represents point in video considered a completion. Once a video reaches this threshold, the plugin will issue a completion statement - `API_KEY` - API key for LRS @@ -20,11 +21,17 @@ An XAPI integrated VLC player - API Secret: Secret for LRS - API Endpoint: Endpoint for LRS (example: https://localhost:8080/xapi) -4) Play a video of your choice and verify the data is flowing from the LRS. +4) Play a video of your choice and verify the data is flowing from an LRS. + +#### Windows +1) double click the installation script located at .\scripts\windows\install.bat +2) (optional) fill out the fields appropriately +3) Open VLC, click the view dropdown and click 'xAPI Integration.' If you did step 2 those fields should be filled out already. +4) Play a video of your choice and verify the data is flowing from an LRS. ### Dev -All code is located at `xapi.lua`. In VLC you can go to the console log (via ctrl+M) to see what the code is doing. If you wish to update the plugin, just save your changes to `xapi.lua` and run `make install` again. +- All code is located at `xapi.lua`. In VLC you can go to the console log (via ctrl+M) to see what the code is doing. If you wish to update the plugin, just save your changes to `xapi.lua` and run `make install` again. ### License diff --git a/scripts/install.sh b/scripts/install.sh index 9f5c253..1fc7695 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,4 +1,10 @@ #!/bin/bash +source "$(dirname "$0")/get-config-dir.sh" + +# copy template into config directory +TARGET_TEMPLATE_PATH="$(get_vlc_config_directory)xapi.json.template" +SOURCE_TEMPLATE="templates/xapi.json.template" +cp "$SOURCE_TEMPLATE" "$TARGET_TEMPLATE_PATH" # Set the source file path SOURCE_FILE="xapi.lua" diff --git a/scripts/windows/get-config-dir.ps1 b/scripts/windows/get-config-dir.ps1 new file mode 100644 index 0000000..55bc06f --- /dev/null +++ b/scripts/windows/get-config-dir.ps1 @@ -0,0 +1,32 @@ +# Define a function to get the VLC configuration directory +function Get-VLCConfigDirectory { + # Get the APPDATA environment variable + $configDir = "$env:APPDATA\vlc\" + + # Check if the directory path is defined + if (-not $configDir) { + Write-Error "Could not determine VLC configuration directory." + return $null + } + + # Ensure the directory exists; create it if it doesn't + if (-not (Test-Path -Path $configDir)) { + try { + New-Item -ItemType Directory -Path $configDir -Force | Out-Null + } catch { + Write-Error "Failed to create directory: $configDir" + return $null + } + } + + # Output the config directory path + return $configDir +} + +# Call the function and store the result in a variable +$configDir = Get-VLCConfigDirectory + +# Display the configuration directory path if successful +if ($configDir) { + Write-Output "VLC configuration directory: $configDir" +} \ No newline at end of file diff --git a/scripts/windows/install.bat b/scripts/windows/install.bat new file mode 100644 index 0000000..884bb65 --- /dev/null +++ b/scripts/windows/install.bat @@ -0,0 +1,11 @@ +@echo off +:: Check if PowerShell is available +where powershell >nul 2>&1 +if %errorlevel% neq 0 ( + echo PowerShell is not installed on this system. + exit /b 1 +) + +:: Call the PowerShell script with bypassed execution policy +powershell.exe -ExecutionPolicy Bypass -File "%~dp0ps-configure.ps1" +powershell.exe -ExecutionPolicy Bypass -File "%~dp0ps-install.ps1" \ No newline at end of file diff --git a/scripts/windows/ps-configure.ps1 b/scripts/windows/ps-configure.ps1 new file mode 100644 index 0000000..a2f0ead --- /dev/null +++ b/scripts/windows/ps-configure.ps1 @@ -0,0 +1,102 @@ +# Load the get-config-dir.ps1 script +. "$PSScriptRoot\get-config-dir.ps1" + +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +# Function to create the input form +function Show-ConfigForm { + $form = New-Object System.Windows.Forms.Form + $form.Text = "Configuration" + $form.Size = New-Object System.Drawing.Size(400, 300) + $form.StartPosition = "CenterScreen" + + # Labels and text boxes + $labels = @("API Key:", "API Secret:", "API Endpoint:", "Threshold:", "Homepage:") + $inputs = @{} + $yPos = 20 + + foreach ($labelText in $labels) { + # Create label + $label = New-Object System.Windows.Forms.Label + $label.Text = $labelText + $label.Location = New-Object System.Drawing.Point(10, $yPos) + $label.AutoSize = $true + $form.Controls.Add($label) + + # Create text box + $textBox = New-Object System.Windows.Forms.TextBox + $textBox.Size = New-Object System.Drawing.Size(250, 20) + $textBox.Location = New-Object System.Drawing.Point(120, $yPos) + $form.Controls.Add($textBox) + + # Store text box in a dictionary + $inputs[$labelText] = $textBox + $yPos += 40 + } + + # OK button + $okButton = New-Object System.Windows.Forms.Button + $okButton.Text = "OK" + $okButton.Location = New-Object System.Drawing.Point(150, $yPos) + $okButton.Add_Click({ + $form.DialogResult = [System.Windows.Forms.DialogResult]::OK + $form.Close() + }) + $form.Controls.Add($okButton) + + # Show the form and get the result + if ($form.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { + return @{ + ApiKey = $inputs["API Key:"].Text + ApiSecret = $inputs["API Secret:"].Text + ApiEndpoint = $inputs["API Endpoint:"].Text + Threshold = $inputs["Threshold:"].Text + Homepage = $inputs["Homepage:"].Text + } + } + return $null +} + +# Function to delete a file if it exists +function Delete-IfExists { + param ([string]$FilePath) + if (Test-Path -Path $FilePath) { + Write-Output "File exists: $FilePath" + Remove-Item -Path $FilePath -Force + Write-Output "File deleted: $FilePath" + } else { + Write-Output "File does not exist: $FilePath" + } +} + +# Get user input via form +$userInput = Show-ConfigForm +if (-not $userInput) { + Write-Output "Configuration canceled." + exit +} + +# Get VLC config directory +$configDir = Get-VLCConfigDirectory +if (-not $configDir) { + Write-Error "Failed to retrieve VLC configuration directory." + exit 1 +} + +# Define config files +$xapiConfigFile = Join-Path -Path $configDir -ChildPath "xapi-extension-config.txt" +$thresholdConfigFile = Join-Path -Path $configDir -ChildPath "xapi-threshold-config.txt" + +# Delete existing config files +Delete-IfExists -FilePath $xapiConfigFile +Delete-IfExists -FilePath $thresholdConfigFile + +# Write user inputs to the appropriate config files +if ($userInput.ApiKey) { Add-Content -Path $xapiConfigFile -Value "api_key = $($userInput.ApiKey)" } +if ($userInput.ApiSecret) { Add-Content -Path $xapiConfigFile -Value "api_secret = $($userInput.ApiSecret)" } +if ($userInput.ApiEndpoint) { Add-Content -Path $xapiConfigFile -Value "api_endpoint = $($userInput.ApiEndpoint)" } +if ($userInput.Threshold) { Add-Content -Path $thresholdConfigFile -Value "threshold = $($userInput.Threshold)" } +if ($userInput.Homepage) { Add-Content -Path $xapiConfigFile -Value "api_homepage = $($userInput.Homepage)" } + +Write-Output "Config written to $xapiConfigFile and threshold written to $thresholdConfigFile." \ No newline at end of file diff --git a/scripts/windows/ps-install.ps1 b/scripts/windows/ps-install.ps1 new file mode 100644 index 0000000..3952473 --- /dev/null +++ b/scripts/windows/ps-install.ps1 @@ -0,0 +1,62 @@ +# Load the Get-VLCConfigDirectory function from get-config-dir.ps1 +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir\get-config-dir.ps1" + +# Get the VLC configuration directory +$configDir = Get-VLCConfigDirectory +if (-not $configDir) { + Write-Error "Failed to retrieve VLC configuration directory." + exit 1 +} + +# Get the project root directory +$projectRoot = Split-Path -Parent (Split-Path -Parent $scriptDir) + +# Define the source template and target paths +$sourceTemplate = "$projectRoot\templates\xapi.json.template" +$targetTemplatePath = Join-Path -Path $configDir -ChildPath "xapi.json.template" + +# Copy the template to the config directory +try { + Copy-Item -Path $sourceTemplate -Destination $targetTemplatePath -Force + Write-Output "Template copied to: $targetTemplatePath" +} catch { + Write-Error "Failed to copy template: $($_.Exception.Message)" + exit 1 +} + +# Define the source Lua file path +$sourceFile = "$projectRoot\xapi.lua" + +# Check if the source Lua file exists +if (-not (Test-Path -Path $sourceFile)) { + Write-Error "Error: $sourceFile not found." + exit 1 +} + +# Define the target directory for VLC extensions +$targetDir = Join-Path -Path "$env:APPDATA\vlc" -ChildPath "lua\extensions" + +# Ensure the target directory exists +if (-not (Test-Path -Path $targetDir)) { + try { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + Write-Output "Created VLC extensions directory at: $targetDir" + } catch { + Write-Error "Failed to create VLC extensions directory: $($_.Exception.Message)" + exit 1 + } +} + +# Copy the Lua file to the target directory +try { + Copy-Item -Path $sourceFile -Destination $targetDir -Force + Write-Output "xapi.lua has been successfully copied to $targetDir. Restart VLC to enable extension." +} catch { + Write-Error "Error: Failed to copy xapi.lua" + exit 1 +} + +# Pause at the end to allow the user to review logs +Add-Type -AssemblyName PresentationFramework +[System.Windows.MessageBox]::Show("xapi.lua has been successfully copied to $targetDir. Restart VLC to enable extension.", "xAPI VLC Installer") | Out-Null \ No newline at end of file diff --git a/templates/xapi.json.template b/templates/xapi.json.template new file mode 100644 index 0000000..3660b01 --- /dev/null +++ b/templates/xapi.json.template @@ -0,0 +1,24 @@ +{ + "actor": { + "account": { + "homePage": #API_HOMEPAGE, + "name": #API_USERID + }, + "objectType": "Agent" + }, + "verb": { + "id": #VERB + }, + "object": { + "id": #OBJECT, + "objectType": "Activity" + }, + "result": { + "extensions": { + #DURATION_URL: #DURATION, + #PROGRESS_URL: #PROGRESS, + #STATUS_URL: #STATUS, + #CURRENT_TIME_URL: #CURRENT_TIME + } + } +} diff --git a/xapi.lua b/xapi.lua index c10efcf..31ae092 100644 --- a/xapi.lua +++ b/xapi.lua @@ -18,18 +18,20 @@ local config_file_path = "" local threshold_file_path = "" local threshold = 0.9 local is_completed = false +local statement_template = "" -- *************** Events ************ function activate() api_userid = get_uid() config_file_path = get_vlc_config_directory() .. "xapi-extension-config.txt" - threshold_file_path = get_vlc_config_directory() .. "xapi-threshold-config.txt" + threshold_file_path = get_vlc_config_directory() .. "xapi-threshold-config.txt" load_config(config_file_path) load_threshold_config(threshold_file_path) vlc.msg.info("threshold value is: "..threshold) vlc.msg.info("config_file_path: "..config_file_path) vlc.msg.info("UID is: " .. api_userid) show_api_settings_dialog() + statement_template = read_template() if socket and http then vlc.msg.info("LuaSocket and socket.http are available!") @@ -68,6 +70,11 @@ function trim(s) return (s:gsub("^%s*(.-)%s*$", "%1")) end +-- necessary to deal with \ appearing in Windows usernames +function sanitize(s) + return string.gsub(trim(s),"\\", "") +end + function get_uid() local command = 'whoami ' local handle = io.popen(command) @@ -76,7 +83,7 @@ function get_uid() if not username then return "" else - return trim(username) + return sanitize(username) end end @@ -221,6 +228,18 @@ end -- *************** Hook ************ +-- URL Encode function, based on the following gist: https://gist.github.com/liukun/f9ce7d6d14fa45fe9b924a3eed5c3d99 +function urlencode(url) + local char_to_hex = function(c) + return string.format("%%%02X", string.byte(c)) + end + + url = url:gsub("\n", "\r\n") + url = url:gsub("([^%w ])", char_to_hex) + url = url:gsub(" ", "+") + return url +end + -- Function to retrieve metadata and send it off function send_metadata(input, status) if not input then @@ -247,7 +266,7 @@ function send_metadata(input, status) vlc.msg.info("Current Time: " .. current_time .. " seconds") vlc.msg.info("Current Position: " .. (position * 100) .. "%") - local statement = form_statement({title = title, + local statement = form_statement({title = urlencode(title), status = status, duration = tostring(duration), current_time = tostring(current_time), @@ -307,6 +326,32 @@ end -- *************** xAPI Statement ************ +-- function for reading template into a string +function read_template() + local template_file_path = get_vlc_config_directory() .. "xapi.json.template" + local file = io.open(template_file_path, "r") + if not file then + vlc.msg.warn("Missing template file at: " .. template_file_path) + return + end + + local content = file:read("*all") + file:close() + + return content +end + +-- function for inserting data into template +function fill_template(data, template) + + -- Replace placeholders with data values + local result = template:gsub("#([%w_]+)", function(key) + return '"' .. (data[key] or "") .. '"' + end) + + return result +end + function form_statement(args) local title = args.title local status = args.status @@ -330,33 +375,23 @@ function form_statement(args) local current_time_url = extension_url .. "currentTime" local status_url = extension_url .. "status" - -- Manually construct the JSON string with results - local json_statement = - '{' .. - '"actor": {' .. - '"account": {' .. - '"homePage": "' .. api_homepage .. '",' .. - '"name": "' .. api_userid .. '"' .. - '},' .. - '"objectType": "Agent"' .. - '},' .. - '"verb": {' .. - '"id": "' .. verb .. '"' .. - '},' .. - '"object": {' .. - '"id": "' .. object .. '",' .. - '"objectType": "Activity"' .. - '},' .. - '"result": {' .. - '"extensions": {' .. - '"' .. duration_url .. '": ' .. duration .. ',' .. - '"' .. progress_url .. '": ' .. progress .. ',' .. - '"' .. status_url .. '": "' .. status .. '",' .. - '"' .. current_time_url .. '": ' .. current_time .. - '}' .. - '}' .. - '}' - return json_statement + -- form a template table for insertion + local template_table = { + API_HOMEPAGE = api_homepage, + API_USERID = api_userid, + VERB = verb, + OBJECT = object, + DURATION_URL = duration_url, + DURATION = duration, + PROGRESS_URL = progress_url, + PROGRESS = progress, + STATUS_URL = status_url, + STATUS = status, + CURRENT_TIME_URL = current_time_url, + CURRENT_TIME = current_time + } + local statement = fill_template(template_table, statement_template) + return statement end -- *************** Rest Client ************ @@ -378,13 +413,25 @@ end function post_request(json_body) -- Encode API key and secret as Base64 for Basic Auth local auth = "Basic " .. base64_encode(api_key .. ":" .. api_secret) - -- Construct the curl command to make the HTTP POST request + -- Construct the curl command to make the HTTP POST request_sync + local command = "" + - local command = 'curl -X POST ' .. api_endpoint .. '/statements ' + if package.config:sub(1,1) == '/' then + -- Linux/MacOS + command = 'curl -X POST ' .. api_endpoint .. '/statements ' .. '-H "Content-Type: application/json" ' .. '-H "Authorization: ' .. auth .. '" ' .. '-H "X-Experience-API-Version: 1.0.3" ' .. '-d \'' .. json_body .. '\'' + else + -- Windows + command = 'curl.exe -X POST ' .. api_endpoint .. '/statements ' + .. '-H "Content-Type: application/json" ' + .. '-H "Authorization: ' .. auth .. '" ' + .. '-H "X-Experience-API-Version: 1.0.3" ' + .. '-d \"' .. json_body .. '\"' + end vlc.msg.info("command: " .. command)