* 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
162 lines
5.6 KiB
Python
162 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import atexit
|
|
import os
|
|
|
|
try:
|
|
from twisted.internet import reactor
|
|
except ImportError: # Allows the database module to be used/tested without Twisted installed.
|
|
reactor = None
|
|
|
|
from Hatenatools import TMB
|
|
|
|
|
|
class Database:
|
|
"""Plain-text Flipnote database used by the original server.
|
|
|
|
This backend is intentionally single-process, matching the original design.
|
|
"""
|
|
|
|
def __init__(self):
|
|
newest_path = "database/new_flipnotes.dat"
|
|
if os.path.exists(newest_path):
|
|
with open(newest_path, "r", encoding="utf-8", newline="") as f:
|
|
raw = f.read()
|
|
else:
|
|
raw = ""
|
|
|
|
self.Newest = [tuple(line.split("\t")) for line in raw.splitlines() if line] if raw else []
|
|
self.Creator = {}
|
|
|
|
self.new = False
|
|
self.Views = 0
|
|
self.Stars = 0
|
|
self.Downloads = 0
|
|
|
|
if reactor is not None:
|
|
reactor.callLater(60 * 3, self.flusher)
|
|
atexit.register(self.write)
|
|
|
|
def flusher(self):
|
|
if reactor is not None:
|
|
reactor.callLater(60 * 3, self.flusher)
|
|
self.write()
|
|
|
|
def write(self):
|
|
if self.new:
|
|
if len(self.Newest) > 5000:
|
|
self.Newest = self.Newest[:5000]
|
|
with open("database/new_flipnotes.dat", "w", encoding="utf-8", newline="") as f:
|
|
f.write("\n".join("\t".join(map(str, row)) for row in self.Newest))
|
|
self.new = False
|
|
|
|
for creator_id in list(self.Creator.keys()):
|
|
creator_dir = os.path.join("database", "Creators", creator_id)
|
|
os.makedirs(creator_dir, exist_ok=True)
|
|
with open(os.path.join(creator_dir, "flipnotes.dat"), "w", encoding="utf-8", newline="") as f:
|
|
f.write("\n".join("\t".join(map(str, row)) for row in self.Creator[creator_id]))
|
|
del self.Creator[creator_id]
|
|
|
|
def CreatorExists(self, CreatorID):
|
|
return os.path.exists(os.path.join("database", "Creators", CreatorID)) or CreatorID in self.Creator
|
|
|
|
def FlipnoteExists(self, CreatorID, filename):
|
|
return os.path.exists(self.FlipnotePath(CreatorID, filename))
|
|
|
|
def GetCreator(self, CreatorID, Store=False):
|
|
if CreatorID in self.Creator:
|
|
return self.Creator[CreatorID]
|
|
|
|
creator_dir = os.path.join("database", "Creators", CreatorID)
|
|
if not os.path.exists(creator_dir):
|
|
return None
|
|
|
|
with open(os.path.join(creator_dir, "flipnotes.dat"), "r", encoding="utf-8", newline="") as f:
|
|
raw = f.read()
|
|
ret = [line.split("\t") for line in raw.splitlines() if line]
|
|
|
|
# Current format:
|
|
# filename, views, stars, green, red, blue, purple, channel, downloads
|
|
for row in ret:
|
|
filename = row[0]
|
|
defaults = (filename, 0, 0, 0, 0, 0, 0, "", 0)
|
|
while len(row) < len(defaults):
|
|
row.append(defaults[len(row)])
|
|
|
|
if Store:
|
|
self.Creator[CreatorID] = ret
|
|
return ret
|
|
|
|
def GetFlipnote(self, CreatorID, filename, Store=False):
|
|
for row in self.GetCreator(CreatorID, Store) or []:
|
|
if row[0] == filename:
|
|
return row
|
|
return False
|
|
|
|
def GetFlipnotePPM(self, CreatorID, filename):
|
|
with open(self.FlipnotePath(CreatorID, filename), "rb") as f:
|
|
return f.read()
|
|
|
|
def GetFlipnoteTMB(self, CreatorID, filename):
|
|
with open(self.FlipnotePath(CreatorID, filename), "rb") as f:
|
|
return f.read(0x6A0)
|
|
|
|
def AddFlipnote(self, content, Channel=""):
|
|
content = bytes(content)
|
|
tmb = TMB().Read(content)
|
|
if not tmb:
|
|
return False
|
|
|
|
CreatorID = tmb.EditorAuthorID
|
|
filename = tmb.CurrentFilename[:-4]
|
|
if self.FlipnoteExists(CreatorID, filename):
|
|
return False
|
|
|
|
self.new = True
|
|
self.Newest.insert(0, (CreatorID, filename))
|
|
|
|
if not self.GetCreator(CreatorID, True):
|
|
self.Creator[CreatorID] = [[filename, 0, 0, 0, 0, 0, 0, Channel, 0]]
|
|
else:
|
|
self.Creator[CreatorID].append([filename, 0, 0, 0, 0, 0, 0, Channel, 0])
|
|
|
|
creator_dir = os.path.join("database", "Creators", CreatorID)
|
|
os.makedirs(creator_dir, exist_ok=True)
|
|
with open(self.FlipnotePath(CreatorID, filename), "wb") as f:
|
|
f.write(content)
|
|
return CreatorID, filename
|
|
|
|
def AddView(self, CreatorID, filename):
|
|
for i, flipnote in enumerate(self.GetCreator(CreatorID, True) or []):
|
|
if flipnote[0] == filename:
|
|
self.Creator[CreatorID][i][1] = int(flipnote[1]) + 1
|
|
self.Views += 1
|
|
return True
|
|
return False
|
|
|
|
def AddStar(self, CreatorID, filename, amount=1, color="yellow"):
|
|
starindices = {"yellow": 2, "green": 3, "red": 4, "blue": 5, "purple": 6}
|
|
if color not in starindices:
|
|
return False
|
|
for i, flipnote in enumerate(self.GetCreator(CreatorID, True) or []):
|
|
if flipnote[0] == filename:
|
|
idx = starindices[color]
|
|
self.Creator[CreatorID][i][idx] = int(flipnote[idx]) + amount
|
|
self.Stars += 1
|
|
return True
|
|
return False
|
|
|
|
def AddDownload(self, CreatorID, filename):
|
|
for i, flipnote in enumerate(self.GetCreator(CreatorID, True) or []):
|
|
if flipnote[0] == filename:
|
|
self.Creator[CreatorID][i][8] = int(flipnote[8]) + 1
|
|
self.Downloads += 1
|
|
return True
|
|
return False
|
|
|
|
def FlipnotePath(self, CreatorID, filename):
|
|
return os.path.join("database", "Creators", CreatorID, filename + ".ppm")
|
|
|
|
|
|
Database = Database()
|
