diff --git a/DOCS.md b/DOCS.md index 1869702..e462a35 100644 --- a/DOCS.md +++ b/DOCS.md @@ -59,7 +59,7 @@ Logs are written to `logs/reximemo.log` and mirrored to standard output unless a | `server.py` | Process entry point, port selection, optional feature flags, logging and the proxy-compatible Twisted Site. | | `hatena.py` | Top-level HTTP resource router. It separates web traffic from DSi traffic, serves static CSS/images, loads the `hatenadir` resource tree and applies IP bans before either surface is reached. | | `reximemo.py` | Shared DSi helpers: regional URLs, current IP/account lookup, DSi keyboard values, DSi dialog/forward headers, UGO menu construction, pagination, PPM first-frame rendering and mini-Flipnote NPF conversion. | -| `webapp.py` | The normal browser client. It renders Home, Browse, About, optional Docs, Flipnote, Creator's Room, account and admin pages and exposes thumbnails/PPM downloads. | +| `webapp.py` | The normal browser client. It renders Home, Browse, About, optional Docs, Flipnote, Creator's Room, account and admin pages and exposes thumbnails, inline PPM playback media and counted PPM downloads. | | `docs_page.py` | The text of this built-in manual. It is imported by the web client only when the documentation route is requested. | | `DB.py` | Compatibility shim used by the older-style resource modules. It imports the single SQLite `Database` object and advertises `DB_type = "sqlite"`. | | `database/__init__.py` | SQLite schema, seed data and all database operations used by the OSE server. | @@ -74,7 +74,8 @@ Logs are written to `logs/reximemo.log` and mirrored to standard output unless a | `hatenadir/images/ds/account.ntft` | 32×32 generated Account menu icon. | | `hatenadir/images/ds/room.ntft` | 32×32 generated Creator's Room menu icon. | | `hatenadir/images/ds/placeholder.npf` | 128×64 generated indexed placeholder banner retained for DSi asset testing/use. | -| `web/static/site.css` | Complete browser stylesheet: colour variables, header/navigation, Flipnote grids, richer watch layout, comments, forms, admin tables, documentation layout and responsive breakpoints. | +| `web/static/site.css` | Complete browser stylesheet: colour variables, header/navigation, Flipnote grids, watch/player layout, playback controls, comments, forms, admin tables, documentation layout and responsive breakpoints. | +| `web/static/flipnote-player.js` | Small browser-side controller around flipnote.js. It loads the PPM, updates play/pause, seek, sound, loop and time controls, and restores the first-frame fallback if playback is unavailable. | | `web/static/RexiMemo_OSE_Logo.png` | White RexiMemo logo with “Open Source Edition” included in the image itself. | | `Hatenatools/__init__.py` | Convenience exports for `PPM`, `TMB`, `UGO` and `NTFT`. | | `Hatenatools/PPM.py` | Legacy Flipnote Studio PPM/TMB parser: metadata, thumbnail data, frame decoding, sound decoding and helper dump functions. OSE mainly uses `PPM.Read()`, `PPM.GetFrame()` and `TMB.Read()`. | @@ -125,6 +126,7 @@ The browser client is server-rendered HTML in `webapp.py`. It has no JavaScript | `/watch//comment` | POST | Adds a text comment using the current account or guest-IP Creator's Room. | | `/thumb/.png` | GET | Decodes the first PPM frame and returns a cached PNG thumbnail. | | `/comment-thumb/.png` | GET | Returns a first-frame PNG preview for a mini-Flipnote comment. | +| `/media/.ppm` | GET | Returns the original PPM inline for browser playback. It does not increment the download counter. | | `/flipnote/.ppm` | GET | Increments downloads and returns the original PPM as an attachment. | | `/creator/` | GET | Creator's Room with its Flipnote grid and account/guest ownership label. | | `/login` | GET/POST | Username/password sign-in. Successful POST writes an `ip_logins` row for the source IP. | @@ -140,7 +142,15 @@ The common page wrapper builds the header, navigation, account/admin actions and ## Flipnote view page -The browser does not attempt to reproduce Flipnote Studio playback. A Flipnote view uses the first decoded frame as a large preview and presents the original `.ppm` as the downloadable playable file. This keeps the web client small and avoids adding a separate animation/audio player implementation. +The watch page has browser playback as well as the server-generated first-frame fallback. `webapp.py` loads the pinned `flipnote.js` 6.3.1 browser build from jsDelivr, then loads the local `web/static/flipnote-player.js` controller. The controller creates a `flipnote.Player` at 320×240, asks it to load `/media/.ppm`, and lets flipnote.js decode the original PPM animation and audio in the browser. OSE does not pre-render a video, GIF or separate audio file. + +The player starts paused. Once flipnote.js reports that the note is ready, OSE hides the first-frame image and enables the large play button and the control row. The controls provide play/pause, a seek range, elapsed/total time, sound mute/unmute and loop on/off. Seeking uses flipnote.js's `startSeek()`, `seek()` and `endSeek()` calls so playback can resume cleanly after dragging. Clicking the rendered Flipnote toggles playback after the player has been started once. + +`/media/.ppm` is deliberately separate from `/flipnote/.ppm`. The media route returns the same on-disk PPM with `Content-Disposition: inline` and a one-hour public cache header, but it does not call `Database.AddDownload()`. The download route remains the explicit **Download PPM** action, increments the download counter and returns the file as an attachment. Opening or replaying a watch page therefore does not count as a file download. + +The `` first-frame preview remains in the page before JavaScript runs. If the flipnote.js global is missing, the browser cannot construct its canvas/WebAudio player, the PPM cannot be parsed, or playback reports an error, `flipnote-player.js` leaves that preview visible, disables the player controls and shows a short playback-unavailable message. The rest of the watch page still works normally. This also gives the page a useful non-JavaScript fallback. + +> **Browser dependency/privacy notice.** The playback library is loaded from the jsDelivr CDN rather than copied into OSE. This keeps a large third-party build out of the repository, but it means a browser visiting a watch page makes a request to jsDelivr for `flipnote.min.js`. The PPM URL itself is same-origin and is not submitted to jsDelivr by OSE. An operator who does not want that third-party browser request should self-host the pinned flipnote.js build and change the script URL in `webapp.py`. Opening the page calls `Database.AddView()` before the Flipnote row is reloaded, so the displayed view count includes the current request. The page then loads up to 100 comments, the channel record, the Creator's Room record and a short list of other non-deleted Flipnotes from the same room. @@ -272,7 +282,7 @@ SQLite holds metadata; PPM bytes are kept as files. A Flipnote path is construct database/Creators//.ppm -The PPM filename stored in SQLite does not include `.ppm`. `GetFlipnotePPM()` reads the whole file. `GetFlipnoteTMB()` reads only the first `0x6A0` bytes for menu thumbnails. +The PPM filename stored in SQLite does not include `.ppm`. `GetFlipnotePPM()` reads the whole file. `GetFlipnoteTMB()` reads only the first `0x6A0` bytes for menu thumbnails. Browser playback does not create another media file: `/media/.ppm` reads and returns this same PPM, while `/flipnote/.ppm` reads the same file through the counted download path. Mini comments use: @@ -487,6 +497,6 @@ OSE is not the production RexiMemo service. The public repository does not conta > **Why the simplification exists.** This is a deliberate release-safety boundary. Security-sensitive production systems, secrets, trust checks and anti-abuse implementation details are excluded so publishing OSE is less likely to disclose or couple itself to RexiMemo's live infrastructure. The public replacements are intentionally smaller and easier to inspect. They exist to reduce the amount of sensitive production material exposed by an open-source release, not because a simpler login, ban or verification mechanism is inherently more secure. -The remaining code should be read with that boundary in mind. An OSE Creator's Room is an account-or-IP ownership record; a login is an IP-to-account database row; a ban is an exact IP row; a web Flipnote preview is frame zero; and the server accepts the old proxy-style DSi HTTP path rather than reproducing the removed network/authentication stack. +The remaining code should be read with that boundary in mind. An OSE Creator's Room is an account-or-IP ownership record; a login is an IP-to-account database row; a ban is an exact IP row; browser playback is a client-side flipnote.js player with a frame-zero fallback; and the server accepts the old proxy-style DSi HTTP path rather than reproducing the removed network/authentication stack. For public deployment work, the places most likely to need replacement or expansion are the password hashing, IP login identity, proxy/reverse-proxy IP handling, transport/authentication boundary, request-rate controls, database migrations, moderation controls, content limits/backups and operational logging. Those are outside the scope of this repository rather than hidden behind the Docs flag. diff --git a/README.md b/README.md index ed2f1ef..e943ad4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RexiMemo: Open Source Edition -RexiMemo OSE is a deliberately reduced public edition of the RexiMemo Flipnote Hatena replacement server. It keeps the old proxy-style DSi connection model, a SQLite database, Flipnote browsing, posting from Flipnote Studio, Creator's Rooms, stars, downloads, web text comments, and one-frame mini Flipnote comments. +RexiMemo OSE is a deliberately reduced public edition of the RexiMemo Flipnote Hatena replacement server. It keeps the old proxy-style DSi connection model, a SQLite database, Flipnote browsing and browser playback, posting from Flipnote Studio, Creator's Rooms, stars, downloads, web text comments, and one-frame mini Flipnote comments. This is not a source dump of the production service. Production-only security, privacy, identity, moderation, operations, migration and community systems have been removed before release so the public repository does not expose sensitive deployment logic, credentials, private integration code or production data-handling machinery. The trade-off is important: OSE is smaller and easier to inspect, but some of its remaining mechanisms are intentionally basic and are not suitable for a hardened public service without further work. @@ -11,6 +11,7 @@ The server does not contain Nintendo NAS, DNS interception, SSLv3/TLS gateway co - SQLite storage instead of the old plaintext `flipnotes.dat` database. - The Flipnote Studio proxy resource tree and PPM/TMB/UGO/NTFT handling. - Browse pages on DSi and web, plus a small About page listing open-source credits. +- In-browser PPM playback through flipnote.js, with play/pause, seeking, sound and loop controls plus a first-frame fallback. - Flipnote posting from Flipnote Studio. Web PPM upload is not included. - Creator's Rooms. A room is created automatically the first time an account or guest IP posts a Flipnote. - Optional username/password accounts. @@ -62,6 +63,16 @@ password: alpine Change or remove that account before using the server anywhere outside a local test setup. +## Web Flipnote playback + +The Flipnote view page uses **flipnote.js 6.3.1** for browser playback. The library is loaded from jsDelivr and `web/static/flipnote-player.js` provides the OSE controls and fallback behaviour. The player decodes the original PPM in the browser; OSE does not create a separate MP4, GIF or audio transcode. + +Playback requests the PPM from `/media/.ppm`. That route returns the original file inline and does **not** increment the Flipnote's download counter. The **Download PPM** button continues to use `/flipnote/.ppm`, which increments the download counter and serves the file as an attachment. This keeps ordinary watch-page playback separate from an explicit file download. + +If flipnote.js cannot load, the browser cannot create its playback canvas, or parsing/playback fails, the page keeps the server-generated first-frame PNG visible and disables the playback controls rather than replacing the rest of the watch page. + +The flipnote.js library is currently requested from a third-party CDN, so a browser opening a watch page makes a request to jsDelivr for that JavaScript file. The PPM itself is fetched from the OSE server and is not sent to jsDelivr by this implementation. If an installation should not make third-party asset requests, host the pinned flipnote.js file locally and change the script source in `webapp.py`. + ## Accounts and Creator's Rooms Accounts are optional. Signing in associates the current source IP with an account. A Flipnote posted from that IP is attributed to the account and creates its Creator's Room if one does not exist yet. If no account is signed in, OSE creates a guest Creator's Room for the posting IP and uses the author name embedded in the Flipnote where available. @@ -98,6 +109,7 @@ RexiMemo OSE builds on earlier Flipnote preservation and replacement-server work - **[Twisted](https://twisted.org/)** — MIT. The networking and HTTP framework used by the server. - **[NumPy](https://numpy.org/)** — BSD-3-Clause. Used by the legacy Flipnote media and format tools. - **[Pillow](https://python-pillow.github.io/)** — MIT-CMU. Used for image decoding and thumbnail generation. +- **[flipnote.js](https://github.com/jaames/flipnote.js)** — MIT, by James Daniel. Used for PPM animation and audio playback in the browser. ### Acknowledgements diff --git a/docs_page.py b/docs_page.py index 81ff071..4eaf554 100644 --- a/docs_page.py +++ b/docs_page.py @@ -55,7 +55,7 @@ python3 server.py server.pyProcess entry point, port selection, optional feature flags, logging and the proxy-compatible Twisted Site. hatena.pyTop-level HTTP resource router. It separates web traffic from DSi traffic, serves static CSS/images, loads the hatenadir resource tree and applies IP bans before either surface is reached. reximemo.pyShared DSi helpers: regional URLs, current IP/account lookup, DSi keyboard values, DSi dialog/forward headers, UGO menu construction, pagination, PPM first-frame rendering and mini-Flipnote NPF conversion. -webapp.pyThe normal browser client. It renders Home, Browse, About, optional Docs, Flipnote, Creator's Room, account and admin pages and exposes thumbnails/PPM downloads. +webapp.pyThe normal browser client. It renders Home, Browse, About, optional Docs, Flipnote, Creator's Room, account and admin pages and exposes thumbnails, inline PPM playback media and counted PPM downloads. docs_page.pyThe text of this built-in manual. It is imported by the web client only when the documentation route is requested. DB.pyCompatibility shim used by the older-style resource modules. It imports the single SQLite Database object and advertises DB_type = "sqlite". database/__init__.pySQLite schema, seed data and all database operations used by the OSE server. @@ -70,7 +70,8 @@ python3 server.py hatenadir/images/ds/account.ntft32×32 generated Account menu icon. hatenadir/images/ds/room.ntft32×32 generated Creator's Room menu icon. hatenadir/images/ds/placeholder.npf128×64 generated indexed placeholder banner retained for DSi asset testing/use. -web/static/site.cssComplete browser stylesheet: colour variables, header/navigation, Flipnote grids, richer watch layout, comments, forms, admin tables, documentation layout and responsive breakpoints. +web/static/site.cssComplete browser stylesheet: colour variables, header/navigation, Flipnote grids, watch/player layout, playback controls, comments, forms, admin tables, documentation layout and responsive breakpoints. +web/static/flipnote-player.jsSmall browser-side controller around flipnote.js. It loads the PPM, updates play/pause, seek, sound, loop and time controls, and restores the first-frame fallback if playback is unavailable. web/static/RexiMemo_OSE_Logo.pngWhite RexiMemo logo with “Open Source Edition” included in the image itself. Hatenatools/__init__.pyConvenience exports for PPM, TMB, UGO and NTFT. Hatenatools/PPM.pyLegacy Flipnote Studio PPM/TMB parser: metadata, thumbnail data, frame decoding, sound decoding and helper dump functions. OSE mainly uses PPM.Read(), PPM.GetFrame() and TMB.Read(). @@ -115,6 +116,7 @@ python3 server.py /watch/<public id>/commentPOSTAdds a text comment using the current account or guest-IP Creator's Room. /thumb/<public id>.pngGETDecodes the first PPM frame and returns a cached PNG thumbnail. /comment-thumb/<id>.pngGETReturns a first-frame PNG preview for a mini-Flipnote comment. +/media/<public id>.ppmGETReturns the original PPM inline for browser playback. It does not increment the download counter. /flipnote/<public id>.ppmGETIncrements downloads and returns the original PPM as an attachment. /creator/<public id>GETCreator's Room with its Flipnote grid and account/guest ownership label. /loginGET/POSTUsername/password sign-in. Successful POST writes an ip_logins row for the source IP. @@ -131,7 +133,11 @@ python3 server.py

