diff --git a/backend/endpoints/feeds.py b/backend/endpoints/feeds.py index 8b1224307..50e981dc1 100644 --- a/backend/endpoints/feeds.py +++ b/backend/endpoints/feeds.py @@ -22,6 +22,11 @@ from endpoints.responses.feeds import ( PKGiFeedPS3ItemSchema, PKGiFeedPSPItemSchema, PKGiFeedPSVitaItemSchema, + PkgjPSPDlcsItemSchema, + PkgjPSPGamesItemSchema, + PkgjPSVDlcsItemSchema, + PkgjPSVGamesItemSchema, + PkgjPSXGamesItemSchema, TinfoilFeedFileSchema, TinfoilFeedSchema, TinfoilFeedTitleDBSchema, @@ -318,8 +323,20 @@ def pkgi_ps3_feed( ) # 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_lines.append( + ",".join( + [ + pkgi_item.contentid, + str(pkgi_item.type), + f'"{pkgi_item.name}"', + pkgi_item.description, + pkgi_item.rap, + f'"{pkgi_item.url}"', + str(pkgi_item.size), + pkgi_item.checksum, + ] + ) + ) txt_content = "\n".join(txt_lines) @@ -388,8 +405,20 @@ def pkgi_psvita_feed( ) # Format: contentid,flags,name,name2,zrif,url,size,checksum - txt_line = f'{pkgi_item.contentid},{pkgi_item.flags},"{pkgi_item.name}",{pkgi_item.name2},{pkgi_item.zrif},"{pkgi_item.url}",{pkgi_item.size},{pkgi_item.checksum}' - txt_lines.append(txt_line) + txt_lines.append( + ",".join( + [ + pkgi_item.contentid, + str(pkgi_item.flags), + f'"{pkgi_item.name}"', + pkgi_item.name2, + pkgi_item.zrif, + f'"{pkgi_item.url}"', + str(pkgi_item.size), + pkgi_item.checksum, + ] + ) + ) txt_content = "\n".join(txt_lines) @@ -460,8 +489,20 @@ def pkgi_psp_feed( ) # 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_lines.append( + ",".join( + [ + pkgi_item.contentid, + str(pkgi_item.type), + f'"{pkgi_item.name}"', + pkgi_item.description, + pkgi_item.rap, + f'"{pkgi_item.url}"', + str(pkgi_item.size), + pkgi_item.checksum, + ] + ) + ) txt_content = "\n".join(txt_lines) @@ -583,8 +624,21 @@ def kekatsu_ds_feed(request: Request, platform_slug: str) -> Response: ) # Format: title platform region version author download_url filename size box_art_url - txt_line = f"{kekatsu_item.title}\t{kekatsu_item.platform}\t{kekatsu_item.region}\t{kekatsu_item.version}\t{kekatsu_item.author}\t{kekatsu_item.download_url}\t{kekatsu_item.filename}\t{kekatsu_item.size}\t{kekatsu_item.box_art_url}" - txt_lines.append(txt_line) + txt_lines.append( + "\t".join( + [ + kekatsu_item.title, + kekatsu_item.platform, + kekatsu_item.region, + kekatsu_item.version, + kekatsu_item.author, + kekatsu_item.download_url, + kekatsu_item.filename, + str(kekatsu_item.size), + kekatsu_item.box_art_url, + ] + ) + ) txt_content = "\n".join(txt_lines) @@ -596,3 +650,319 @@ def kekatsu_ds_feed(request: Request, platform_slug: str) -> Response: "Cache-Control": "no-cache", }, ) + + +def _format_pkgj_datetime(value: datetime | None) -> str: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %H:%M:%S") + return "" + + +@protected_route( + router.get, + "/pkgj/psp/games", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ], +) +def pkgj_psp_games_feed(request: Request) -> Response: + platform = db_platform_handler.get_platform_by_slug(UPS.PSP) + if not platform: + raise HTTPException( + status_code=404, detail="PlayStation Portable platform not found" + ) + + roms = db_rom_handler.get_roms_scalar(platform_ids=[platform.id]) + txt_lines = [] + txt_lines.append( + "Title ID\tRegion\tType\tName\tPKG direct link\tContent ID\tLast Modification Date\tRAP\tDownload .RAP file\tFile Size\tSHA256" + ) + + for rom in roms: + download_url = generate_rom_download_url(request, rom) + last_modified = _format_pkgj_datetime(rom.updated_at) + + pkgj_item = PkgjPSPGamesItemSchema( + title_id="", + region=rom.regions[0] if rom.regions else "", + type="PSP", + name=(rom.name or rom.fs_name_no_tags).strip(), + download_link=download_url, + content_id="", + last_modified=rom.updated_at, + rap="", + download_rap_file="", + file_size=rom.fs_size_bytes, + sha_256=rom.sha1_hash or "", + ) + + txt_lines.append( + "\t".join( + [ + pkgj_item.title_id, + pkgj_item.region, + pkgj_item.type, + pkgj_item.name, + pkgj_item.download_link, + pkgj_item.content_id, + last_modified, + pkgj_item.rap, + pkgj_item.download_rap_file, + str(pkgj_item.file_size), + pkgj_item.sha_256, + ] + ) + ) + + return Response( + content="\n".join(txt_lines), + media_type="text/plain", + headers={ + "Content-Disposition": "filename=titles_pspgames.txt", + "Cache-Control": "no-cache", + }, + ) + + +@protected_route( + router.get, + "/pkgj/psp/dlc", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ], +) +def pkgj_psp_dlcs_feed(request: Request) -> Response: + platform = db_platform_handler.get_platform_by_slug(UPS.PSP) + if not platform: + raise HTTPException( + status_code=404, detail="PlayStation Portable platform not found" + ) + + roms = db_rom_handler.get_roms_scalar(platform_ids=[platform.id]) + txt_lines = [] + txt_lines.append( + "Title ID\tRegion\tName\tPKG direct link\tContent ID\tLast Modification Date\tRAP\tDownload .RAP file\tFile Size\tSHA256" + ) + + for rom in roms: + download_url = generate_rom_download_url(request, rom) + last_modified = _format_pkgj_datetime(rom.updated_at) + + pkgj_item = PkgjPSPDlcsItemSchema( + title_id="", + region=rom.regions[0] if rom.regions else "", + name=(rom.name or rom.fs_name_no_tags).strip(), + download_link=download_url, + content_id="", + last_modified=rom.updated_at, + rap="", + download_rap_file="", + file_size=rom.fs_size_bytes, + sha_256=rom.sha1_hash or "", + ) + + txt_lines.append( + "\t".join( + [ + pkgj_item.title_id, + pkgj_item.region, + pkgj_item.name, + pkgj_item.download_link, + pkgj_item.content_id, + last_modified, + pkgj_item.rap, + pkgj_item.download_rap_file, + str(pkgj_item.file_size), + pkgj_item.sha_256, + ] + ) + ) + + return Response( + content="\n".join(txt_lines), + media_type="text/plain", + headers={ + "Content-Disposition": "filename=titles_pspdlcs", + "Cache-Control": "no-cache", + }, + ) + + +@protected_route( + router.get, + "/pkgj/psvita/games", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ], +) +def pkgj_psv_games_feed(request: Request) -> Response: + platform = db_platform_handler.get_platform_by_slug(UPS.PSVITA) + if not platform: + raise HTTPException( + status_code=404, detail="PlayStation Vita platform not found" + ) + + roms = db_rom_handler.get_roms_scalar(platform_ids=[platform.id]) + txt_lines = [] + txt_lines.append( + "Title ID\tRegion\tName\tPKG direct link\tzRIF\tContent ID\tLast Modification Date\tOriginal Name\tFile Size\tSHA256\tRequired FW\tApp Version" + ) + + for rom in roms: + download_url = generate_rom_download_url(request, rom) + last_modified = _format_pkgj_datetime(rom.updated_at) + + pkgj_item = PkgjPSVGamesItemSchema( + title_id="", + region=rom.regions[0] if rom.regions else "", + name=(rom.name or rom.fs_name_no_tags).strip(), + download_link=download_url, + zrif="", + content_id="", + last_modified=rom.updated_at, + original_name="", + file_size=rom.fs_size_bytes, + sha_256=rom.sha1_hash or "", + required_fw="", + app_version="", + ) + + txt_lines.append( + "\t".join( + [ + pkgj_item.title_id, + pkgj_item.region, + pkgj_item.name, + pkgj_item.download_link, + pkgj_item.zrif, + pkgj_item.content_id, + last_modified, + pkgj_item.original_name, + str(pkgj_item.file_size), + pkgj_item.sha_256, + pkgj_item.required_fw, + pkgj_item.app_version, + ] + ) + ) + + return Response( + content="\n".join(txt_lines), + media_type="text/plain", + headers={ + "Content-Disposition": "filename=titles_psvgames.tsv", + "Cache-Control": "no-cache", + }, + ) + + +@protected_route( + router.get, + "/pkgj/psvita/dlc", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ], +) +def pkgj_psv_dlcs_feed(request: Request) -> Response: + platform = db_platform_handler.get_platform_by_slug(UPS.PSVITA) + if not platform: + raise HTTPException( + status_code=404, detail="PlayStation Vita platform not found" + ) + + roms = db_rom_handler.get_roms_scalar(platform_ids=[platform.id]) + txt_lines = [] + txt_lines.append( + "Title ID\tRegion\tName\tPKG direct link\tzRIF\tContent ID\tLast Modification Date\tFile Size\tSHA256" + ) + + for rom in roms: + download_url = generate_rom_download_url(request, rom) + last_modified = _format_pkgj_datetime(rom.updated_at) + + pkgj_item = PkgjPSVDlcsItemSchema( + title_id="", + region=rom.regions[0] if rom.regions else "", + name=(rom.name or rom.fs_name_no_tags).strip(), + download_link=download_url, + zrif="", + content_id="", + last_modified=rom.updated_at, + file_size=rom.fs_size_bytes, + sha_256=rom.sha1_hash or "", + ) + + txt_lines.append( + "\t".join( + [ + pkgj_item.title_id, + pkgj_item.region, + pkgj_item.name, + pkgj_item.download_link, + pkgj_item.zrif, + pkgj_item.content_id, + last_modified, + str(pkgj_item.file_size), + pkgj_item.sha_256, + ] + ) + ) + + return Response( + content="\n".join(txt_lines), + media_type="text/plain", + headers={ + "Content-Disposition": "filename=titles_psvdlcs.tsv", + "Cache-Control": "no-cache", + }, + ) + + +@protected_route( + router.get, + "/pkgj/psx/games", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ], +) +def pkgj_psx_games_feed(request: Request) -> Response: + platform = db_platform_handler.get_platform_by_slug(UPS.PSX) + if not platform: + raise HTTPException(status_code=404, detail="PlayStation platform not found") + + roms = db_rom_handler.get_roms_scalar(platform_ids=[platform.id]) + txt_lines = [] + txt_lines.append( + "Title ID\tRegion\tName\tPKG direct link\tContent ID\tLast Modification Date\tOriginal Name\tFile Size\tSHA256" + ) + + for rom in roms: + download_url = generate_rom_download_url(request, rom) + last_modified = _format_pkgj_datetime(rom.updated_at) + + pkgj_item = PkgjPSXGamesItemSchema( + title_id="", + region=rom.regions[0] if rom.regions else "", + name=(rom.name or rom.fs_name_no_tags).strip(), + download_link=download_url, + content_id="", + last_modified=rom.updated_at, + original_name="", + file_size=rom.fs_size_bytes, + sha_256=rom.sha1_hash or "", + ) + + txt_lines.append( + "\t".join( + [ + pkgj_item.title_id, + pkgj_item.region, + pkgj_item.name, + pkgj_item.download_link, + pkgj_item.content_id, + last_modified, + pkgj_item.original_name, + str(pkgj_item.file_size), + pkgj_item.sha_256, + ] + ) + ) + + return Response( + content="\n".join(txt_lines), + media_type="text/plain", + headers={ + "Content-Disposition": "filename=titles_psxgames.tsv", + "Cache-Control": "no-cache", + }, + ) diff --git a/backend/endpoints/responses/feeds.py b/backend/endpoints/responses/feeds.py index 1cb6236f0..b9240be3a 100644 --- a/backend/endpoints/responses/feeds.py +++ b/backend/endpoints/responses/feeds.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Annotated, Any, Final, NotRequired, TypedDict from pydantic import BaseModel, BeforeValidator, Field, field_validator @@ -244,3 +245,42 @@ class KekatsuDSItemSchema(BaseModel): filename: str size: int box_art_url: str + + +# Pkgj feed formats +# Source: https://github.com/rommapp/romm/issues/2899 +class PkgjBaseItemSchema(BaseModel): + title_id: str + region: str + name: str + download_link: str + content_id: str + last_modified: datetime + file_size: int + sha_256: str + + +class PkgjPSPGamesItemSchema(PkgjBaseItemSchema): + type: str + rap: str + download_rap_file: str + + +class PkgjPSPDlcsItemSchema(PkgjBaseItemSchema): + rap: str + download_rap_file: str + + +class PkgjPSVGamesItemSchema(PkgjBaseItemSchema): + zrif: str + original_name: str + required_fw: str + app_version: str + + +class PkgjPSVDlcsItemSchema(PkgjBaseItemSchema): + zrif: str + + +class PkgjPSXGamesItemSchema(PkgjBaseItemSchema): + original_name: str