Files
zwift-offline/workouts_manifest.py
fatsbrown 50bfa24d99 Maintain server side workouts manifest
Fix user home sync status in single player mode.
Sync workouts before rendering user home.
Changes in launcher pages.
Revert unnecessary changes.
2026-05-31 13:49:15 -03:00

96 lines
3.0 KiB
Python

import hashlib
import os
import xml.etree.ElementTree as ET
MANIFEST_FILENAME = 'workouts.files'
def manifest_file(workouts_dir):
return os.path.join(workouts_dir, MANIFEST_FILENAME)
def parse_int(value):
try:
return int((value or '0').strip() or '0')
except ValueError:
return 0
def file_checksum(content):
return (-sum(content)) % 256
def file_guid(filename):
digest = hashlib.sha1(filename.encode('utf-8')).digest()
return (int.from_bytes(digest[:4], 'big') & 0x7fffffff) or 1
def load_manifest(workouts_dir):
path = manifest_file(workouts_dir)
if not os.path.exists(path):
return []
try:
root = ET.parse(path).getroot()
except ET.ParseError:
return []
entries = []
for node in root.findall('custom_file'):
name = (node.findtext('name') or '').strip()
if not name:
continue
entries.append({
'name': name,
'time': parse_int(node.findtext('time')),
'guid': parse_int(node.findtext('guid')),
'checksum': parse_int(node.findtext('checksum')),
'deleted': (node.findtext('deleted') or 'false').strip().lower() == 'true',
})
return entries
def save_manifest(workouts_dir, entries):
os.makedirs(workouts_dir, exist_ok=True)
root = ET.Element('custom_file_directory')
for entry in sorted(entries, key=lambda item: item['name'].lower()):
node = ET.SubElement(root, 'custom_file')
ET.SubElement(node, 'name').text = entry['name']
ET.SubElement(node, 'time').text = str(int(entry['time']))
ET.SubElement(node, 'guid').text = str(int(entry['guid']))
ET.SubElement(node, 'checksum').text = str(int(entry['checksum']))
ET.SubElement(node, 'deleted').text = 'true' if entry.get('deleted') else 'false'
ET.SubElement(root, 'deleted_files')
tree = ET.ElementTree(root)
ET.indent(tree, space=' ')
path = manifest_file(workouts_dir)
tree.write(path, encoding='utf-8', xml_declaration=False)
with open(path, 'a', encoding='utf-8') as fd:
fd.write('\n')
return path
def upsert_manifest_entry(workouts_dir, filename, content, timestamp=None):
entries = [entry for entry in load_manifest(workouts_dir) if entry['name'] != filename]
timestamp = int(timestamp if timestamp is not None else os.path.getmtime(os.path.join(workouts_dir, filename)))
entry = {
'name': filename,
'time': timestamp,
'guid': file_guid(filename),
'checksum': file_checksum(content),
'deleted': False,
}
entries.append(entry)
save_manifest(workouts_dir, entries)
return entry
def remove_prefixed_workouts(workouts_dir, prefixes):
entries = load_manifest(workouts_dir)
removed = 0
for entry in entries:
if entry['name'].startswith(tuple(prefixes)):
entry['deleted'] = True
removed += 1
save_manifest(workouts_dir, entries)
return removed