Flipnote view page

-

The browser does not attempt to reproduce Flipnote Studio playback. A Flipnote view uses the first decoded frame as a large preview and presents the original .ppm as the downloadable playable file. This keeps the web client small and avoids adding a separate animation/audio player implementation.

+

The watch page has browser playback as well as the server-generated first-frame fallback. webapp.py loads the pinned flipnote.js 6.3.1 browser build from jsDelivr, then loads the local web/static/flipnote-player.js controller. The controller creates a flipnote.Player at 320×240, asks it to load /media/<public id>.ppm, and lets flipnote.js decode the original PPM animation and audio in the browser. OSE does not pre-render a video, GIF or separate audio file.

+

The player starts paused. Once flipnote.js reports that the note is ready, OSE hides the first-frame image and enables the large play button and the control row. The controls provide play/pause, a seek range, elapsed/total time, sound mute/unmute and loop on/off. Seeking uses flipnote.js's startSeek(), seek() and endSeek() calls so playback can resume cleanly after dragging. Clicking the rendered Flipnote toggles playback after the player has been started once.

+

/media/<public id>.ppm is deliberately separate from /flipnote/<public id>.ppm. The media route returns the same on-disk PPM with Content-Disposition: inline and a one-hour public cache header, but it does not call Database.AddDownload(). The download route remains the explicit Download PPM action, increments the download counter and returns the file as an attachment. Opening or replaying a watch page therefore does not count as a file download.

