diff --git a/backend/endpoints/feeds.py b/backend/endpoints/feeds.py index 5e01d67e0..f587a2236 100644 --- a/backend/endpoints/feeds.py +++ b/backend/endpoints/feeds.py @@ -18,6 +18,7 @@ from endpoints.responses.feeds import ( WEBRCADE_SUPPORTED_PLATFORM_SLUGS, KekatsuDSItemSchema, PKGiFeedPS3ItemSchema, + PKGiFeedPSPItemSchema, PKGiFeedPSVitaItemSchema, TinfoilFeedFileSchema, TinfoilFeedSchema, @@ -29,6 +30,8 @@ from endpoints.responses.feeds import ( ) from handler.auth.constants import Scope from handler.database import db_platform_handler, db_rom_handler +from handler.filesystem import fs_rom_handler +from handler.filesystem.roms_handler import is_compressed_file from handler.metadata import meta_igdb_handler from handler.metadata.base_handler import ( SONY_SERIAL_REGEX, @@ -229,6 +232,11 @@ def validate_pkgi_file(file: RomFile, content_type: RomFileCategory) -> bool: if content_type == RomFileCategory.GAME and not file.is_top_level: return False + # Compressed files get a free pass + full_path = fs_rom_handler.validate_path(file.full_path) + if content_type == RomFileCategory.GAME and is_compressed_file(str(full_path)): + return True + # PKGi only supports PKG files if file.file_extension.lower() != "pkg": return False @@ -395,6 +403,78 @@ def pkgi_psvita_feed( ) +@protected_route( + router.get, + "/pkgi/psp/{content_type}", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ], +) +def pkgi_psp_feed( + request: Request, + content_type: Annotated[str, PathVar(description="Content type")], +) -> Response: + """Get PKGi PSP feed endpoint + https://github.com/bucanero/pkgi-psp + + Args: + request (Request): Fastapi Request object + content_type (str): Content type (game, dlc, demo, update, patch, mod, translation, prototype) + + Returns: + Response: txt file with PKGi PSP database format + """ + psp_platform = db_platform_handler.get_platform_by_fs_slug(UPS.PSP) + if not psp_platform: + raise HTTPException( + status_code=404, detail="PlayStation Portable platform not found" + ) + + try: + content_type_enum = RomFileCategory(content_type) + content_type_int = CONTENT_TYPE_MAP[content_type_enum] + except (ValueError, KeyError) as e: + raise HTTPException( + status_code=400, detail=f"Invalid content type: {content_type}" + ) from e + + roms = db_rom_handler.get_roms_scalar(platform_id=psp_platform.id) + txt_lines = [] + + for rom in roms: + for file in db_rom_handler.get_rom_files(rom.id): + if not validate_pkgi_file(file, content_type_enum): + continue + + content_id = generate_content_id(file) + download_url = generate_download_url(request, file) + + # Validate the item schema + pkgi_item = PKGiFeedPSPItemSchema( + contentid=content_id, + type=content_type_int, + name=file.file_name_no_tags.replace(",", " "), + description="", + rap="", + url=download_url, + size=file.file_size_bytes, + checksum=file.sha1_hash or "", + ) + + # Format: contentid,type,name,description,rap,url,size,checksum + txt_line = f"{pkgi_item.contentid},{pkgi_item.type},{pkgi_item.name},{pkgi_item.description},{pkgi_item.rap},{pkgi_item.url},{pkgi_item.size},{pkgi_item.checksum}" + txt_lines.append(txt_line) + + txt_content = "\n".join(txt_lines) + + return Response( + content=txt_content, + media_type="text/plain", + headers={ + "Content-Disposition": f"filename=pkgi_{content_type_enum.value}.txt", + "Cache-Control": "no-cache", + }, + ) + + @protected_route( router.get, "/kekatsu/{platform_slug}", diff --git a/backend/endpoints/responses/feeds.py b/backend/endpoints/responses/feeds.py index 13fcbaca5..722184570 100644 --- a/backend/endpoints/responses/feeds.py +++ b/backend/endpoints/responses/feeds.py @@ -184,6 +184,18 @@ class PKGiFeedPS3ItemSchema(BaseModel): checksum: str +# PKGi PSP feed format +# Source: https://github.com/bucanero/pkgi-psp +class PKGiFeedPSPItemSchema(PKGiFeedPS3ItemSchema): + """Schema for PKGi PSP feed items. + + Follows the PKGi database format: + contentid,type,name,description,rap,url,size,checksum + """ + + pass + + # PKGi PS Vita feed format # Source: https://github.com/mmozeiko/pkgi class PKGiFeedPSVitaItemSchema(BaseModel): diff --git a/backend/models/rom.py b/backend/models/rom.py index 766019fd5..ab092ebab 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -106,7 +106,10 @@ class RomFile(BaseModel): @cached_property def is_top_level(self) -> bool: - return self.file_path == self.rom.full_path + # File is the same as the rom's full path, or nested file in the rom's directory + return self.rom.full_path == ( + self.file_path if self.is_nested else self.full_path + ) def file_name_for_download(self, hidden_folder: bool = False) -> str: # This needs a trailing slash in the path to work!