Files
holyarahippo06--PS4-CDN-Ser…/src/backend/pkg_manager.py
T
2025-07-28 18:59:31 +02:00

149 lines
8.9 KiB
Python

# backend/pkg_manager.py
import os
import difflib
from . import ps4_pkg_info, pkg_parser, hb_formatter, pss_scraper
# --- THE NEW, SIMPLER ALIAS SYSTEM ---
# The KEY is the EXACT, RAW title from the SFO as seen in the server logs.
# The VALUE is the precise search term you want to use instead.
RAW_TITLE_ALIASES = {
"HITMAN 3": "HITMAN World of Assassination",
"Minecraft: PlayStation®4 Edition": "Minecraft",
"Outlast Trinity: Outlast & Outlast Whistleblower": "Outlast Trinity"
}
def _clean_title(title: str) -> str:
"""
Helper function to normalize titles for comparison.
"""
title = title.replace('®', '').replace('™', '').replace(':', ' ').replace('&', ' ')
title = title.replace('.', ' ').replace('_', ' ')
# Collapse all whitespace into single spaces for clean comparison
return ' '.join(title.lower().split())
def process_pkg_file(pkg_path: str, icon_cache_dir: str):
"""Processes a single PKG file, returning its raw metadata."""
print(f"\n-> Processing: {os.path.basename(pkg_path)}")
info = ps4_pkg_info.get_ps4_pkg_info(pkg_path, generate_base64_icon=False)
if not info or not info.param_sfo:
print(f" [!] Failed to process PKG: {os.path.basename(pkg_path)}"); return None
metadata = info.param_sfo
# ... (icon and metadata setup is the same) ...
icon_data = info.icon0_raw
if icon_data:
base_pkg_filename = os.path.basename(pkg_path); icon_filename = f"{os.path.splitext(base_pkg_filename)[0]}.png"
icon_save_path = os.path.join(icon_cache_dir, icon_filename); os.makedirs(icon_cache_dir, exist_ok=True)
with open(icon_save_path, 'wb') as f: f.write(icon_data)
metadata['icon_url'] = f"/static/icons/{icon_filename}"; print(f" [+] Icon saved to {icon_filename}")
else:
metadata['icon_url'] = None; print(" [-] No icon found for this package.")
metadata['file_path'] = pkg_path; metadata['SIZE'] = pkg_parser.convert_bytes(os.path.getsize(pkg_path))
metadata['apptype'] = hb_formatter.get_apptype_from_path(pkg_path)
if metadata['apptype'] == 'Unknown' and metadata.get('CATEGORY', '').lower() in ('gp', 'gpc'): metadata['apptype'] = 'Patch'
# --- FINAL: Scraper with the CORRECT Alias System ---
if metadata.get('apptype') in ['HB Game', 'App'] and 'TITLE_ID' in metadata and 'TITLE' in metadata:
sfo_title_id = metadata['TITLE_ID']
sfo_content_id = metadata.get('CONTENT_ID', 'N/A')
sfo_title_raw = metadata['TITLE'] # The original, uncleaned title
print(f" [*] Attempting to fetch store data for '{sfo_title_raw}' (Content ID: {sfo_content_id})...")
# --- NEW LOGIC: Check for a RAW alias FIRST ---
if sfo_title_raw in RAW_TITLE_ALIASES:
search_title = RAW_TITLE_ALIASES[sfo_title_raw]
print(f" [*] Using RAW title alias: '{sfo_title_raw}' -> '{search_title}'")
else:
# If no raw alias, fall back to cleaning the title for the search
search_title = _clean_title(sfo_title_raw)
try:
search_results = pss_scraper.search_playstation_store(search_title)
if not search_results:
print(" [-] No results returned from PlayStation Store.")
return metadata
best_match = None; match_type = ""
# 1. Primary Method: Exact CUSA ID match
for game in search_results:
if game.get('cusa_id') == sfo_title_id:
best_match = game; match_type = "Exact CUSA ID"; break
# 2. Fallback Method: Fuzzy title match
if not best_match:
highest_score = 0.0
# The search_title is now either a clean alias or a cleaned SFO title
clean_search_title = _clean_title(search_title)
for game in search_results:
store_title_clean = _clean_title(game.get('name', ''))
score = difflib.SequenceMatcher(None, clean_search_title, store_title_clean).ratio()
if score > highest_score:
highest_score = score; best_match = game
if highest_score > 0.85:
match_type = f"Fuzzy Title ({int(highest_score * 100)}%)"
else:
best_match = None
# 3. Process the result
if best_match:
print(f" [+] Match Found! (Game: '{best_match['name']}', Type: {match_type}). Fetching details...")
details = pss_scraper.get_game_details(best_match['link'])
metadata['description'] = details.get('description', 'Description not found.')
metadata['rating'] = details.get('rating', 'N/A')
metadata['publisher'] = details.get('publisher', 'N/A')
metadata['release_date'] = details.get('release_date', '2024-01-01')
print(f" [+] Rating: {metadata['rating']}, Author: {metadata['publisher']}, Description/Date: Loaded, Release: {metadata['release_date']}.")
else:
print(" [-] Could not find a confident match in search results.")
except Exception as e:
print(f" [!] Scraper failed: {e}")
return metadata
# The scan_directory function and all post-processing loops remain completely unchanged.
def scan_directory(base_path):
# ... (no changes here) ...
if not base_path or not os.path.isdir(base_path): return []
print(f"\n[*] Starting recursive scan in directory: {base_path}"); icon_cache_dir = os.path.abspath(os.path.join('frontend', 'static', 'icons')); all_packages = []
for root, _, files in os.walk(base_path):
for file in sorted(files):
if file.lower().endswith('.pkg'):
full_path = os.path.join(root, file); package_data = process_pkg_file(full_path, icon_cache_dir)
if package_data: all_packages.append(package_data)
print(f"\n[*] Initial scan complete. Found {len(all_packages)} packages."); print("[*] Starting post-processing pass for missing DLC/Patch info.")
master_info = {}
placeholder_titles = ['sample', 'test', 'dlc', 'patch', 'update']
for pkg in all_packages:
apptype = pkg.get("apptype", "Unknown"); title_id = pkg.get("TITLE_ID")
if apptype not in ["Patch", "DLC"] and title_id and pkg.get('TITLE', '').lower() not in placeholder_titles:
if title_id not in master_info: master_info[title_id] = { 'TITLE': pkg.get('TITLE'), 'icon_url': pkg.get('icon_url'), 'publisher': pkg.get('publisher'), 'release_date': pkg.get('release_date'), 'rating': pkg.get('rating') }
fixed_count = 0
for pkg in all_packages:
apptype = pkg.get("apptype", "Unknown"); title_id = pkg.get("TITLE_ID")
if apptype in ["Patch", "DLC"] and title_id in master_info:
master = master_info[title_id]; fixed = False
if not pkg.get('icon_url') and master.get('icon_url'): pkg['icon_url'] = master.get('icon_url'); fixed = True
if pkg.get('TITLE', '').lower() in placeholder_titles: pkg['TITLE'] = master.get('TITLE'); fixed = True
if not pkg.get('publisher') and master.get('publisher'): pkg['publisher'] = master.get('publisher'); fixed = True
if not pkg.get('release_date') and master.get('release_date'): pkg['release_date'] = master.get('release_date'); fixed = True
if not pkg.get('rating') and master.get('rating'): pkg['rating'] = master.get('rating'); fixed = True
if fixed: fixed_count += 1
if fixed_count > 0: print(f"[+] Post-processing complete. Fixed info for {fixed_count} packages.")
print("[*] Starting post-processing pass for paired Themes..."); packages_by_path = {pkg['file_path']: pkg for pkg in all_packages}; themes_fixed_count = 0
for master_theme in all_packages:
if master_theme.get('apptype') == 'Theme' and master_theme.get('file_path', '').endswith('_2.pkg'):
partner_path = master_theme['file_path'].replace('_2.pkg', '_1.pkg'); partner_theme = packages_by_path.get(partner_path)
if partner_theme:
print(f" [+] Found theme pair: {os.path.basename(partner_path)} & {os.path.basename(master_theme['file_path'])}"); partner_theme['TITLE'] = master_theme.get('TITLE', 'Untitled Theme'); partner_theme['icon_url'] = master_theme.get('icon_url'); partner_theme['TITLE_ID'] = master_theme.get('TITLE_ID'); partner_theme['CONTENT_ID'] = master_theme.get('CONTENT_ID')
base_title = partner_theme['TITLE']; partner_theme['TITLE'] = f"{base_title} 1"; master_theme['TITLE'] = f"{base_title} 2"
print(f" -> Applied metadata and renamed to '{base_title} 1/2'"); themes_fixed_count += 1
if themes_fixed_count > 0: print(f"[+] Theme pairing complete. Processed {themes_fixed_count} pairs.")
return all_packages