+

The <img> first-frame preview remains in the page before JavaScript runs. If the flipnote.js global is missing, the browser cannot construct its canvas/WebAudio player, the PPM cannot be parsed, or playback reports an error, flipnote-player.js leaves that preview visible, disables the player controls and shows a short playback-unavailable message. The rest of the watch page still works normally. This also gives the page a useful non-JavaScript fallback.

+
Browser dependency/privacy notice. The playback library is loaded from the jsDelivr CDN rather than copied into OSE. This keeps a large third-party build out of the repository, but it means a browser visiting a watch page makes a request to jsDelivr for flipnote.min.js. The PPM URL itself is same-origin and is not submitted to jsDelivr by OSE. An operator who does not want that third-party browser request should self-host the pinned flipnote.js build and change the script URL in webapp.py.

Opening the page calls Database.AddView() before the Flipnote row is reloaded, so the displayed view count includes the current request. The page then loads up to 100 comments, the channel record, the Creator's Room record and a short list of other non-deleted Flipnotes from the same room.

The information panel displays stars, views, downloads and comment count, then the post time, channel, creator FSID, PPM filename and public identifier. These values come directly from the OSE SQLite row; there is no secondary profile, device or ownership lookup.

Metadata notice. Creator metadata shown here is repository data, not a cryptographically verified identity. OSE deliberately avoids the production ownership/device verification chain so those security mechanisms are not exposed in the public codebase. Treat FSIDs, embedded creator names and room ownership as local OSE identifiers.
@@ -237,7 +243,7 @@ python3 server.py

