feat: port Hatena server to Python 3
* Update server-side database and dynamic resource code for Python 3 * Preserve existing Flipnote, UGO, NTFT, CSS, localization, and Hatena assets * Port Flipnote upload and movie endpoints * Update front page UGO menu generators * Preserve existing database and sample Flipnote data * Add regression tests for database operations and binary formats * Add server resource loading tests * Maintain compatibility with the original DSi Flipnote Studio protocol
This commit is contained in:
1 parent
687a2bcb43
commit
e4bb0b00ba
48 files changed
+3463
No files matched your search
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from Hatenatools import NTFT, PPM, TMB, UGO
|
||||
from Hatenatools.PPM import WriteImage, get_metadata
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SAMPLE_PPM = ROOT / "database" / "Creators" / "913304C0CC74CC1F" / "74CC1F_0D3AA40F2B3A0_000.ppm"
|
||||
NEWS_NTFT = ROOT / "hatenadir" / "ds" / "v2-xx" / "__ntfts" / "News.ntft"
|
||||
KAERU_NTFT = ROOT / "hatenadir" / "images" / "ds" / "kaeru.ntft"
|
||||
INDEX_UGOXML = ROOT / "hatenadir" / "ds" / "v2-xx" / "index.ugoxml"
|
||||
INBOX_UGOXML = ROOT / "hatenadir" / "ds" / "v2-xx" / "inbox.ugoxml"
|
||||
|
||||
|
||||
class PPMTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.data = SAMPLE_PPM.read_bytes()
|
||||
|
||||
def test_tmb_metadata_and_exact_pack(self):
|
||||
tmb = TMB().Read(self.data[:0x6A0], DecodeThumbnail=True)
|
||||
self.assertTrue(tmb)
|
||||
self.assertEqual(tmb.EditorAuthorID, "913304C0CC74CC1F")
|
||||
self.assertEqual(tmb.CurrentFilename, "74CC1F_0D3AA40F2B3A0_000.tmb")
|
||||
self.assertEqual(tmb.FrameCount, 3)
|
||||
self.assertEqual(tmb.GetThumbnail().shape, (64, 48))
|
||||
self.assertEqual(tmb.Pack(), self.data[:0x6A0])
|
||||
|
||||
def test_ppm_full_parse_and_frame(self):
|
||||
ppm = PPM().Read(self.data, DecodeThumbnail=True, ReadFrames=True, ReadSound=True)
|
||||
self.assertTrue(ppm)
|
||||
self.assertEqual(ppm.EditorAuthorID, "913304C0CC74CC1F")
|
||||
self.assertEqual(ppm.CurrentFilename, "74CC1F_0D3AA40F2B3A0_000.ppm")
|
||||
self.assertEqual(ppm.FrameCount, 3)
|
||||
self.assertEqual(ppm.GetThumbnail().shape, (64, 48))
|
||||
frame = ppm.GetFrame(0)
|
||||
self.assertIsInstance(frame, np.ndarray)
|
||||
self.assertEqual(frame.shape, (256, 192))
|
||||
self.assertEqual(len(ppm.SoundData), 4)
|
||||
|
||||
def test_metadata_helper_infers_filetype(self):
|
||||
ppm = PPM().Read(self.data, ReadFrames=False)
|
||||
metadata = get_metadata(ppm)
|
||||
self.assertEqual(metadata["Current filename"], "74CC1F_0D3AA40F2B3A0_000.ppm")
|
||||
self.assertEqual(metadata["Number of frames"], 3)
|
||||
self.assertIn("Frame speed", metadata)
|
||||
|
||||
def test_image_export(self):
|
||||
ppm = PPM().Read(self.data, DecodeThumbnail=True, ReadFrames=False)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp) / "thumb.png"
|
||||
self.assertTrue(WriteImage(ppm.GetThumbnail(), str(output)))
|
||||
self.assertGreater(output.stat().st_size, 0)
|
||||
|
||||
|
||||
class NTFTTests(unittest.TestCase):
|
||||
def test_exact_roundtrip_news(self):
|
||||
raw = NEWS_NTFT.read_bytes()
|
||||
image = NTFT().Read(raw, (32, 32))
|
||||
self.assertTrue(image)
|
||||
self.assertEqual(image.Pack(), raw)
|
||||
|
||||
def test_exact_roundtrip_kaeru(self):
|
||||
raw = KAERU_NTFT.read_bytes()
|
||||
image = NTFT().Read(raw, (36, 30))
|
||||
self.assertTrue(image)
|
||||
self.assertEqual(image.Pack(), raw)
|
||||
|
||||
|
||||
class UGOTests(unittest.TestCase):
|
||||
def _roundtrip_xml(self, path: Path):
|
||||
ugo = UGO().ReadXML(str(path), False)
|
||||
self.assertTrue(ugo)
|
||||
packed = ugo.Pack()
|
||||
self.assertIsInstance(packed, bytes)
|
||||
self.assertEqual(packed[:4], b"UGAR")
|
||||
decoded = UGO().Read(packed)
|
||||
self.assertTrue(decoded)
|
||||
# Embedded-file names are XML-side metadata and are not stored in the
|
||||
# binary UGO format. The meaningful binary round-trip is idempotence.
|
||||
self.assertEqual(decoded.Pack(), packed)
|
||||
|
||||
def test_index_xml(self):
|
||||
self._roundtrip_xml(INDEX_UGOXML)
|
||||
|
||||
def test_inbox_xml(self):
|
||||
self._roundtrip_xml(INBOX_UGOXML)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user
