Add Windows card reader, code-signing helpers and playlist diagnostics

- windows/win_card_reader.py: Windows-native card reader via the Raw Input API
  with a low-level keyboard-hook fallback.
- windows/sign_exe.ps1: sign the built executable with a .pfx certificate.
- windows/create_self_signed_cert.ps1: generate a self-signed cert for local
  testing (not trusted by Smart App Control).
- windows/verify_sendinput_fix.py: verification helper for the SendInput
  foreground-unlock fix in run_win.py.
- documentation/CODE_SIGNING_SMART_APP_CONTROL.md: signing guidance.
- working_files/execute_playlist_retrieve.py,
  working_files/raw_server_playlist.json: playlist retrieval diagnostics.

Note: windows/archive_list.txt and windows/build_last.txt are build output and
were committed by request rather than by convention.
This commit is contained in:
ske087
2026-09-10 16:44:12 +03:00
parent a0704efa3c
commit 5d9aa02c07
7 changed files with 1371 additions and 0 deletions
@@ -0,0 +1,108 @@
# Kiwy Signage Player — Code Signing & Smart App Control (Production)
> **TL;DR:** If production PCs have **Smart App Control (SAC) ON** and you
> cannot disable it, the player `.exe` **must be signed by a certificate from a
> reputable public CA**. There is no other way — SAC blocks unsigned binaries at
> the kernel level (no "Run anyway" button). Self-signed certs and Defender
> exclusions do **not** satisfy SAC.
---
## 1. Why Smart App Control blocks the app
- SAC (Windows 11 22H2+, "Smart App Control" in **Windows Security → App &
browser control**) only runs apps that are **signed by a reputable publisher**.
- Your locally-built `KiwySignagePlayer.exe` is **unsigned**
(`Get-AuthenticodeSignature``NotSigned`), so SAC refuses to launch it and
shows "An Application Control policy has blocked this file."
- Unlike classic SmartScreen, SAC has **no "Run anyway" button** and cannot be
bypassed per-file. Disabling SAC is **permanent** and only possible with admin
rights — so it is not viable for locked-down production PCs.
---
## 2. The solution for production: a real code-signing certificate
1. **Buy an OV code-signing certificate** from a reputable CA, e.g.:
- Sectigo Code Signing
- SSL.com Code Signing
- DigiCert Code Signing
- GlobalSign Code Signing
OV is sufficient for SAC; EV gives the highest trust level. Cost is roughly
USD 100300/yr. The CA will issue a `.pfx`/`.p12` (or `.cer`+key).
2. **Sign the exe** after each build. Place your pfx at
`windows\kiwy_signing.pfx` (or set `KIWY_SIGN_PFX` env var) — `build_win.bat`
will then auto-sign via `sign_exe.ps1`:
```powershell
# One-off, from the windows\ folder:
.\sign_exe.ps1 -CertPath "C:\certs\mycodesign.pfx" -CertPassword "yourpwd"
```
The script:
- locates `signtool.exe` (Windows SDK) — install with
`winget install Microsoft.WindowsSDK.10.0.26100` if missing,
- signs with **SHA256** + **RFC3161 timestamp** (required for SAC and to
keep the signature valid after the cert expires),
- verifies the result with `Get-AuthenticodeSignature`.
3. **Test** — confirm on one production PC:
```powershell
Get-AuthenticodeSignature "dist\KiwySignagePlayer\KiwySignagePlayer.exe"
# Status must be: Valid
```
---
## 3. Dev / test machines (where you have admin rights)
If a test PC has SAC **off**, you can make the app trusted locally without
buying a cert:
```powershell
# Run as Administrator
.\create_self_signed_cert.ps1
```
This creates a self-signed code-signing cert, exports `kiwy_dev_signing.pfx`,
and installs it into **Trusted Root + Trusted Publisher + Trusted People** for
the current user, so the player runs without SmartScreen/Defender prompts on
that dev PC.
⚠️ **This does NOT satisfy SAC.** It is only for machines where SAC is off or
where you have admin rights.
---
## 4. Build → sign → verify workflow
```bat
:: 1. Build (produces dist\KiwySignagePlayer\KiwySignagePlayer.exe)
cd windows
venv\Scripts\python.exe -m PyInstaller build.spec --clean --noconfirm
:: 2. Sign (auto if kiwy_signing.pfx present, else manual)
.\sign_exe.ps1 -CertPath "C:\certs\mycodesign.pfx" -CertPassword "..."
:: 3. Verify
Get-AuthenticodeSignature "dist\KiwySignagePlayer\KiwySignagePlayer.exe"
```
`build_win.bat` now does step 1 + step 2 automatically when a pfx is present.
---
## 5. Important caveats
- **Timestamping is mandatory.** The sign script timestamps by default
(`http://timestamp.digicert.com`). Without a timestamp, the signature becomes
invalid once the certificate expires and SAC will block the app.
- **SAC reputation takes time.** Even a validly signed exe from a brand-new
certificate may be blocked until the CA's reputation builds. EV certificates
and well-known CAs (DigiCert, Sectigo, SSL.com) pass immediately.
- **Re-sign after every build.** PyInstaller creates a new exe each time; the
old signature is lost. The auto-sign step in `build_win.bat` handles this.
- **Do not use UPX** on the signed exe — it invalidates the signature and can
trigger false positives. (`upx=True` in the spec currently does nothing
because UPX is not installed; if you ever install UPX, set it to False.)
+110
View File
@@ -0,0 +1,110 @@
<#
================================================================================
Kiwy Signage Player - Self-Signed Certificate Creator
================================================================================
Creates a self-signed code-signing certificate for DEV/TEST machines.
IMPORTANT (please read before running):
A self-signed certificate, even when trusted locally, does NOT satisfy
Smart App Control (SAC). SAC only trusts reputable public CAs. This script
is therefore ONLY for development / test PCs where you have admin rights
and where SAC is either OFF or the app is run with SAC disabled.
For production PCs (SAC ON, no admin), you MUST buy a code-signing cert
from a public CA (Sectigo/SSL.com/DigiCert/GlobalSign) and use
sign_exe.ps1 with that .pfx.
What this does:
1. Creates a self-signed code-signing cert in the Current User store
(never expires, exportable so you can move it to the build machine).
2. Exports it to a .pfx (password-protected) so build_win.bat can sign.
3. Asks if you want to trust it on THIS machine (installs to Root + Trusted
Publisher + Trusted People) so the player runs without SmartScreen/
Defender prompts on this dev PC.
Usage (run as Administrator):
.\create_self_signed_cert.ps1
.\create_self_signed_cert.ps1 -CertName "Kiwy Signage Dev" -PfxPassword "ChangeMe!1" -ExportPath "C:\certs\kiwy_dev.pfx"
================================================================================
#>
[CmdletBinding()]
param(
[string]$CertName = 'Kiwy Signage Player (Dev)',
[string]$Subject = 'CN=Kiwy Signage Player (Dev)',
[string]$PfxPassword = 'KiwySignage2026!',
[string]$ExportPath = (Join-Path $PSScriptRoot 'kiwy_dev_signing.pfx'),
[switch]$SkipTrust
)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
# ── 1. Create the self-signed code-signing certificate ──────────────
Write-Host "[STEP] Creating self-signed code-signing certificate..." -ForegroundColor Cyan
$cert = New-SelfSignedCertificate `
-Subject $Subject `
-FriendlyName $CertName `
-Type CodeSigningCert `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-KeyExportPolicy Exportable `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-NotAfter (Get-Date).AddYears(10)
if (-not $cert) {
Write-Host "[ERROR] Failed to create certificate." -ForegroundColor Red
exit 1
}
Write-Host "[OK ] Created cert: $($cert.Subject) thumbprint=$($cert.Thumbprint)" -ForegroundColor Green
# ── 2. Export to PFX ─────────────────────────────────────────────────
Write-Host "[STEP] Exporting to PFX: $ExportPath" -ForegroundColor Cyan
$securePwd = ConvertTo-SecureString -String $PfxPassword -Force -AsPlainText
try {
Export-PfxCertificate -Cert $cert -FilePath $ExportPath -Password $securePwd -Force | Out-Null
Write-Host "[OK ] PFX written: $ExportPath" -ForegroundColor Green
} catch {
Write-Host "[WARN ] Could not export PFX (still usable from cert store): $($_.Exception.Message)" -ForegroundColor Yellow
}
# ── 3. Trust it on THIS machine (Root + Trusted Publisher) ───────────
if (-not $SkipTrust) {
Write-Host "[STEP] Installing to Trusted Root + Trusted Publisher (requires admin)..." -ForegroundColor Cyan
try {
$rootStore = New-Object System.Security.Cryptography.X509Certificates.X509Store(
'Root', 'CurrentUser')
$rootStore.Open('ReadWrite')
$rootStore.Add($cert)
$rootStore.Close()
$pubStore = New-Object System.Security.Cryptography.X509Certificates.X509Store(
'TrustedPublisher', 'CurrentUser')
$pubStore.Open('ReadWrite')
$pubStore.Add($cert)
$pubStore.Close()
$peopleStore = New-Object System.Security.Cryptography.X509Certificates.X509Store(
'TrustedPeople', 'CurrentUser')
$peopleStore.Open('ReadWrite')
$peopleStore.Add($cert)
$peopleStore.Close()
Write-Host "[OK ] Certificate trusted on this machine." -ForegroundColor Green
} catch {
Write-Host "[WARN ] Trust install failed (run as Administrator): $($_.Exception.Message)" -ForegroundColor Yellow
}
}
Write-Host ""
Write-Host "================ RESULT ================" -ForegroundColor Green
Write-Host "Cert : $($cert.Subject)"
Write-Host "Thumb : $($cert.Thumbprint)"
Write-Host "PFX : $ExportPath (password: $PfxPassword)"
Write-Host ""
Write-Host "To sign the exe with this cert:"
Write-Host " .\sign_exe.ps1 -CertPath `"$ExportPath`" -CertPassword `"$PfxPassword`""
Write-Host ""
Write-Host "REMINDER: This self-signed cert is for DEV ONLY. Production PCs"
Write-Host "with Smart App Control ON need a cert from a public CA."
Write-Host "========================================" -ForegroundColor Green
+156
View File
@@ -0,0 +1,156 @@
<#
================================================================================
Kiwy Signage Player - Code Signing Script
================================================================================
Signs the built KiwySignagePlayer.exe with an Authenticode certificate.
For PRODUCTION PCs that have Smart App Control (SAC) ENABLED:
- The cert MUST be issued by a reputable public CA (e.g. Sectigo, SSL.com,
DigiCert, GlobalSign). Self-signed certs will NOT satisfy SAC.
- You must use this script with -CertPath pointing at your .pfx/.p12.
For DEV/TEST machines where you have admin rights:
- A self-signed cert trusted in the local Root store + Trusted Publisher
works (see create_self_signed_cert.ps1), but it does NOT satisfy SAC.
Usage:
.\sign_exe.ps1 -CertPath "C:\certs\mycodesign.pfx" -CertPassword "secret"
.\sign_exe.ps1 -CertPath "C:\certs\mycodesign.pfx" # prompt for pwd
.\sign_exe.ps1 -CertThumbprint "A1B2..." # from cert store
.\sign_exe.ps1 -CertPath "C:\certs\mycodesign.pfx" -SkipTimestamp $false
Optional:
-TimestampUrl RFC3161 timestamp server (default DigiCert).
Timestamping is REQUIRED for the signature to stay valid
after the cert expires and to satisfy Smart App Control.
-ExePath Path to the exe to sign (default dist\KiwySignagePlayer\KiwySignagePlayer.exe)
-Force Re-sign even if already signed
================================================================================
#>
[CmdletBinding()]
param(
[string]$CertPath,
[string]$CertPassword,
[string]$CertThumbprint,
[string]$ExePath = (Join-Path $PSScriptRoot 'dist\KiwySignagePlayer\KiwySignagePlayer.exe'),
[string]$TimestampUrl = 'http://timestamp.digicert.com',
[switch]$SkipTimestamp,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
function Find-Signtool {
$candidates = @(
(Get-Command signtool.exe -ErrorAction SilentlyContinue).Source,
"$env:ProgramFiles(x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe",
"$env:ProgramFiles(x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe",
"$env:ProgramFiles(x86)\Windows Kits\10\bin\10.0.22000.0\x64\signtool.exe",
"$env:ProgramFiles(x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe"
)
foreach ($c in $candidates) {
if ($c -and (Test-Path $c)) { return $c }
}
# Fallback: newest SDK on disk
$sdkBin = "$env:ProgramFiles(x86)\Windows Kits\10\bin"
if (Test-Path $sdkBin) {
$found = Get-ChildItem $sdkBin -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
if ($found) { return $found }
}
return $null
}
$signtool = Find-Signtool
if ($signtool) {
Write-Host "[INFO ] signtool: $signtool" -ForegroundColor Green
} else {
Write-Host "[WARN ] signtool.exe not found - will use PowerShell Set-AuthenticodeSignature fallback." -ForegroundColor Yellow
Write-Host "[WARN ] NOTE: the fallback cannot apply an RFC3161 timestamp. For production (SAC),"
Write-Host "[WARN ] install the Windows SDK signtool: winget install Microsoft.WindowsSDK.10.0.26100"
}
if (-not (Test-Path $ExePath)) {
Write-Host "[ERROR] Exe not found: $ExePath" -ForegroundColor Red
Write-Host "Run build_win.bat first, or pass -ExePath."
exit 1
}
# Already signed?
$sig = Get-AuthenticodeSignature -FilePath $ExePath
if ($sig.Status -eq 'Valid' -and -not $Force) {
Write-Host "[INFO ] Exe is already validly signed by: $($sig.SignerCertificate.Subject)" -ForegroundColor Green
exit 0
}
# ── Load the certificate ────────────────────────────────────────────
$cert = $null
if ($CertThumbprint) {
$cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Thumbprint -eq $CertThumbprint } | Select-Object -First 1
if (-not $cert) {
Write-Host "[ERROR] No certificate with thumbprint $CertThumbprint in My store." -ForegroundColor Red
exit 1
}
} elseif ($CertPath) {
if (-not (Test-Path $CertPath)) {
Write-Host "[ERROR] Cert file not found: $CertPath" -ForegroundColor Red
exit 1
}
if (-not $CertPassword) {
$secure = Read-Host "Certificate password for $CertPath" -AsSecureString
$CertPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
}
$securePwd = ConvertTo-SecureString -String $CertPassword -Force -AsPlainText
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertPath, $securePwd)
} else {
Write-Host "[ERROR] Provide -CertPath or -CertThumbprint." -ForegroundColor Red
exit 1
}
# ── Sign (signtool preferred, PowerShell fallback) ──────────────────
if ($signtool) {
$args = @()
if ($CertThumbprint) {
$args = @('sign', '/sha1', $CertThumbprint, '/fd', 'SHA256')
} else {
$args = @('sign', '/f', $CertPath, '/p', $CertPassword, '/fd', 'SHA256')
}
if (-not $SkipTimestamp) {
$args += @('/tr', $TimestampUrl, '/td', 'SHA256')
}
$args += @('"' + $ExePath + '"')
$cmd = "& `"$signtool`" " + ($args -join ' ')
Write-Host "[INFO ] Signing with signtool..." -ForegroundColor Cyan
Write-Host "[CMD ] $cmd"
Invoke-Expression $cmd
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] signtool failed with exit code $LASTEXITCODE" -ForegroundColor Red
exit $LASTEXITCODE
}
} else {
Write-Host "[INFO ] Signing with PowerShell Set-AuthenticodeSignature (no timestamp)..." -ForegroundColor Cyan
if (-not $cert.HasPrivateKey) {
Write-Host "[ERROR] Certificate has no private key - cannot sign." -ForegroundColor Red
exit 1
}
$sig = Set-AuthenticodeSignature -FilePath $ExePath -Certificate $cert -HashAlgorithm SHA256
if ($sig.Status -notin @('Valid','UnknownError')) {
Write-Host "[ERROR] Signing failed: $($sig.StatusMessage)" -ForegroundColor Red
exit 1
}
}
# Verify
$sig = Get-AuthenticodeSignature -FilePath $ExePath
Write-Host ""
Write-Host "[INFO ] Signature status: $($sig.Status)" -ForegroundColor Green
Write-Host "[INFO ] Signer: $($sig.SignerCertificate.Subject)" -ForegroundColor Green
if ($sig.Status -eq 'Valid') {
Write-Host "[OK ] KiwySignagePlayer.exe is now digitally signed." -ForegroundColor Green
} else {
Write-Host "[WARN ] Signature status is '$($sig.Status)' - inspect above." -ForegroundColor Yellow
Write-Host "[WARN ] If no timestamp was applied, SAC may still block after cert expiry." -ForegroundColor Yellow
}
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Rigorous verification: is the fixed SendInput code in the compiled run_win?"""
import marshal
import types
import dis
from PyInstaller.archive.readers import CArchiveReader
EXE = r'dist\KiwySignagePlayer\KiwySignagePlayer.exe'
def collect_strings(code, acc):
for c in code.co_consts:
if isinstance(c, str):
acc.append(c)
elif isinstance(c, types.CodeType):
collect_strings(c, acc)
def collect_codes(code, acc):
acc.append(code)
for c in code.co_consts:
if isinstance(c, types.CodeType):
collect_codes(c, acc)
def main():
arc = CArchiveReader(EXE)
data = arc.extract('run_win')
code = marshal.loads(data)
all_codes = []
collect_codes(code, all_codes)
strings = []
collect_strings(code, strings)
# 1) c_ulonglong can ONLY come from the fixed code (old used c_ulong + POINTER)
has_c_ulonglong = any('c_ulonglong' in c.co_names for c in all_codes)
print(f'c_ulonglong in co_names of any code object: {has_c_ulonglong}')
# 2) INPUTUNION should be STORE_DEREF/STORE_NAME'd inside
# _force_foreground_sendinput (classes are defined in a closure,
# so they're stored via STORE_DEREF).
store_names = set()
for c in all_codes:
for instr in dis.get_instructions(c):
if instr.opname in ('STORE_NAME', 'STORE_FAST', 'STORE_DEREF'):
store_names.add(instr.argval)
print(f'INPUTUNION stored: {"INPUTUNION" in store_names}')
print(f'KEYBDINPUT stored: {"KEYBDINPUT" in store_names}')
print(f'MOUSEINPUT stored: {"MOUSEINPUT" in store_names}')
print(f'HARDWAREINPUT stored: {"HARDWAREINPUT" in store_names}')
# 3) The new code uses INPUT() + field assignment (type, u.ki.wVk, u.ki.dwFlags)
names = set()
for c in all_codes:
names.update(c.co_names)
print(f'Has "u" attribute usage: {"u" in names}')
# 4) string markers
print(f'string "undersized buffer": {any("undersized buffer" in s for s in strings)}')
print(f'string "real Win32 x64": {any("real Win32 x64" in s for s in strings)}')
print(f'string "ULONG_PTR": {any("ULONG_PTR" in s for s in strings)}')
# 5) Console-window fix in _find_kivy_hwnd: cellvars/freevars + tuple consts
find_kv = [c for c in all_codes if c.co_name == '_find_kivy_hwnd']
cell_vars = set()
for c in find_kv:
cell_vars.update(c.co_cellvars)
cell_vars.update(c.co_freevars)
enum_cb = [c for c in all_codes
if c.co_name == '_enum_cb' and 'SDL_CLASSES' in c.co_freevars]
tuple_strs = set()
for c in enum_cb:
for x in c.co_consts:
if isinstance(x, tuple):
tuple_strs.update(str(v) for v in x)
print(f'_find_kivy_hwnd cell/free vars (SDL_CLASSES/sdl_windows/our_pid): '
f'{"SDL_CLASSES" in cell_vars and "sdl_windows" in cell_vars and "our_pid" in cell_vars}')
print(f'console class excluded (ConsoleWindowClass in _enum_cb tuple): '
f'{"ConsoleWindowClass" in tuple_strs}')
verdict = has_c_ulonglong and 'INPUTUNION' in store_names
console_fix = ('SDL_CLASSES' in cell_vars and 'ConsoleWindowClass' in tuple_strs)
print(f'\nVerdict SendInput: {"FIX PRESENT" if verdict else "FIX ABSENT - rebuild needed"}')
print(f'Verdict console-hwnd: {"FIX PRESENT" if console_fix else "FIX ABSENT - rebuild needed"}')
if __name__ == '__main__':
main()
+720
View File
@@ -0,0 +1,720 @@
"""
Windows Card Reader (Raw Input API + LL-hook fallback)
=======================================================
Drop-in replacement for the Linux `CardReader` class in `src/main.py`.
The Linux implementation reads keystrokes from `/dev/input/event*` via
`evdev`. On Windows there is no `/dev/input`; instead HID devices (such as
USB card readers that emulate a keyboard) are accessed through the Win32
**Raw Input API** (`WM_INPUT`).
Design
------
* A dedicated hidden message-only window + message pump runs on a background
thread. It registers for Raw Input of all *keyboard* HID devices with
`RIDEV_INPUTSINK`, so it receives `WM_INPUT` even though it never has focus.
* Device selection mirrors the Linux priority logic:
1. A device whose name contains "card" / "reader" / "rfid".
2. A USB HID keyboard that is *not* the PS/2 system keyboard.
3. Any keyboard device (excluding obvious touchscreens/mice).
A config override `card_reader_device` (substring of the device name, e.g.
`VID_08FF`) takes precedence.
* While reading, only key events from the *selected* device are accepted, so a
physical keyboard used for maintenance cannot pollute the card data.
* Card data ends on Enter (`VK_RETURN`), mirroring the Linux behaviour.
* If Raw Input registration fails (or config `card_reader_mode = "hook"`), it
falls back to a low-level keyboard hook (`WH_KEYBOARD_LL`).
Config keys (in `config/app_config.json`):
"card_reader_device": "VID_08FF" # substring of device name (optional)
"card_reader_mode": "auto" # "auto" | "raw" | "hook"
"card_reader_timeout": 5 # seconds (optional)
"""
import ctypes
import ctypes.wintypes as wintypes
import os
import json
import threading
import time
# ── Win32 constants ──────────────────────────────────────────────────────────
WM_INPUT = 0x00FF
WM_INPUT_DEVICE_CHANGE = 0x00FE
WM_KEYDOWN = 0x0100
WM_SYSKEYDOWN = 0x0104
RIM_TYPEMOUSE = 0
RIM_TYPEKEYBOARD = 1
RIM_TYPEHID = 2
RID_INPUT = 0x10000003
RIDI_DEVICENAME = 0x20000007
RIDEV_INPUTSINK = 0x00000100
RIDEV_DEVNOTIFY = 0x00002000
WH_KEYBOARD_LL = 13
HC_ACTION = 0
# Virtual keys we never treat as card data
_VK_SHIFT = 0x10
_VK_CONTROL = 0x11
_VK_MENU = 0x12
_VK_CAPITAL = 0x14
_VK_ESCAPE = 0x1B
_VK_RETURN = 0x0D
_VK_TAB = 0x09
_VK_LSHIFT = 0xA0
_VK_RSHIFT = 0xA1
_VK_LCONTROL = 0xA2
_VK_RCONTROL = 0xA3
_VK_LMENU = 0xA4
_VK_RMENU = 0xA5
# ── ctypes structures ───────────────────────────────────────────────────────
# ctypes.wintypes does not export LRESULT; it is a signed pointer-sized value.
# Use ctypes.c_long (standard ctypes workaround; fine for WNDPROC/LL-hook).
_LRESULT = ctypes.c_long
_WND_PROC = ctypes.WINFUNCTYPE(
_LRESULT, wintypes.HWND, wintypes.UINT,
wintypes.WPARAM, wintypes.LPARAM,
)
_LL_KEYBOARD_PROC = ctypes.WINFUNCTYPE(
_LRESULT, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM,
)
class _WNDCLASSW(ctypes.Structure):
_fields_ = [
('style', wintypes.UINT),
('lpfnWndProc', _WND_PROC),
('cbClsExtra', ctypes.c_int),
('cbWndExtra', ctypes.c_int),
('hInstance', wintypes.HINSTANCE),
('hIcon', wintypes.HICON),
('hCursor', ctypes.c_void_p), # HCURSOR (not exported by wintypes)
('hbrBackground', wintypes.HBRUSH),
('lpszMenuName', wintypes.LPCWSTR),
('lpszClassName', wintypes.LPCWSTR),
]
class _MSG(ctypes.Structure):
_fields_ = [
('hwnd', wintypes.HWND),
('message', wintypes.UINT),
('wParam', wintypes.WPARAM),
('lParam', wintypes.LPARAM),
('time', wintypes.DWORD),
('pt', wintypes.POINT),
]
class _RAWINPUTDEVICE(ctypes.Structure):
_fields_ = [
('usUsagePage', wintypes.USHORT),
('usUsage', wintypes.USHORT),
('dwFlags', wintypes.DWORD),
('hwndTarget', wintypes.HWND),
]
class _RAWINPUTDEVICELIST(ctypes.Structure):
_fields_ = [
('hDevice', wintypes.HANDLE),
('dwType', wintypes.DWORD),
]
class _RAWINPUTHEADER(ctypes.Structure):
_fields_ = [
('dwType', wintypes.DWORD),
('dwSize', wintypes.DWORD),
('hDevice', wintypes.HANDLE),
('wParam', wintypes.WPARAM),
]
class _RAWKEYBOARD(ctypes.Structure):
_fields_ = [
('MakeCode', wintypes.USHORT),
('Flags', wintypes.USHORT),
('Reserved', wintypes.USHORT),
('VKey', wintypes.USHORT),
('Message', wintypes.UINT),
('ExtraInformation', wintypes.ULONG),
]
class _RAWINPUT_UNION(ctypes.Union):
_fields_ = [('keyboard', _RAWKEYBOARD)]
class _RAWINPUT(ctypes.Structure):
_fields_ = [
('header', _RAWINPUTHEADER),
('u', _RAWINPUT_UNION),
]
class _KBDLLHOOKSTRUCT(ctypes.Structure):
_fields_ = [
('vkCode', wintypes.DWORD),
('scanCode', wintypes.DWORD),
('flags', wintypes.DWORD),
('time', wintypes.DWORD),
('dwExtraInfo', wintypes.WPARAM),
]
# ── Win32 function bindings with explicit signatures ────────────────────────
_user32 = ctypes.windll.user32
_kernel32 = ctypes.windll.kernel32
_user32.RegisterClassW.argtypes = [ctypes.POINTER(_WNDCLASSW)]
_user32.RegisterClassW.restype = wintypes.ATOM
_user32.CreateWindowExW.argtypes = [
wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD,
ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID,
]
_user32.CreateWindowExW.restype = wintypes.HWND
_user32.DestroyWindow.argtypes = [wintypes.HWND]
_user32.UnregisterClassW.argtypes = [wintypes.LPCWSTR, wintypes.HINSTANCE]
_user32.GetMessageW.argtypes = [
ctypes.POINTER(_MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT,
]
_user32.GetMessageW.restype = wintypes.BOOL
_user32.TranslateMessage.argtypes = [ctypes.POINTER(_MSG)]
_user32.DispatchMessageW.argtypes = [ctypes.POINTER(_MSG)]
_user32.DefWindowProcW.argtypes = [
wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM,
]
_user32.DefWindowProcW.restype = _LRESULT
_user32.PostMessageW.argtypes = [
wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM,
]
_user32.PostMessageW.restype = wintypes.BOOL
_user32.RegisterRawInputDevices.argtypes = [
ctypes.POINTER(_RAWINPUTDEVICE), wintypes.UINT, wintypes.UINT,
]
_user32.RegisterRawInputDevices.restype = wintypes.BOOL
_user32.GetRawInputDeviceList.argtypes = [
ctypes.POINTER(_RAWINPUTDEVICELIST), ctypes.POINTER(wintypes.UINT),
wintypes.UINT,
]
_user32.GetRawInputDeviceList.restype = wintypes.UINT
_user32.GetRawInputDeviceInfoW.argtypes = [
wintypes.HANDLE, wintypes.UINT, wintypes.LPVOID,
ctypes.POINTER(wintypes.UINT),
]
_user32.GetRawInputDeviceInfoW.restype = wintypes.UINT
_user32.GetRawInputData.argtypes = [
wintypes.HANDLE, wintypes.UINT, wintypes.LPVOID,
ctypes.POINTER(wintypes.UINT), wintypes.UINT,
]
_user32.GetRawInputData.restype = wintypes.UINT
_user32.MapVirtualKeyW.argtypes = [wintypes.UINT, wintypes.UINT]
_user32.MapVirtualKeyW.restype = wintypes.UINT
_user32.SetWindowsHookExW.argtypes = [
ctypes.c_int, _LL_KEYBOARD_PROC, wintypes.HINSTANCE, wintypes.DWORD,
]
_user32.SetWindowsHookExW.restype = wintypes.HHOOK
_user32.UnhookWindowsHookEx.argtypes = [wintypes.HHOOK]
_user32.UnhookWindowsHookEx.restype = wintypes.BOOL
_user32.CallNextHookEx.argtypes = [
wintypes.HHOOK, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM,
]
_user32.CallNextHookEx.restype = _LRESULT
_kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]
_kernel32.GetModuleHandleW.restype = wintypes.HINSTANCE
_WND_CLASS_NAME = 'KiwyWinCardReaderWindow'
# The single active reader instance (the Raw Input / LL-hook callbacks are
# invoked from a background thread; a module-level ref avoids GC of the proc).
_ACTIVE_READER = None
# ── Small helpers ───────────────────────────────────────────────────────────
def _log(msg):
"""Log via Kivy Logger when available, else print."""
try:
from kivy.logger import Logger
Logger.info(f"CardReaderWin: {msg}")
except Exception:
try:
print(f"[CardReaderWin] {msg}")
except Exception:
pass
def _get_config():
"""Read app_config.json (next to exe, or project config in dev mode)."""
try:
data_dir = os.environ.get('KIWY_DATA_DIR', '')
candidates = []
if data_dir:
candidates.append(os.path.join(data_dir, 'config', 'app_config.json'))
# Dev fallback: <project>/config/app_config.json
here = os.path.dirname(os.path.abspath(__file__)) # windows/
candidates.append(os.path.join(os.path.dirname(here), 'config', 'app_config.json'))
for path in candidates:
if path and os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f) or {}
except Exception:
pass
return {}
def _vk_to_char(vk):
"""Convert a virtual-key code to its character, or None if not data."""
# Numeric keypad 0-9
if 0x60 <= vk <= 0x69:
return chr(vk - 0x60 + 0x30)
# Main-row digits
if 0x30 <= vk <= 0x39:
return chr(vk)
# Letters (uppercase) — card readers typically emit these
if 0x41 <= vk <= 0x5A:
return chr(vk)
if vk == 0x20: # space
return ' '
# Anything else (symbols) via MapVirtualKey
try:
res = _user32.MapVirtualKeyW(vk, 2) # MAPVK_VK_TO_CHAR
if res & 0x80000000:
return None # dead key
c = res & 0xFFFF
if 0x20 <= c <= 0x7E:
return chr(c)
except Exception:
pass
return None
def _get_device_name(hdev):
"""Return the Win32 device name (e.g. \\\\?\\HID#VID_08FF&PID_0009#...)."""
try:
size = wintypes.UINT(0)
if _user32.GetRawInputDeviceInfoW(hdev, RIDI_DEVICENAME, None, ctypes.byref(size)) == 0xFFFFFFFF:
return ''
if not size.value:
return ''
buf = ctypes.create_unicode_buffer(int(size.value) + 2)
if _user32.GetRawInputDeviceInfoW(hdev, RIDI_DEVICENAME, buf, ctypes.byref(size)) == 0xFFFFFFFF:
return ''
return buf.value or ''
except Exception:
return ''
def _enumerate_keyboards():
"""Return [{handle, name}] for all Raw-Input keyboard-type devices."""
result = []
try:
count = wintypes.UINT(0)
_user32.GetRawInputDeviceList(None, ctypes.byref(count), ctypes.sizeof(_RAWINPUTDEVICELIST))
if not count.value:
return result
buf = (_RAWINPUTDEVICELIST * count.value)()
n = _user32.GetRawInputDeviceList(buf, ctypes.byref(count), ctypes.sizeof(_RAWINPUTDEVICELIST))
for i in range(int(n)):
dev = buf[i]
if dev.dwType != RIM_TYPEKEYBOARD:
continue
result.append({'handle': dev.hDevice, 'name': _get_device_name(dev.hDevice)})
except Exception as e:
_log(f"enumerate_keyboards error: {e}")
return result
def _choose_card_reader(override=''):
"""Pick the most likely card-reader device, mirroring the Linux logic."""
keyboards = _enumerate_keyboards()
if not keyboards:
_log("No keyboard-type Raw Input devices found")
return None
for dev in keyboards:
_log(f" candidate: {dev['name']}")
# Config override (substring of device name, e.g. VID_08FF)
if override:
for dev in keyboards:
if override.lower() in dev['name'].lower():
_log(f"Using config override -> {dev['name']}")
return dev
_log(f"No device matched config override '{override}'; continuing auto-detect")
exclusion = ('touch', 'mouse', 'trackpad', 'pen', 'stylus', 'monitor', 'video')
# Priority 1: explicit card / reader / rfid
for dev in keyboards:
name = dev['name'].lower()
if 'card' in name or 'reader' in name or 'rfid' in name:
_log(f"Priority 1 (explicit reader) -> {dev['name']}")
return dev
# Priority 2: USB HID keyboard that is NOT the PS/2 system keyboard
for dev in keyboards:
name = dev['name'].lower()
if 'hid' in name and 'vid' in name:
if any(p in name for p in ('pnp0303', 'pnp0c0e', 'pnp0320', 'acpi')):
continue
if any(k in name for k in exclusion):
continue
_log(f"Priority 2 (USB HID keyboard) -> {dev['name']}")
return dev
# Priority 3: any keyboard that isn't obviously a touchscreen/mouse
for dev in keyboards:
name = dev['name'].lower()
if any(k in name for k in exclusion):
continue
_log(f"Priority 3 (any keyboard) -> {dev['name']}")
return dev
_log(f"Fallback: using first keyboard device -> {keyboards[0]['name']}")
return keyboards[0]
# ── WndProc / LL-hook callbacks (called on the pump thread) ─────────────────
def _wnd_proc(hwnd, msg, wparam, lparam):
"""Handle WM_INPUT / WM_INPUT_DEVICE_CHANGE for the hidden window."""
try:
reader = _ACTIVE_READER
if reader is not None:
if msg == WM_INPUT:
reader._on_raw_input(lparam)
elif msg == WM_INPUT_DEVICE_CHANGE:
reader._on_device_change()
return _user32.DefWindowProcW(hwnd, msg, wparam, lparam)
except Exception:
try:
return _user32.DefWindowProcW(hwnd, msg, wparam, lparam)
except Exception:
return 0
def _ll_hook_proc(nCode, wParam, lParam):
"""Low-level keyboard hook used as a fallback capture path."""
try:
if nCode == HC_ACTION:
reader = _ACTIVE_READER
if reader is not None and wParam in (WM_KEYDOWN, WM_SYSKEYDOWN):
kb = _KBDLLHOOKSTRUCT.from_address(lParam)
reader._on_key_event(kb.vkCode)
return _user32.CallNextHookEx(None, nCode, wParam, lParam)
except Exception:
try:
return _user32.CallNextHookEx(None, nCode, wParam, lParam)
except Exception:
return 1
# ── Background message-pump thread ──────────────────────────────────────────
class _RawInputThread(threading.Thread):
def __init__(self, owner):
super().__init__(daemon=True)
self._owner = owner
self._hwnd = None
self._hook = None
self.ready = threading.Event()
def run(self):
try:
hinst = _kernel32.GetModuleHandleW(None)
wndclass = _WNDCLASSW()
wndclass.lpfnWndProc = _WND_PROC(_wnd_proc)
wndclass.hInstance = hinst
wndclass.lpszClassName = _WND_CLASS_NAME
if not _user32.RegisterClassW(ctypes.byref(wndclass)):
raise ctypes.WinError(ctypes.get_last_error(), "RegisterClassW failed")
hwnd = _user32.CreateWindowExW(
0, _WND_CLASS_NAME, 'KiwyWinCardReader', 0,
0, 0, 0, 0, None, None, hinst, None,
)
if not hwnd:
raise ctypes.WinError(ctypes.get_last_error(), "CreateWindowExW failed")
self._hwnd = hwnd
self._owner._hwnd = hwnd
# Register Raw Input for ALL keyboards (sink = receive w/o focus)
rids = (_RAWINPUTDEVICE * 1)()
rids[0].usUsagePage = 0x01
rids[0].usUsage = 0x06 # Generic Desktop / Keyboard
rids[0].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY
rids[0].hwndTarget = hwnd
raw_ok = bool(_user32.RegisterRawInputDevices(
rids, 1, ctypes.sizeof(_RAWINPUTDEVICE)))
self._owner._raw_ok = raw_ok
if raw_ok:
_log("Raw Input registered for keyboard devices")
# LL-hook fallback: use it when config says so, or if raw failed
mode = getattr(self._owner, '_mode', 'auto')
if mode == 'hook' or not raw_ok:
proc = _LL_KEYBOARD_PROC(_ll_hook_proc)
hook = _user32.SetWindowsHookExW(WH_KEYBOARD_LL, proc, hinst, 0)
if hook:
self._hook = hook
self._hook_proc = proc # keep reference alive
_log("Low-level keyboard hook ACTIVE (fallback capture)")
self.ready.set()
msg = _MSG()
while _user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
_user32.TranslateMessage(ctypes.byref(msg))
_user32.DispatchMessageW(ctypes.byref(msg))
if self._hook:
try:
_user32.UnhookWindowsHookEx(self._hook)
except Exception:
pass
_user32.DestroyWindow(hwnd)
_user32.UnregisterClassW(_WND_CLASS_NAME, hinst)
except Exception as e:
_log(f"Message pump thread error: {e}")
self.ready.set()
def stop(self):
try:
if self._hwnd:
_user32.PostMessageW(self._hwnd, 0x0012, 0, 0) # WM_QUIT
except Exception:
pass
# ── Public drop-in replacement for the Linux CardReader ─────────────────────
class WindowsCardReader:
"""Windows-native card reader with the same interface as main.py's CardReader.
Interface used by SignagePlayer:
read_card_async(callback) -> starts listening; callback(card_data) on
Enter, or callback(None) on timeout/cancel
stop_reading() -> stops listening
"""
def __init__(self):
self._device = None
self._device_name = ''
self._reading = False
self._finished = False
self._callback = None
self._card_buffer = []
self._last_activity = 0.0
self._timeout = 5.0
self._mode = 'auto'
self._hwnd = None
self._raw_ok = False
self._thread = None
self._lock = threading.Lock()
self._last_device_change = 0.0 # debounce WM_INPUT_DEVICE_CHANGE
# -- config -------------------------------------------------------
def _load_settings(self):
cfg = _get_config()
self._mode = str(cfg.get('card_reader_mode', 'auto')).lower().strip() or 'auto'
try:
self._timeout = float(cfg.get('card_reader_timeout', 5))
except Exception:
self._timeout = 5.0
if self._timeout <= 0:
self._timeout = 5.0
return str(cfg.get('card_reader_device', '') or '').strip()
# -- public API ---------------------------------------------------
def read_card_async(self, callback):
"""Start reading; callback(card_data) on Enter, callback(None) on timeout."""
if self._reading:
_log("read_card_async called while already reading — ignoring")
return
override = self._load_settings()
global _ACTIVE_READER
_ACTIVE_READER = self
self._callback = callback
self._reading = True
self._finished = False
self._card_buffer = []
self._last_activity = time.time()
# Choose the target device (raw mode only). In 'hook' mode we capture
# from all keyboards via the LL-hook instead.
if self._mode != 'hook':
self._device = None
self._device_name = ''
chosen = _choose_card_reader(override=override)
if chosen:
self._device = chosen['handle']
self._device_name = chosen['name']
_log(f"Selected card reader: {chosen['name']}")
else:
_log("No dedicated device found — will accept any keyboard input")
else:
self._device = None
self._device_name = ''
# Ensure the message pump is running
if self._thread is None or not self._thread.is_alive():
self._thread = _RawInputThread(self)
self._thread.start()
self._thread.ready.wait(timeout=3.0)
# Watchdog for the 5-second timeout
threading.Thread(target=self._timeout_watchdog, daemon=True).start()
_log(f"Waiting for card swipe (mode={self._mode}, timeout={self._timeout}s)")
def stop_reading(self):
"""Stop listening (also stops the timeout watchdog)."""
self._reading = False
def shutdown(self):
"""Stop the background pump thread (call on app exit)."""
self.stop_reading()
global _ACTIVE_READER
if _ACTIVE_READER is self:
_ACTIVE_READER = None
if self._thread is not None:
self._thread.stop()
# -- internals ----------------------------------------------------
def _timeout_watchdog(self):
while self._reading and not self._finished:
if time.time() - self._last_activity > self._timeout:
_log("Read timeout — sending None")
self._finish(None)
return
time.sleep(0.25)
def _on_raw_input(self, lparam):
"""Process a WM_INPUT message (called on the pump thread)."""
try:
size = wintypes.UINT(0)
_user32.GetRawInputData(lparam, RID_INPUT, None, ctypes.byref(size),
ctypes.sizeof(_RAWINPUTHEADER))
if not size.value:
return
buf = ctypes.create_string_buffer(int(size.value))
got = _user32.GetRawInputData(lparam, RID_INPUT, buf, ctypes.byref(size),
ctypes.sizeof(_RAWINPUTHEADER))
if got == 0xFFFFFFFF or got == 0:
return
raw = ctypes.cast(buf, ctypes.POINTER(_RAWINPUT)).contents
if raw.header.dwType != RIM_TYPEKEYBOARD:
return
# Only accept input from the selected card-reader device.
if self._device is not None and raw.header.hDevice != self._device:
return
kb = raw.u.keyboard
if kb.Message in (WM_KEYDOWN, WM_SYSKEYDOWN):
self._on_key_event(kb.VKey)
except Exception as e:
_log(f"_on_raw_input error: {e}")
def _on_device_change(self):
"""A HID device was added/removed — re-select only if the actual
choice changes, and never more than once per second (the initial
registration/enumeration triggers a spurious change event)."""
try:
if not self._reading or self._mode == 'hook':
return
now = time.time()
if now - self._last_device_change < 1.0:
return # debounce
self._last_device_change = now
override = str(_get_config().get('card_reader_device', '') or '').strip()
chosen = _choose_card_reader(override=override)
if chosen is None:
return
# Only re-select if the winning device actually changed.
if self._device is not None and chosen['handle'] == self._device:
return
self._device = chosen['handle']
self._device_name = chosen['name']
_log(f"Device change — re-selected: {chosen['name']}")
except Exception:
pass
def _on_key_event(self, vk):
"""Handle a single key event (from Raw Input or LL-hook)."""
if not self._reading or self._finished:
return
if vk == _VK_RETURN:
data = ''.join(self._card_buffer).strip()
self._card_buffer = []
if data:
_log(f"Card read complete: '{data}' (len={len(data)})")
self._finish(data)
else:
# Accidental Enter with no data — keep waiting
self._last_activity = time.time()
return
if vk in (_VK_SHIFT, _VK_LSHIFT, _VK_RSHIFT,
_VK_CONTROL, _VK_LCONTROL, _VK_RCONTROL,
_VK_MENU, _VK_LMENU, _VK_RMENU,
_VK_CAPITAL, _VK_TAB, _VK_ESCAPE):
return # modifiers / control keys are not card data
ch = _vk_to_char(vk)
if ch:
self._card_buffer.append(ch)
self._last_activity = time.time()
def _finish(self, data):
"""Deliver the result exactly once (thread-safe), on the Kivy thread."""
with self._lock:
if self._finished:
return
self._finished = True
self._reading = False
cb = self._callback
self._callback = None
if cb is None:
return
# Prefer scheduling on the Kivy thread when a Kivy app is running
# (the callback touches Kivy widgets). If Kivy isn't running
# (manual test / non-GUI context), invoke the callback directly.
kivy_running = False
try:
from kivy.app import App
kivy_running = App.get_running_app() is not None
except Exception:
kivy_running = False
if kivy_running:
try:
from kivy.clock import Clock
Clock.schedule_once(lambda dt, d=data: cb(d), 0)
return
except Exception:
pass
try:
cb(data)
except Exception:
pass
if __name__ == '__main__':
# Simple manual test (no Kivy): run for a few seconds and print any card.
print("Windows Card Reader - manual test (swipe a card within 10s)")
def _cb(data):
print(f"CALLBACK: {data!r}")
reader = WindowsCardReader()
reader.read_card_async(_cb)
try:
time.sleep(10)
except KeyboardInterrupt:
pass
reader.stop_reading()
reader.shutdown()
print("Test done.")
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Execute the exact server playlist retrieval flow the player performs:
1. Load config/app_config.json (player code: screen_name + quickconnect_key)
2. Authenticate with the server (POST /api/auth/player)
3. Fetch playlist using the player_id + auth_code (GET /api/playlists/{player_id})
4. Print the RAW JSON exactly as received (before any local processing)
"""
import os
import sys
import json
import traceback
# Resolve the real src directory (script lives in working_files/, source is in src/)
HERE = os.path.dirname(os.path.abspath(__file__))
SRC_DIR = os.path.join(os.path.dirname(HERE), 'src')
sys.path.insert(0, SRC_DIR)
from get_playlists_v2 import get_auth_instance # noqa: E402
from player_auth import PlayerAuth # noqa: E402
CONFIG_PATH = os.path.join(os.path.dirname(HERE), 'config', 'app_config.json')
def main():
print("=" * 80)
print("EXECUTE SERVER PLAYLIST RETRIEVAL (player flow)")
print("=" * 80)
# 1. Load the player config
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
print(f"\n[1] Player config loaded from: {CONFIG_PATH}")
print(f" server_ip : {config.get('server_ip')}")
print(f" port : {config.get('port')}")
print(f" screen_name : {config.get('screen_name')} <- player code")
print(f" quickconnect_key: {config.get('quickconnect_key')} <- player quick-connect code")
print(f" use_https : {config.get('use_https')}")
print(f" verify_ssl : {config.get('verify_ssl')}")
# 2. Build server URL the same way ensure_authenticated() does
import re
server_ip = config.get("server_ip", "")
port = config.get("port", "")
use_https = config.get("use_https", True)
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
if re.match(ip_pattern, server_ip):
if use_https:
server_url = f'https://{server_ip}:{port}' if port else f'https://{server_ip}'
else:
server_url = f'http://{server_ip}:{port}' if port else f'http://{server_ip}'
else:
server_url = f'https://{server_ip}' if use_https else f'http://{server_ip}'
print(f"\n[2] Server URL used: {server_url}")
# 3. Authenticate with the player code
auth = get_auth_instance(
config_file=os.path.join(SRC_DIR, 'player_auth.json'),
use_https=config.get('use_https', True),
verify_ssl=config.get('verify_ssl', True)
)
print(f"\n[3] Authenticating player '{config.get('screen_name')}' with quickconnect code...")
success, error = auth.authenticate(
server_url=server_url,
hostname=config.get('screen_name', ''),
quickconnect_code=config.get('quickconnect_key', '')
)
if not success:
print(f" AUTH FAILED: {error}")
print(" Cannot retrieve playlist without a valid player code/auth.")
return
print(f" Authenticated as: {auth.get_player_name()} (player_id={auth.get_player_id()}, "
f"playlist_id={auth.auth_data.get('playlist_id')})")
# 4. Fetch the playlist (GET /api/playlists/{player_id})
print(f"\n[4] Fetching playlist from: {server_url}/api/playlists/{auth.get_player_id()}")
playlist_data = auth.get_playlist()
if playlist_data is None:
print(" PLAYLIST FETCH FAILED")
return
# 5. Print the RAW JSON exactly as received from the server
print(f"\n[5] RAW JSON received from server (status 200):\n")
print(json.dumps(playlist_data, indent=2, ensure_ascii=False))
# 6. Save the raw response for inspection
out_path = os.path.join(HERE, 'raw_server_playlist.json')
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(playlist_data, f, indent=2, ensure_ascii=False)
print(f"\n[6] Raw server response saved to: {out_path}")
# 7. Summary
print("\n" + "=" * 80)
print(f"SUMMARY: playlist_version={playlist_data.get('playlist_version')}, "
f"count={playlist_data.get('count')}, items={len(playlist_data.get('playlist', []))}")
print("=" * 80)
if __name__ == '__main__':
try:
main()
except Exception:
traceback.print_exc()
sys.exit(1)
+81
View File
@@ -0,0 +1,81 @@
{
"count": 6,
"player_id": 2,
"player_name": "Windows-Player1",
"playlist": [
{
"audio": "off",
"description": null,
"duration": 30,
"edit_on_player": false,
"file_name": "anders-jilden-cYrMQA7a3Wc-unsplash.jpg",
"id": 9,
"muted": true,
"position": 1,
"type": "image",
"url": "http://192.168.0.107:8080/static/uploads/anders-jilden-cYrMQA7a3Wc-unsplash.jpg"
},
{
"audio": "off",
"description": null,
"duration": 14,
"edit_on_player": true,
"file_name": "sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
"id": 2,
"muted": true,
"position": 2,
"type": "image",
"url": "http://192.168.0.107:8080/static/uploads/sean-oulashin-KMn4VEeEPR8-unsplash.jpg"
},
{
"audio": "on",
"description": null,
"duration": 31,
"edit_on_player": false,
"file_name": "sample-30s.mp4",
"id": 4,
"muted": false,
"position": 3,
"type": "video",
"url": "http://192.168.0.107:8080/static/uploads/sample-30s.mp4"
},
{
"audio": "off",
"description": null,
"duration": 50,
"edit_on_player": true,
"file_name": "edited_media/5/eye_e_v2.jpg",
"id": 5,
"muted": true,
"position": 4,
"type": "image",
"url": "http://192.168.0.107:8080/static/uploads/edited_media/5/eye_e_v2.jpg"
},
{
"audio": "off",
"description": "https://moto-adv.com/",
"duration": 30,
"edit_on_player": false,
"file_name": "weblink-ecc2705c34e5",
"id": 7,
"muted": true,
"position": 5,
"type": "weblink",
"url": "https://moto-adv.com/"
},
{
"audio": "off",
"description": null,
"duration": 30,
"edit_on_player": false,
"file_name": "jack-anstey-XVoyX7l9ocY-unsplash.jpg",
"id": 8,
"muted": true,
"position": 6,
"type": "image",
"url": "http://192.168.0.107:8080/static/uploads/jack-anstey-XVoyX7l9ocY-unsplash.jpg"
}
],
"playlist_id": 1,
"playlist_version": 32
}