On-disk media

SQLite holds metadata; PPM bytes are kept as files. A Flipnote path is constructed as:

database/Creators/<upper-case FSID>/<filename>.ppm
-

The PPM filename stored in SQLite does not include .ppm. GetFlipnotePPM() reads the whole file. GetFlipnoteTMB() reads only the first 0x6A0 bytes for menu thumbnails.

+

The PPM filename stored in SQLite does not include .ppm. GetFlipnotePPM() reads the whole file. GetFlipnoteTMB() reads only the first 0x6A0 bytes for menu thumbnails. Browser playback does not create another media file: /media/<public id>.ppm reads and returns this same PPM, while /flipnote/<public id>.ppm reads the same file through the counted download path.

Mini comments use:

database/Comments/<numeric comment id>.ppm

The database stores that basename in memo_filename. The numeric filename is assigned after the row insert because the SQLite row ID is used as the stable file name.

@@ -410,7 +416,7 @@ python3 server.py

Deliberately absent systems

OSE is not the production RexiMemo service. The public repository does not contain Nintendo NAS/DNS/auth gateway code, certificate/private-key material, production session/device management, FSID ownership proof, PPM signature verification, emulator/bot detection, passkeys, profile pictures, Creator's Room themes, experiments, Sudomemo migration, Discord integration, support-centre flows, policy/terms pages, notifications, production API tokens, web PPM upload, advanced moderation cases or the production moderation toolset.

