mirror of
https://github.com/holyarahippo06/PS4-CDN-Server-py.git
synced 2026-10-06 07:00:23 +02:00
149 lines
8.9 KiB
Python
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
|