Why the simplification exists. This is a deliberate release-safety boundary. Security-sensitive production systems, secrets, trust checks and anti-abuse implementation details are excluded so publishing OSE is less likely to disclose or couple itself to RexiMemo's live infrastructure. The public replacements are intentionally smaller and easier to inspect. They exist to reduce the amount of sensitive production material exposed by an open-source release, not because a simpler login, ban or verification mechanism is inherently more secure.
-

The remaining code should be read with that boundary in mind. An OSE Creator's Room is an account-or-IP ownership record; a login is an IP-to-account database row; a ban is an exact IP row; a web Flipnote preview is frame zero; and the server accepts the old proxy-style DSi HTTP path rather than reproducing the removed network/authentication stack.

+

The remaining code should be read with that boundary in mind. An OSE Creator's Room is an account-or-IP ownership record; a login is an IP-to-account database row; a ban is an exact IP row; browser playback is a client-side flipnote.js player with a frame-zero fallback; and the server accepts the old proxy-style DSi HTTP path rather than reproducing the removed network/authentication stack.

For public deployment work, the places most likely to need replacement or expansion are the password hashing, IP login identity, proxy/reverse-proxy IP handling, transport/authentication boundary, request-rate controls, database migrations, moderation controls, content limits/backups and operational logging. Those are outside the scope of this repository rather than hidden behind the Docs flag.