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:
Rekushi authored and GitHub committed 2026-08-31 20:33:10 +01:00
1 parent 687a2bcb43
commit e4bb0b00ba
48 files changed
+3463

No files matched your search

@@ -0,0 +1 @@
74CC1F_0D3AA40F2B3A0_000 2 0 0 0 0 0 0
+161
View File
@@ -0,0 +1,161 @@
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()
+1
View File
@@ -0,0 +1 @@
913304C0CC74CC1F 74CC1F_0D3AA40F2B3A0_000
+830
View File
@@ -0,0 +1,830 @@
@charset "UTF-8";
table.profbox {
background-color: #82c300;
color: #ffffff;
}
div.prof_out {
margin: 0 11px 0 11px;
background-color: #82c300;
border-style: solid;
border-width: 0 1px 0 1px;
border-color: #82c300;
}
span.more-f{
color: #ffffff;
}
div.notice2 {
margin:0 11px 5px 11px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#82c300;
background-color:#fbfbfb;
}
span.url{
text-decoration:underline;
}
span.comptxt {
color:#ff8a00;
}
span.star0 {
color:#fdc556;
}
span.star1 {
color:#138200;
}
span.star2 {
color:#ff455c;
}
span.star3 {
color:#00b0f9;
}
span.star4 {
color:#a411b9;
}
span.star0c {
color:#fdc556;
}
span.star1c {
color:#138200;
}
span.star2c {
color:#ff455c;
}
span.star3c {
color:#00b0f9;
}
span.star4c {
color:#a411b9;
}
span.orange{
color: #ff8d45;
}
span.green {
color: #82c300;
}
span.gray{
color: #666666;
}
span.white {
color:#FFFFFF;
}
html {
margin:0;
padding:0;
}
body {
color:#333333;
margin:0;
padding:0;
}
div.title {
margin:0 7px 0 7px;
padding:7px 5px 7px 5px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.description {
margin:0 7px 0 7px;
padding:7px 5px 7px 5px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.hr {
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.hr-tab {
border-style:solid;
border-width:0 0 1px 0;
border-color:#82c300;
}
span.timestamp {
color:#999999;
}
a.timestamp {
color:#999999;
}
a.violation {
color:#333333;
}
div.box {
padding:7px;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
background-color:#FFFFFF;
}
div.more {
margin:7px 0 0 0;
}
div.pager {
padding:5px;
}
div.num {
margin:5px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
background-color:#FFFFFF;
}
span.new {
color: #cc0000;
}
span.price {
color: #92cb22;
}
span.date {
color: #cccccc;
}
table.detail{
margin:0 7px 0 7px;
}
table.fullsize {
margin:0;
}
div.item-term {
padding:3px 0 3px 5px;
}
div.item-value {
padding:3px 5px 3px 0;
}
div.item-text {
padding:3px 5px 3px 15px;
}
div.item-tcom {
padding: 3px 5px 0 5px;
}
div.item-com {
padding: 0 5px 0 5px;
}
div.td-body {
padding:3px 5px 3px 5px;
}
div.comlist {
background-color:#FFFFFF;
}
div.comment-text {
padding:3px 5px 5px 15px;
}
div.comment-memo {
padding:0 0 0 15px;
}
div.staff {
color:#FFFFFF;
background-color:#1a1a1a;
}
a.staffnam {
color:#FFFFFF;
}
div.notice {
width:226px;
margin:7px 7px 3px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.colorstar {
margin:7px 0 7px 0;
}
div.code-box{
width:226px;
margin:7px 0 0 0;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.code-data{
margin:7px 0 7px 0;
}
div.mail-reply {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.lett_txt {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0 2px;
border-color:#92d300;
background-color:#ffffff;
}
div.lett_to {
margin: 0 11px 0 11px;
background-color:#92d300;
color:#ffffff;
padding: 1px 0 6px 5px;
}
div.pad0{
padding: 0 0 0 0;
}
div.pad5t{
padding: 5px 0 0 0;
}
div.pad5b{
padding: 0 0 5px 0;
}
div.pad1b{
padding: 0 0 1px 0;
}
div.pad2b{
padding: 0 0 2px 0;
}
div.pad2{
padding: 2px 2px 2px 2px;
}
div.pad5{
padding: 5px 5px 5px 5px;
}
div.pad0-5{
padding: 0 5px 0 5px;
}
td.pad2{
padding: 2px 2px 2px 2px;
}
div.pad3-5{
padding: 3px 5px 3px 5px;
}
div.lett_from{
padding: 3px 11px 3px 0;
}
div.pad11{
padding: 11px 11px 11px 11px;
}
div.pad5-11{
padding: 5px 11px 5px 11px;
}
div.pad10b{
padding: 0 0 10px 0;
}
div.pad20b{
padding: 0 0 20px 0;
}
div.mar0-7{
margin: 0px 7px 0px 7px;
}
div.mar0-11{
margin: 0px 11px 0px 11px;
}
div.mar0{
margin: 0px 0px 0px 0px;
}
div.mar5 {
margin: 5px 5px 5px 5px;
}
div.yaritori{
margin: 0 11px 0 11px;
background-color:#ffffff;
border-style:solid;
border-width:0 2px 0 2px;
border-color:#92d300;
}
div.gr-tit{
margin: 0 11px 0 11px;
background-color:#92d300;
color:#ffffff;
}
div.kadomaru{
margin: 0 11px 0 11px;
}
div.lett-note{
margin: 5px 11px 5px 11px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92d300;
background-color:#ffffff;
}
div.pk{
background-color:#ff3ab7;
}
div.blue{
background-color:#3acfff;
}
div.gray{
background-color:#cccccc;
}
div.gray_box{
padding:5px;
border-style:solid;
border-width:1px;
border-color:#cccccc;
background-color:#ffffff;
}
div.vio-intro {
width:240px;
padding:7px 0 7px 7px;
}
div.vio-memo {
padding:0 0 10px 15px;
}
div.vio-text {
padding:0 0 10px 15px;
}
div.vio-note {
color:#999999;
padding:0 0 10px 15px;
}
div.vio-submit {
padding:10px 0 10px 0;
}
div.info-head {
padding:5px 7px 5px 7px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
background-color:#eeeeee;
}
div.noborder{
border-style:solid;
border-color:#CCCCCC;
border-width:0 0 0 0;
}
div.info-body {
padding:7px;
}
div.info-msg {
margin:0 0 7px 0;
}
div.news-head {
padding:5px 7px 5px 7px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
background-color:#eeeeee;
}
div.news-body {
padding:7px;
}
div.news-h1 {
margin:7px 7px 0 7px;
padding:7px;
border-style:solid;
border-width:3px;
border-color:#91C900;
}
div.news-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#91C900;
}
img.news-memo {
margin:0 10px 5px 0;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
}
div.info-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#91C900;
}
div.num-box{
background-color:#cccccc;
margin:0 30px;
}
td.tit-star{
background-color:#e9f8c3;
}
img.help{
border-style:solid;
border-width:2px;
border-color:#ffffff;
}
div.list{
padding:0 0 5px 0;
}
div.star-det{
padding:5px 0 5px 0;
}
img.sc{
border-style:solid;
border-width:1px;
border-color:#cccccc;
}
td.border {
background-color: #82c300;
}
div.border {
border-style: solid;
border-width: 0 0 5px 0;
border-color: #82c300;
}
div.border-c {
border-style: solid;
border-width: 0 0 2px 0;
border-color: #82c300;
}
td.space {
background-color: #82c300;
color: #82c300;
}
td.tabon {
color: #82c300;
}
td.taboff {
background-color: #82c300;
color: #ffffff;
}
td.tabpager {
background-color: #82c300;
color: #ffffff;
}
div.on {
padding: 1px 6px 3px 6px;
}
span.border {
color: #b2db59;
}
span.border-gray {
color: #cccccc;
}
a.taboff {
color: #ffffff;
}
div.comme {
padding: 3px 3px 3px 12px;
color: #b2db59;
}
span.text {
color: #333333;
}
td.gray {
background-color: #f2f2f2;
}
div.message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.error-message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#F5C8A7;
background-color:#FFEED8;
color:#9B2E2E;
}
div.ok-message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#C5DABA;
background-color:#E3F9DB;
color:#517B29;
}
div.guide-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#66ccff;
}
div.guide-h3 {
padding:3px 3px 3px 3px;
background-color:#66ccff;
color: #ffffff;
}
div.select-t {
color: #66ccff;
padding: 0 0 3px 0;
}
div.select-b {
padding:5px 3px 5px 3px;
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #99ccff;
background-color: #ffffff;
}
span.border-s {
color: #99ccff;
}
span.color {
color: #ffffff;
}
div.credit {
padding:5px 7px 5px 7px;
background-color:#eeeeee;
}
div.rule-tit {
background-color: #82c300;
color: #ffffff;
padding: 3px 3px 3px 3px;
}
div.rule-ind {
margin: 0 0 0 10px;
}
div.hr-prof {
border-style:solid;
border-width:0 0 3px 0;
border-color:transparent;
}
table.send {
margin: 5px 0 0 0 ;
}
div.icon {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #cccccc;
}
td.username {
height:20px;
}
td.mystatus {
}
div.prev {
padding: 1px 0 5px 0;
}
div.next {
padding: 5px 0 1px 0;
}
div.prof-det{
padding: 0 5px 0 5px;
}
div.catch {
margin: 5px 5px 5px 5px;
padding: 5px 5px 5px 5px;
border-style: solid;
border-width: 2px 2px 2px 2px;
border-color: #ffffff;
}
a.still {
color: #ffffff;
}
div.head {
margin: 0px 0 0 0;
}
div.send {
margin: 0px 10px 3px 11px;
}
div.link {
margin: 0px 10px 0 11px;
}
img.button {
margin: 5px 0 0 0;
}
td.theme {
margin: 0 0 0 3px;
}
div.trial-h1 {
margin: 5px 0 5px 0;
padding:3px;
border-style:solid;
border-width:3px;
border-color:#91C900;
}
div.mar-b10{
margin:0 0 10px 0;
}
div.cam1{
margin:7px 7px 0 7px;
padding:7px;
background-color: #ff2828;
color: #ffffff;
}
div.cam2{
margin:0 7px 0 7px;
padding:7px;
border-style:solid;
border-width: 2px 2px 2px 2px;
border-color: #ff2828;
}
table.xmas2009{
margin: 0 10px 0 11px;
background-color: #c70606;
color: #ffffff;
}
div.yellow {
margin: 0 11px 0 11px;
background-color:#ffe800;
}
div.sttr_mes {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_gacha {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#ea475c;
background-color:#ffffff;
}
div.compsp {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#ffe800;
background-color:#fff9b7;
}
div.sttr_war {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#F5C8A7;
background-color:#FFEED8;
color:#9B2E2E;
}
div.sttr_price {
background-color:#FF9933;
color: #ffffff;
}
div.sttr_item {
width:226px;
margin:0px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_theme {
width:226px;
margin:0px 7px 3px 7px;
padding:5px;
border-style:solid;
border-width:0px 1px 1px 1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_back {
width:226px;
margin:0px 7px 0px 7px;
}
div.gift_l {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0px 2px;
border-color:#ff5252;
}
div.gift_h {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0px 2px;
border-color:#ffd616;
}
img.shop_icon {
margin:0 1px 1px 0;
}
div.help_guide {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#ffac3e;
}
div.guide-head {
color: #ffffff;
background-color: #5abcea;
}
div.guide-title {
color: #ffffff;
background-color: #5abcea;
}
div.guide-main {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#5abcea;
}
div.green-title {
color: #ffffff;
background-color: #91C900;
}
div.green-main {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#91C900;
}
table.method{
border-style:solid;
border-width:1px;
border-color:#cccccc;
}
div.search {
background-color: #eeeeee;
border-style: solid;
border-width: 1px;
border-color:#cccccc;
}
td.keyboard {
background-color: #eeeeee;
}
table.key-tab{
margin: 0 6px 0 6px;
}
div.keyout {
margin: 0 7px 0 7px;
border-style: solid;
border-width: 0 1px 1px 1px;
border-color:#cccccc;
}
div.on-ja {
border-style: solid;
border-width: 1px 1px 0 1px;
border-color:#cccccc;
}
div.off-en {
border-style: solid;
border-width: 1px 1px 0 0;
border-color:#cccccc;
}
div.off-aa {
border-style: solid;
border-width: 1px 1px 0 0;
border-color:#cccccc;
}
table.camp-detail {
border-style: solid;
border-width: 1px;
border-color:#cccccc;
}
table.center {
margin: 0 15px 0 16px;
}
div.codeborder {
border-style: solid;
border-width:0 1px 1px 1px;
border-color:#82c300;
}
div.codeprev {
border-style: solid;
border-width:1px;
border-color:#82c300;
}
td.code {
background-color:#ffffff;
}
table.codetab {
border-style: solid;
border-width: 1px 1px 0 1px;
border-color:#82c300;
}
div.codetab {
border-style: solid;
border-width: 0 1px 1px 1px;
border-color:#82c300;
background-color:#ffffff;
}
div.c_tab2 {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color:#82c300;
background-color:#ffffff;
}
div.topic {
border-style:solid;
border-width:1px;
border-color:#fff273;
background-color:#fff8b2;
}
div.icon-green {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #82c300;
}
div.p-block {
border-style:solid;
border-width:1px;
border-color:#000000;
color:#000000;
background-color:#ffffff;
}
div.clstar-help{
background-color:#ffffff;
color:#82c300;
}
div.ugo4th{
border-style:solid;
border-width:1px;
border-color:#c4260f;
background-color:#d52515;
}
+863
View File
@@ -0,0 +1,863 @@
@charset "UTF-8";
div.head {
margin: 10px 0 0 0;
}
a {
color:#e63333;
}
table.profbox {
background-color: #353535;
color: #ffffff;
}
div.prof_out {
margin: 0 11px 0 11px;
background-color: #353535;
border-style: solid;
border-width: 0 1px 0 1px;
border-color: #6c8ff2;
}
span.more-f{
color: #e63333;
}
div.notice2 {
margin:0 11px 5px 11px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#139a2f;
background-color:#ffffff;
}
td.border{
background-color: #000000;
}
div.border {
border-style: solid;
border-width: 0 0 5px 0;
border-color: #000000;
}
div.border-c {
border-style: solid;
border-width: 0 0 2px 0;
border-color: #000000;
}
td.space {
background-color: #000000;
}
td.tabon {
color: #ed5151;
}
td.taboff {
background-color: #000000;
color: #ed5151;
}
td.tabpager {
background-color: #000000;
color: #ed5151;
}
span.url{
text-decoration:underline;
}
span.comptxt {
color:#ff8a00;
}
span.star0 {
color:#fdc556;
}
span.star1 {
color:#138200;
}
span.star2 {
color:#ff455c;
}
span.star3 {
color:#00b0f9;
}
span.star4 {
color:#a411b9;
}
span.star0c {
color:#fdc556;
}
span.star1c {
color:#138200;
}
span.star2c {
color:#ff455c;
}
span.star3c {
color:#00b0f9;
}
span.star4c {
color:#a411b9;
}
span.orange{
color: #ff8d45;
}
span.green {
color: #82c300;
}
span.gray{
color: #666666;
}
span.white {
color:#FFFFFF;
}
html {
margin:0;
padding:0;
}
body {
color:#333333;
margin:0;
padding:0;
}
div.title {
margin:0 7px 0 7px;
padding:7px 5px 7px 5px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.description {
margin:0 7px 0 7px;
padding:7px 5px 7px 5px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.hr {
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.hr-tab {
border-style:solid;
border-width:0 0 1px 0;
border-color:#82c300;
}
span.timestamp {
color:#999999;
}
a.timestamp {
color:#999999;
}
a.violation {
color:#333333;
}
div.box {
padding:7px;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
background-color:#FFFFFF;
}
div.more {
margin:7px 0 0 0;
}
div.pager {
padding:5px;
}
div.num {
margin:5px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
background-color:#FFFFFF;
}
span.new {
color: #cc0000;
}
span.price {
color: #92cb22;
}
span.date {
color: #cccccc;
}
table.detail{
margin:0 7px 0 7px;
}
table.fullsize {
margin:0;
}
div.item-term {
padding:3px 0 3px 5px;
}
div.item-value {
padding:3px 5px 3px 0;
}
div.item-text {
padding:3px 5px 3px 15px;
}
div.item-tcom {
padding: 3px 5px 0 5px;
}
div.item-com {
padding: 0 5px 0 5px;
}
div.td-body {
padding:3px 5px 3px 5px;
}
div.comlist {
background-color:#FFFFFF;
}
div.comment-text {
padding:3px 5px 5px 15px;
}
div.comment-memo {
padding:0 0 0 15px;
}
div.staff {
color:#FFFFFF;
background-color:#1a1a1a;
}
a.staffnam {
color:#FFFFFF;
}
div.notice {
width:226px;
margin:7px 7px 3px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.colorstar {
margin:7px 0 7px 0;
}
div.code-box{
width:226px;
margin:7px 0 0 0;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.code-data{
margin:7px 0 7px 0;
}
div.mail-reply {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.lett_txt {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0 2px;
border-color:#92d300;
background-color:#ffffff;
}
div.lett_to {
margin: 0 11px 0 11px;
background-color:#92d300;
color:#ffffff;
padding: 1px 0 6px 5px;
}
div.pad0{
padding: 0 0 0 0;
}
div.pad5t{
padding: 5px 0 0 0;
}
div.pad5b{
padding: 0 0 5px 0;
}
div.pad1b{
padding: 0 0 1px 0;
}
div.pad2b{
padding: 0 0 2px 0;
}
div.pad2{
padding: 2px 2px 2px 2px;
}
div.pad5{
padding: 5px 5px 5px 5px;
}
div.pad0-5{
padding: 0 5px 0 5px;
}
td.pad2{
padding: 2px 2px 2px 2px;
}
div.pad3-5{
padding: 3px 5px 3px 5px;
}
div.lett_from{
padding: 3px 11px 3px 0;
}
div.pad11{
padding: 11px 11px 11px 11px;
}
div.pad5-11{
padding: 5px 11px 5px 11px;
}
div.pad10b{
padding: 0 0 10px 0;
}
div.pad20b{
padding: 0 0 20px 0;
}
div.mar0-7{
margin: 0px 7px 0px 7px;
}
div.mar0-11{
margin: 0px 11px 0px 11px;
}
div.mar0{
margin: 0px 0px 0px 0px;
}
div.mar5 {
margin: 5px 5px 5px 5px;
}
div.yaritori{
margin: 0 11px 0 11px;
background-color:#ffffff;
border-style:solid;
border-width:0 2px 0 2px;
border-color:#92d300;
}
div.gr-tit{
margin: 0 11px 0 11px;
background-color:#92d300;
color:#ffffff;
}
div.kadomaru{
margin: 0 11px 0 11px;
}
div.lett-note{
margin: 5px 11px 5px 11px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92d300;
background-color:#ffffff;
}
div.pk{
background-color:#ff3ab7;
}
div.blue{
background-color:#3acfff;
}
div.gray{
background-color:#cccccc;
}
div.gray_box{
padding:5px;
border-style:solid;
border-width:1px;
border-color:#cccccc;
background-color:#ffffff;
}
div.vio-intro {
width:240px;
padding:7px 0 7px 7px;
}
div.vio-memo {
padding:0 0 10px 15px;
}
div.vio-text {
padding:0 0 10px 15px;
}
div.vio-note {
color:#999999;
padding:0 0 10px 15px;
}
div.vio-submit {
padding:10px 0 10px 0;
}
div.info-head {
padding:5px 7px 5px 7px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
background-color:#eeeeee;
}
div.noborder{
border-style:solid;
border-color:#CCCCCC;
border-width:0 0 0 0;
}
div.info-body {
padding:7px;
}
div.info-msg {
margin:0 0 7px 0;
}
div.news-head {
padding:5px 7px 5px 7px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
background-color:#eeeeee;
}
div.news-body {
padding:7px;
}
div.news-h1 {
margin:7px 7px 0 7px;
padding:7px;
border-style:solid;
border-width:3px;
border-color:#91C900;
}
div.news-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#91C900;
}
img.news-memo {
margin:0 10px 5px 0;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
}
div.info-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#91C900;
}
div.num-box{
background-color:#cccccc;
margin:0 30px;
}
td.tit-star{
background-color:#e9f8c3;
}
img.help{
border-style:solid;
border-width:2px;
border-color:#ffffff;
}
div.list{
padding:0 0 5px 0;
}
div.star-det{
padding:5px 0 5px 0;
}
img.sc{
border-style:solid;
border-width:1px;
border-color:#cccccc;
}
td.border {
background-color: #82c300;
}
div.border {
border-style: solid;
border-width: 0 0 5px 0;
border-color: #82c300;
}
div.border-c {
border-style: solid;
border-width: 0 0 2px 0;
border-color: #82c300;
}
td.space {
background-color: #82c300;
color: #82c300;
}
td.tabon {
color: #82c300;
}
td.taboff {
background-color: #82c300;
color: #ffffff;
}
td.tabpager {
background-color: #82c300;
color: #ffffff;
}
div.on {
padding: 1px 6px 3px 6px;
}
span.border {
color: #b2db59;
}
span.border-gray {
color: #cccccc;
}
a.taboff {
color: #ffffff;
}
div.comme {
padding: 3px 3px 3px 12px;
color: #b2db59;
}
span.text {
color: #333333;
}
td.gray {
background-color: #f2f2f2;
}
div.message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.error-message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#F5C8A7;
background-color:#FFEED8;
color:#9B2E2E;
}
div.ok-message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#C5DABA;
background-color:#E3F9DB;
color:#517B29;
}
div.guide-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#66ccff;
}
div.guide-h3 {
padding:3px 3px 3px 3px;
background-color:#66ccff;
color: #ffffff;
}
div.select-t {
color: #66ccff;
padding: 0 0 3px 0;
}
div.select-b {
padding:5px 3px 5px 3px;
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #99ccff;
background-color: #ffffff;
}
span.border-s {
color: #99ccff;
}
span.color {
color: #ffffff;
}
div.credit {
padding:5px 7px 5px 7px;
background-color:#eeeeee;
}
div.rule-tit {
background-color: #82c300;
color: #ffffff;
padding: 3px 3px 3px 3px;
}
div.rule-ind {
margin: 0 0 0 10px;
}
div.hr-prof {
border-style:solid;
border-width:0 0 3px 0;
border-color:transparent;
}
table.send {
margin: 5px 0 0 0 ;
}
div.icon {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #cccccc;
}
td.username {
height:20px;
}
td.mystatus {
}
div.prev {
padding: 1px 0 5px 0;
}
div.next {
padding: 5px 0 1px 0;
}
div.prof-det{
padding: 0 5px 0 5px;
}
div.catch {
margin: 5px 5px 5px 5px;
padding: 5px 5px 5px 5px;
border-style: solid;
border-width: 2px 2px 2px 2px;
border-color: #ffffff;
}
a.still {
color: #ffffff;
}
div.head {
margin: 0px 0 0 0;
}
div.send {
margin: 0px 10px 3px 11px;
}
div.link {
margin: 0px 10px 0 11px;
}
img.button {
margin: 5px 0 0 0;
}
td.theme {
margin: 0 0 0 3px;
}
div.trial-h1 {
margin: 5px 0 5px 0;
padding:3px;
border-style:solid;
border-width:3px;
border-color:#91C900;
}
div.mar-b10{
margin:0 0 10px 0;
}
div.cam1{
margin:7px 7px 0 7px;
padding:7px;
background-color: #ff2828;
color: #ffffff;
}
div.cam2{
margin:0 7px 0 7px;
padding:7px;
border-style:solid;
border-width: 2px 2px 2px 2px;
border-color: #ff2828;
}
table.xmas2009{
margin: 0 10px 0 11px;
background-color: #c70606;
color: #ffffff;
}
div.yellow {
margin: 0 11px 0 11px;
background-color:#ffe800;
}
div.sttr_mes {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_gacha {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#ea475c;
background-color:#ffffff;
}
div.compsp {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#ffe800;
background-color:#fff9b7;
}
div.sttr_war {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#F5C8A7;
background-color:#FFEED8;
color:#9B2E2E;
}
div.sttr_price {
background-color:#FF9933;
color: #ffffff;
}
div.sttr_item {
width:226px;
margin:0px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_theme {
width:226px;
margin:0px 7px 3px 7px;
padding:5px;
border-style:solid;
border-width:0px 1px 1px 1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_back {
width:226px;
margin:0px 7px 0px 7px;
}
div.gift_l {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0px 2px;
border-color:#ff5252;
}
div.gift_h {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0px 2px;
border-color:#ffd616;
}
img.shop_icon {
margin:0 1px 1px 0;
}
div.help_guide {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#ffac3e;
}
div.guide-head {
color: #ffffff;
background-color: #5abcea;
}
div.guide-title {
color: #ffffff;
background-color: #5abcea;
}
div.guide-main {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#5abcea;
}
div.green-title {
color: #ffffff;
background-color: #91C900;
}
div.green-main {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#91C900;
}
table.method{
border-style:solid;
border-width:1px;
border-color:#cccccc;
}
div.search {
background-color: #eeeeee;
border-style: solid;
border-width: 1px;
border-color:#cccccc;
}
td.keyboard {
background-color: #eeeeee;
}
table.key-tab{
margin: 0 6px 0 6px;
}
div.keyout {
margin: 0 7px 0 7px;
border-style: solid;
border-width: 0 1px 1px 1px;
border-color:#cccccc;
}
div.on-ja {
border-style: solid;
border-width: 1px 1px 0 1px;
border-color:#cccccc;
}
div.off-en {
border-style: solid;
border-width: 1px 1px 0 0;
border-color:#cccccc;
}
div.off-aa {
border-style: solid;
border-width: 1px 1px 0 0;
border-color:#cccccc;
}
table.camp-detail {
border-style: solid;
border-width: 1px;
border-color:#cccccc;
}
table.center {
margin: 0 15px 0 16px;
}
div.codeborder {
border-style: solid;
border-width:0 1px 1px 1px;
border-color:#82c300;
}
div.codeprev {
border-style: solid;
border-width:1px;
border-color:#82c300;
}
td.code {
background-color:#ffffff;
}
table.codetab {
border-style: solid;
border-width: 1px 1px 0 1px;
border-color:#82c300;
}
div.codetab {
border-style: solid;
border-width: 0 1px 1px 1px;
border-color:#82c300;
background-color:#ffffff;
}
div.c_tab2 {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color:#82c300;
background-color:#ffffff;
}
div.topic {
border-style:solid;
border-width:1px;
border-color:#fff273;
background-color:#fff8b2;
}
div.icon-green {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #82c300;
}
div.p-block {
border-style:solid;
border-width:1px;
border-color:#000000;
color:#000000;
background-color:#ffffff;
}
div.clstar-help{
background-color:#ffffff;
color:#82c300;
}
div.ugo4th{
border-style:solid;
border-width:1px;
border-color:#c4260f;
background-color:#d52515;
}
+808
View File
@@ -0,0 +1,808 @@
@charset "UTF-8";
span.url{
text-decoration:underline;
}
span.comptxt {
color:#ff8a00;
}
span.star0 {
color:#fdc556;
}
span.star1 {
color:#138200;
}
span.star2 {
color:#ff455c;
}
span.star3 {
color:#00b0f9;
}
span.star4 {
color:#a411b9;
}
span.star0c {
color:#fdc556;
}
span.star1c {
color:#138200;
}
span.star2c {
color:#ff455c;
}
span.star3c {
color:#00b0f9;
}
span.star4c {
color:#a411b9;
}
span.orange{
color: #ff8d45;
}
span.green {
color: #82c300;
}
span.gray{
color: #666666;
}
span.white {
color:#FFFFFF;
}
html {
margin:0;
padding:0;
}
body {
color:#333333;
margin:0;
padding:0;
}
div.title {
margin:0 7px 0 7px;
padding:7px 5px 7px 5px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.description {
margin:0 7px 0 7px;
padding:7px 5px 7px 5px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.hr {
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
}
div.hr-tab {
border-style:solid;
border-width:0 0 1px 0;
border-color:#82c300;
}
span.timestamp {
color:#999999;
}
a.timestamp {
color:#999999;
}
a.violation {
color:#333333;
}
div.box {
padding:7px;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
background-color:#FFFFFF;
}
div.more {
margin:7px 0 0 0;
}
div.pager {
padding:5px;
}
div.num {
margin:5px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
background-color:#FFFFFF;
}
span.new {
color: #cc0000;
}
span.price {
color: #92cb22;
}
span.date {
color: #cccccc;
}
table.detail{
margin:0 7px 0 7px;
}
table.fullsize {
margin:0;
}
div.item-term {
padding:3px 0 3px 5px;
}
div.item-value {
padding:3px 5px 3px 0;
}
div.item-text {
padding:3px 5px 3px 15px;
}
div.item-tcom {
padding: 3px 5px 0 5px;
}
div.item-com {
padding: 0 5px 0 5px;
}
div.td-body {
padding:3px 5px 3px 5px;
}
div.comlist {
background-color:#FFFFFF;
}
div.comment-text {
padding:3px 5px 5px 15px;
}
div.comment-memo {
padding:0 0 0 15px;
}
div.staff {
color:#FFFFFF;
background-color:#1a1a1a;
}
a.staffnam {
color:#FFFFFF;
}
div.notice {
width:226px;
margin:7px 7px 3px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.colorstar {
margin:7px 0 7px 0;
}
div.code-box{
width:226px;
margin:7px 0 0 0;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.code-data{
margin:7px 0 7px 0;
}
div.mail-reply {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.lett_txt {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0 2px;
border-color:#92d300;
background-color:#ffffff;
}
div.lett_to {
margin: 0 11px 0 11px;
background-color:#92d300;
color:#ffffff;
padding: 1px 0 6px 5px;
}
div.pad0{
padding: 0 0 0 0;
}
div.pad5t{
padding: 5px 0 0 0;
}
div.pad5b{
padding: 0 0 5px 0;
}
div.pad1b{
padding: 0 0 1px 0;
}
div.pad2b{
padding: 0 0 2px 0;
}
div.pad2{
padding: 2px 2px 2px 2px;
}
div.pad5{
padding: 5px 5px 5px 5px;
}
div.pad0-5{
padding: 0 5px 0 5px;
}
td.pad2{
padding: 2px 2px 2px 2px;
}
div.pad3-5{
padding: 3px 5px 3px 5px;
}
div.lett_from{
padding: 3px 11px 3px 0;
}
div.pad11{
padding: 11px 11px 11px 11px;
}
div.pad5-11{
padding: 5px 11px 5px 11px;
}
div.pad10b{
padding: 0 0 10px 0;
}
div.pad20b{
padding: 0 0 20px 0;
}
div.mar0-7{
margin: 0px 7px 0px 7px;
}
div.mar0-11{
margin: 0px 11px 0px 11px;
}
div.mar0{
margin: 0px 0px 0px 0px;
}
div.mar5 {
margin: 5px 5px 5px 5px;
}
div.yaritori{
margin: 0 11px 0 11px;
background-color:#ffffff;
border-style:solid;
border-width:0 2px 0 2px;
border-color:#92d300;
}
div.gr-tit{
margin: 0 11px 0 11px;
background-color:#92d300;
color:#ffffff;
}
div.kadomaru{
margin: 0 11px 0 11px;
}
div.lett-note{
margin: 5px 11px 5px 11px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92d300;
background-color:#ffffff;
}
div.pk{
background-color:#ff3ab7;
}
div.blue{
background-color:#3acfff;
}
div.gray{
background-color:#cccccc;
}
div.gray_box{
padding:5px;
border-style:solid;
border-width:1px;
border-color:#cccccc;
background-color:#ffffff;
}
div.vio-intro {
width:240px;
padding:7px 0 7px 7px;
}
div.vio-memo {
padding:0 0 10px 15px;
}
div.vio-text {
padding:0 0 10px 15px;
}
div.vio-note {
color:#999999;
padding:0 0 10px 15px;
}
div.vio-submit {
padding:10px 0 10px 0;
}
div.info-head {
padding:5px 7px 5px 7px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
background-color:#eeeeee;
}
div.noborder{
border-style:solid;
border-color:#CCCCCC;
border-width:0 0 0 0;
}
div.info-body {
padding:7px;
}
div.info-msg {
margin:0 0 7px 0;
}
div.news-head {
padding:5px 7px 5px 7px;
border-style:solid;
border-width:0 0 1px 0;
border-color:#CCCCCC;
background-color:#eeeeee;
}
div.news-body {
padding:7px;
}
div.news-h1 {
margin:7px 7px 0 7px;
padding:7px;
border-style:solid;
border-width:3px;
border-color:#91C900;
}
div.news-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#91C900;
}
img.news-memo {
margin:0 10px 5px 0;
border-style:solid;
border-width:1px;
border-color:#CCCCCC;
}
div.info-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#91C900;
}
div.num-box{
background-color:#cccccc;
margin:0 30px;
}
td.tit-star{
background-color:#e9f8c3;
}
img.help{
border-style:solid;
border-width:2px;
border-color:#ffffff;
}
div.list{
padding:0 0 5px 0;
}
div.star-det{
padding:5px 0 5px 0;
}
img.sc{
border-style:solid;
border-width:1px;
border-color:#cccccc;
}
td.border {
background-color: #82c300;
}
div.border {
border-style: solid;
border-width: 0 0 5px 0;
border-color: #82c300;
}
div.border-c {
border-style: solid;
border-width: 0 0 2px 0;
border-color: #82c300;
}
td.space {
background-color: #82c300;
color: #82c300;
}
td.tabon {
color: #82c300;
}
td.taboff {
background-color: #82c300;
color: #ffffff;
}
td.tabpager {
background-color: #82c300;
color: #ffffff;
}
div.on {
padding: 1px 6px 3px 6px;
}
span.border {
color: #b2db59;
}
span.border-gray {
color: #cccccc;
}
a.taboff {
color: #ffffff;
}
div.comme {
padding: 3px 3px 3px 12px;
color: #b2db59;
}
span.text {
color: #333333;
}
td.gray {
background-color: #f2f2f2;
}
div.message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#BCCDED;
background-color:#E5EEFE;
}
div.error-message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#F5C8A7;
background-color:#FFEED8;
color:#9B2E2E;
}
div.ok-message {
width:226px;
margin:7px 7px 2px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#C5DABA;
background-color:#E3F9DB;
color:#517B29;
}
div.guide-h2 {
padding:0 0 3px 7px;
border-style:solid;
border-width:0 0 1px 4px;
border-color:#66ccff;
}
div.guide-h3 {
padding:3px 3px 3px 3px;
background-color:#66ccff;
color: #ffffff;
}
div.select-t {
color: #66ccff;
padding: 0 0 3px 0;
}
div.select-b {
padding:5px 3px 5px 3px;
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #99ccff;
background-color: #ffffff;
}
span.border-s {
color: #99ccff;
}
span.color {
color: #ffffff;
}
div.credit {
padding:5px 7px 5px 7px;
background-color:#eeeeee;
}
div.rule-tit {
background-color: #82c300;
color: #ffffff;
padding: 3px 3px 3px 3px;
}
div.rule-ind {
margin: 0 0 0 10px;
}
div.hr-prof {
border-style:solid;
border-width:0 0 3px 0;
border-color:transparent;
}
table.send {
margin: 5px 0 0 0 ;
}
div.icon {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #cccccc;
}
td.username {
height:20px;
}
td.mystatus {
}
div.prev {
padding: 1px 0 5px 0;
}
div.next {
padding: 5px 0 1px 0;
}
div.prof-det{
padding: 0 5px 0 5px;
}
div.catch {
margin: 5px 5px 5px 5px;
padding: 5px 5px 5px 5px;
border-style: solid;
border-width: 2px 2px 2px 2px;
border-color: #ffffff;
}
a.still {
color: #ffffff;
}
div.head {
margin: 0px 0 0 0;
}
div.send {
margin: 0px 10px 3px 11px;
}
div.link {
margin: 0px 10px 0 11px;
}
img.button {
margin: 5px 0 0 0;
}
td.theme {
margin: 0 0 0 3px;
}
div.trial-h1 {
margin: 5px 0 5px 0;
padding:3px;
border-style:solid;
border-width:3px;
border-color:#91C900;
}
div.mar-b10{
margin:0 0 10px 0;
}
div.cam1{
margin:7px 7px 0 7px;
padding:7px;
background-color: #ff2828;
color: #ffffff;
}
div.cam2{
margin:0 7px 0 7px;
padding:7px;
border-style:solid;
border-width: 2px 2px 2px 2px;
border-color: #ff2828;
}
table.xmas2009{
margin: 0 10px 0 11px;
background-color: #c70606;
color: #ffffff;
}
div.yellow {
margin: 0 11px 0 11px;
background-color:#ffe800;
}
div.sttr_mes {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_gacha {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#ea475c;
background-color:#ffffff;
}
div.compsp {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#ffe800;
background-color:#fff9b7;
}
div.sttr_war {
width:226px;
margin:7px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#F5C8A7;
background-color:#FFEED8;
color:#9B2E2E;
}
div.sttr_price {
background-color:#FF9933;
color: #ffffff;
}
div.sttr_item {
width:226px;
margin:0px 7px 0px 7px;
padding:5px;
border-style:solid;
border-width:1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_theme {
width:226px;
margin:0px 7px 3px 7px;
padding:5px;
border-style:solid;
border-width:0px 1px 1px 1px;
border-color:#92cb22;
background-color:#ffffff;
}
div.sttr_back {
width:226px;
margin:0px 7px 0px 7px;
}
div.gift_l {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0px 2px;
border-color:#ff5252;
}
div.gift_h {
margin: 0 11px 0 11px;
border-style:solid;
border-width:0 2px 0px 2px;
border-color:#ffd616;
}
img.shop_icon {
margin:0 1px 1px 0;
}
div.help_guide {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#ffac3e;
}
div.guide-head {
color: #ffffff;
background-color: #5abcea;
}
div.guide-title {
color: #ffffff;
background-color: #5abcea;
}
div.guide-main {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#5abcea;
}
div.green-title {
color: #ffffff;
background-color: #91C900;
}
div.green-main {
border-style:solid;
border-width:0 1px 1px 1px;
border-color:#91C900;
}
table.method{
border-style:solid;
border-width:1px;
border-color:#cccccc;
}
div.search {
background-color: #eeeeee;
border-style: solid;
border-width: 1px;
border-color:#cccccc;
}
td.keyboard {
background-color: #eeeeee;
}
table.key-tab{
margin: 0 6px 0 6px;
}
div.keyout {
margin: 0 7px 0 7px;
border-style: solid;
border-width: 0 1px 1px 1px;
border-color:#cccccc;
}
div.on-ja {
border-style: solid;
border-width: 1px 1px 0 1px;
border-color:#cccccc;
}
div.off-en {
border-style: solid;
border-width: 1px 1px 0 0;
border-color:#cccccc;
}
div.off-aa {
border-style: solid;
border-width: 1px 1px 0 0;
border-color:#cccccc;
}
table.camp-detail {
border-style: solid;
border-width: 1px;
border-color:#cccccc;
}
table.center {
margin: 0 15px 0 16px;
}
div.codeborder {
border-style: solid;
border-width:0 1px 1px 1px;
border-color:#82c300;
}
div.codeprev {
border-style: solid;
border-width:1px;
border-color:#82c300;
}
td.code {
background-color:#ffffff;
}
table.codetab {
border-style: solid;
border-width: 1px 1px 0 1px;
border-color:#82c300;
}
div.codetab {
border-style: solid;
border-width: 0 1px 1px 1px;
border-color:#82c300;
background-color:#ffffff;
}
div.c_tab2 {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color:#82c300;
background-color:#ffffff;
}
div.topic {
border-style:solid;
border-width:1px;
border-color:#fff273;
background-color:#fff8b2;
}
div.icon-green {
border-style: solid;
border-width: 1px 1px 1px 1px;
border-color: #82c300;
}
div.p-block {
border-style:solid;
border-width:1px;
border-color:#000000;
color:#000000;
background-color:#ffffff;
}
div.clstar-help{
background-color:#ffffff;
color:#82c300;
}
div.ugo4th{
border-style:solid;
border-width:1px;
border-color:#c4260f;
background-color:#d52515;
}
+1
View File
@@ -0,0 +1 @@
����������������������������������������������������������������ςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σςς�Σ�Σςς�Σ�Σ�Σ�Σ�Σ�Σςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σ�ΣΟ‚�Σςςςς�ΣΟ‚�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�ΣΟ‚�Σςςςς�ΣΟ‚�Σ�Σ�Σ�Σ�Σςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σςςςςςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σςςςςςςςςςς�Σ�Σ�Σ�Σςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σςς�Σ�Σ�Σ�Σ�Σ�Σςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σςςς�Σ�Σ�Σ�Σ�Σ�Σςςς�Σ�Σ�Σςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σςςςς�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σςςςς�Σ�Σςςςς�Σ�Σ�Σ�Σςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σςςςςςςςςςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σςςςςςςςςςςςςςςς�Σςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σςςςςςς�Σςςςςςςςςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σςςςςςςςςς�Σςςςςςςςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σςςςςςςςςςςςςςςς�Σςςςςςς�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςς�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σ�Σςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςςς����������������������������������������������������������������
@@ -0,0 +1 @@
���������������サ纎ロワ������������������������ン�モォョ完完完完完諌�5ササ゚������������������ン�ソョ完完完完完完完完完完完寄椢ロ���������������xマョ完完完完完諌儒�潁漬式完完完完岐「ワ������������5キョ完完完�5」垤�ヌ�ヒ�ヒ�ヒ�ヒ�ヒシソxッム聴完完官噛゚����������ッョ完完岐埆サ�ヒ�ヒ�ヒ�ヒ�ヒWォョ岸テ�ヒ�ヒ�ヒyウョ完完完噛゚��������Vテョ完完碍ァ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォョ寄繭キ�ヒ�ヒ�ヒシソミ聴完完款������壙ョ完完蓋ォ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォョ完完官�6ァンテ�ヒンテミ聴完貫������ミ粍完�4」�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォッ式完完完官孜ウ�ヒワソョ完完楽モ����燮ョ完諌釣ヌ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ�シソ5」ョ完完官爵テ�ヒ垤ョ完観�����ョ完噛キ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヌWォョ完棺サ�ヒ�ヒ�ョ完噛ラ��ワ�完諌�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヒ5」ョ岸テ�ヒ�ヒ垤ョ完�ォ��Wテョ完�5」�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヌミ�6ァ�ヒ�ヒ�ヒ�ヌョ完完��ウョ完噛キ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒンテ��ヒ�ヒ�ヒ�ヒ�ヒ�ョ完款��ョ完款ソ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ4洌完鎌ラ�ム坩完岸テ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ5」ョ完岳マ�ム坩完岸テ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ5」ョ完岳マ��ョ完棺サ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ洌完噛ラ�5サョ完岳ッ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォ��ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ョ完含�yモョ完�5」�ヒ�ヒ�ヒ�ヒ�ヒンテxッ漑�6ァササWォ��ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒンテョ完観��ン�完官岸テ�ヒ�ヒ�ヒ6ァョ完完完完完諌梯�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒWォョ完�5キ���4キョ完碍ァ�ヒ�ヒシソョ完完完完完完完岐�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒンテッ完完棺���サ羂完完款ソ�ヒササョ完完完完完完完岐�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒVァョ完岐ヲ�����ウョ完諌釣テ�ヒ�ョ完完完完完官湿ソ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ匍ョ完完棺�����シ�完完�巫テ�ヒyウWォ��6ァyウ�ヌ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒササョ完完�4ウ�������壤ョ完完寄吊ソ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒxッョ完完寄���������xマョ完完完学ォ�ヌ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒ�ヒササ洌完完諌吊����������壅ョ完完完諌�5」垤ンデテ�ヌ゙テササxッ岼完完完岐ヲ�������������シ�ォョ完完完完完完完完完完完完完完蓋ヌ����������������サ゚ッョ完完完完完完完完完官桟ソワ�������������������ワ�マッム墟佃漬梯「5ササ゚�������������������������������������������
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
RQBuAGcAbABpAHMAaAA= en
1 RQBuAGcAbABpAHMAaAA= en
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,73 @@
from twisted.web import resource
from twisted.internet import reactor
import time
from hatena import Log, NotFound, request_args, request_path
from DB import Database
from Hatenatools import UGO
class PyResource(resource.Resource):
isLeaf = True
def __init__(self):
super().__init__()
self.pages = []
self.newestflip = None
self.newestview = None
self.Update()
def render(self, request):
try:
page = int(request_args(request).get("page", ["1"])[0])
except ValueError:
page = 1
if 1 <= page <= len(self.pages):
path = "/".join(request_path(request).split("/")[3:])
Log(request, "%s page %i" % (path, page))
request.setHeader(b"content-type", b"text/plain")
return self.pages[page - 1]
return NotFound.render(request)
def Update(self):
reactor.callLater(600, self.Update)
current = Database.Newest[0] if Database.Newest else None
if current != self.newestflip or self.newestview != Database.Views:
self.newestflip = current
self.newestview = Database.Views
reactor.callInThread(self.UpdateThreaded, list(Database.Newest))
def UpdateThreaded(self, flipnotes):
def score(item):
i, (creator_id, flip) = item
views, stars = Database.GetFlipnote(creator_id, flip)[1:3]
return int(views) * 60 + int(stars) // 4 - i
flipnotes = [x[1] for x in sorted(enumerate(flipnotes), key=score, reverse=True)[:500]]
pages = []
pagecount = (len(flipnotes) + 49) // 50
pagecount = min(pagecount, 10)
flipcount = min(len(flipnotes), 500)
for i in range(pagecount):
pages.append(self.MakePage(flipnotes[i * 50 : i * 50 + 50], i + 1, i < pagecount - 1, flipcount))
if self.pages:
print(time.strftime("[%H:%M:%S] Updated hotmovies.ugo"))
self.pages = pages
def MakePage(self, flipnotes, page, has_next, count):
ugo = UGO()
ugo.Loaded = True
ugo.Items = []
ugo.Items.append(("layout", (2, 1)))
ugo.Items.append(("topscreen text", ["Popular Flipnotes", "Flipnotes", str(count), "", "The most popular new Flipnotes."], 0))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/hotmovies.uls", "Most Popular", True))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/likedmovies.uls", "Most Liked", False))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/newmovies.uls", "New Flipnotes", False))
ugo.Items.append(("unknown", ("3", "http://flipnote.hatena.com/ds/v2-xx/help/post_howto.htm", "UABvAHMAdAAgAEYAbABpAHAAbgBvAHQAZQA=")))
if page > 1:
ugo.Items.append(("button", 115, "Previous", "http://flipnote.hatena.com/ds/v2-xx/frontpage/hotmovies.uls?page=%i" % (page - 1), ("", ""), None))
for creatorid, filename in flipnotes:
stars = str(Database.GetFlipnote(creatorid, filename)[2])
ugo.Items.append(("button", 3, "", "http://flipnote.hatena.com/ds/v2-xx/movie/%s/%s.ppm" % (creatorid, filename), (stars, "765", "573", "0"), (filename + ".ppm", Database.GetFlipnoteTMB(creatorid, filename))))
if has_next:
ugo.Items.append(("button", 115, "Next", "http://flipnote.hatena.com/ds/v2-xx/frontpage/hotmovies.uls?page=%i" % (page + 1), ("", ""), None))
return ugo.Pack()
@@ -0,0 +1,73 @@
from twisted.web import resource
from twisted.internet import reactor
import time
from hatena import Log, NotFound, request_args, request_path
from DB import Database
from Hatenatools import UGO
class PyResource(resource.Resource):
isLeaf = True
def __init__(self):
super().__init__()
self.pages = []
self.newestflip = None
self.neweststar = None
reactor.callLater(2, self.Update)
def render(self, request):
try:
page = int(request_args(request).get("page", ["1"])[0])
except ValueError:
page = 1
if 1 <= page <= len(self.pages):
path = "/".join(request_path(request).split("/")[3:])
Log(request, "%s page %i" % (path, page))
request.setHeader(b"content-type", b"text/plain")
return self.pages[page - 1]
return NotFound.render(request)
def Update(self):
reactor.callLater(600, self.Update)
current = Database.Newest[0] if Database.Newest else None
if current != self.newestflip or self.neweststar != Database.Stars:
self.newestflip = current
self.neweststar = Database.Stars
reactor.callInThread(self.UpdateThreaded, list(Database.Newest))
def UpdateThreaded(self, flipnotes):
def score(item):
i, (creator_id, flip) = item
views, stars = Database.GetFlipnote(creator_id, flip)[1:3]
return int(stars) * 110 + int(views) // 10 - i
flipnotes = [x[1] for x in sorted(enumerate(flipnotes), key=score, reverse=True)[:500]]
pages = []
pagecount = (len(flipnotes) + 49) // 50
pagecount = min(pagecount, 10)
flipcount = min(len(flipnotes), 500)
for i in range(pagecount):
pages.append(self.MakePage(flipnotes[i * 50 : i * 50 + 50], i + 1, i < pagecount - 1, flipcount))
if self.pages:
print(time.strftime("[%H:%M:%S] Updated likedmovies.ugo"))
self.pages = pages
def MakePage(self, flipnotes, page, has_next, count):
ugo = UGO()
ugo.Loaded = True
ugo.Items = []
ugo.Items.append(("layout", (2, 1)))
ugo.Items.append(("topscreen text", ["Liked Flipnotes", "Flipnotes", str(count), "", "The most liked new Flipnotes."], 0))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/hotmovies.uls", "Most Popular", False))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/likedmovies.uls", "Most Liked", True))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/newmovies.uls", "New Flipnotes", False))
ugo.Items.append(("unknown", ("3", "http://flipnote.hatena.com/ds/v2-xx/help/post_howto.htm", "UABvAHMAdAAgAEYAbABpAHAAbgBvAHQAZQA=")))
if page > 1:
ugo.Items.append(("button", 115, "Previous", "http://flipnote.hatena.com/ds/v2-xx/frontpage/likedmovies.uls?page=%i" % (page - 1), ("", ""), None))
for creatorid, filename in flipnotes:
stars = str(Database.GetFlipnote(creatorid, filename)[2])
ugo.Items.append(("button", 3, "", "http://flipnote.hatena.com/ds/v2-xx/movie/%s/%s.ppm" % (creatorid, filename), (stars, "765", "573", "0"), (filename + ".ppm", Database.GetFlipnoteTMB(creatorid, filename))))
if has_next:
ugo.Items.append(("button", 115, "Next", "http://flipnote.hatena.com/ds/v2-xx/frontpage/likedmovies.uls?page=%i" % (page + 1), ("", ""), None))
return ugo.Pack()
@@ -0,0 +1,67 @@
from twisted.web import resource
from twisted.internet import reactor
import time
from hatena import Log, NotFound, request_args, request_path
from DB import Database
from Hatenatools import UGO
class PyResource(resource.Resource):
isLeaf = True
def __init__(self):
super().__init__()
self.pages = []
self.newestflip = None
reactor.callLater(4, self.Update)
def render(self, request):
try:
page = int(request_args(request).get("page", ["1"])[0])
except ValueError:
page = 1
if 1 <= page <= len(self.pages):
path = "/".join(request_path(request).split("/")[3:])
Log(request, "%s page %i" % (path, page))
request.setHeader(b"content-type", b"text/plain")
return self.pages[page - 1]
return NotFound.render(request)
def Update(self):
reactor.callLater(60, self.Update)
current = Database.Newest[0] if Database.Newest else None
if current != self.newestflip:
self.newestflip = current
reactor.callInThread(self.UpdateThreaded, list(Database.Newest))
def UpdateThreaded(self, flipnotes):
flipnotes = list(flipnotes)
pages = []
pagecount = (len(flipnotes) + 49) // 50
pagecount = min(pagecount, 10)
flipcount = len(flipnotes)
for i in range(pagecount):
pages.append(self.MakePage(flipnotes[i * 50 : i * 50 + 50], i + 1, i < pagecount - 1, flipcount))
if self.pages:
print(time.strftime("[%H:%M:%S] Updated newmovies.ugo"))
self.pages = pages
def MakePage(self, flipnotes, page, has_next, count):
ugo = UGO()
ugo.Loaded = True
ugo.Items = []
ugo.Items.append(("layout", (2, 1)))
ugo.Items.append(("topscreen text", ["New Flipnotes", "Flipnotes", str(count), "", "The newest Flipnotes submitted."], 0))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/hotmovies.uls", "Most Popular", False))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/likedmovies.uls", "Most Liked", False))
ugo.Items.append(("category", "http://flipnote.hatena.com/ds/v2-xx/frontpage/newmovies.uls", "New Flipnotes", True))
ugo.Items.append(("unknown", ("3", "http://flipnote.hatena.com/ds/v2-xx/help/post_howto.htm", "UABvAHMAdAAgAEYAbABpAHAAbgBvAHQAZQA=")))
if page > 1:
ugo.Items.append(("button", 115, "Previous", "http://flipnote.hatena.com/ds/v2-xx/frontpage/newmovies.uls?page=%i" % (page - 1), ("", ""), None))
for creatorid, filename in flipnotes:
stars = str(Database.GetFlipnote(creatorid, filename)[2])
ugo.Items.append(("button", 3, "", "http://flipnote.hatena.com/ds/v2-xx/movie/%s/%s.ppm" % (creatorid, filename), (stars, "765", "573", "0"), (filename + ".ppm", Database.GetFlipnoteTMB(creatorid, filename))))
if has_next:
ugo.Items.append(("button", 115, "Next", "http://flipnote.hatena.com/ds/v2-xx/frontpage/newmovies.uls?page=%i" % (page + 1), ("", ""), None))
return ugo.Pack()
+1
View File
@@ -0,0 +1 @@
<html>␍ <head>␍ <title>Info</title>␍ <meta name="uppertitle" content="Info">␍ <link rel="stylesheet" type="text/css" href="http://flipnote.hatena.com/css/ds/common.css">␍ </head>␍ <body>␍ <div class="container">␍ <div class="title" align="center">pbsds's Hatena Server</div>␍ <div class="news-body">␍ <p>This server is a proof of concept server. There's not much functionality yet because it's in early stages. Please enjoy your stay.</p>␍ </div>␍ </div>␍ </body>␍</html>
+1
View File
@@ -0,0 +1 @@
<html>␍ <head>␍ <title>Post Flipnote</title>␍ <meta name="uppertitle" content="Post Flipnote">␍ <meta name="postchannelbutton" content="http://flipnote.hatena.com/ds/v2-xx/post/flipnote.post">␍ <link rel="stylesheet" type="text/css" href="http://flipnote.hatena.com/css/ds/common.css">␍ </head>␍ <body>␍ <div class="container">␍ <div class="title" align="center">Posting a Flipnote</div>␍ <div class="news-body">␍ <p>Posting a flipnote is as easy as pressing the button down below and selecting the flipnote you want to upload!</p>␍ </div>␍ </div>␍ </body>␍</html>
+20
View File
@@ -0,0 +1,20 @@
<?xml version='1.0' encoding='UTF-8'?>
<ugo_xml>
<layout>
<value>2</value>
<value>1</value>
</layout>
<title>
<label>Inbox</label>
<label />
<label />
<label />
<label />
<num>0</num>
</title>
<button>
<label>Info</label>
<address>http://flipnote.hatena.com/ds/v2-xx/help/info.htm</address>
<trait>3</trait>
</button>
</ugo_xml>
+29
View File
@@ -0,0 +1,29 @@
<?xml version='1.0' encoding='UTF-8'?>
<ugo_xml>
<layout>
<value>0</value>
</layout>
<title>
<label />
<label />
<label />
<label />
<label />
<num>0</num>
</title>
<button>
<label>Browse Flipnotes</label>
<address>http://flipnote.hatena.com/ds/v2-xx/frontpage/hotmovies.uls</address>
<trait>100</trait>
<value />
<value>0</value>
</button>
<button>
<label>Info</label>
<address>http://flipnote.hatena.com/ds/v2-xx/help/info.htm</address>
<trait>0</trait>
<value />
<value>0</value>
<embedded_file>__ntfts/News.ntft</embedded_file>
</button>
</ugo_xml>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+221
View File
@@ -0,0 +1,221 @@
from twisted.web import resource
from hatena import Log, ServerLog, Silent, NotFound, as_text, request_path, request_args, request_headers, client_ip
from DB import Database
from Hatenatools import TMB
class PyResource(resource.Resource):
isLeaf = False
def __init__(self):
super().__init__()
self.CreatorID = CreatorIDResource()
def getChild(self, name, request):
name = as_text(name)
if Database.CreatorExists(name):
return self.CreatorID
if name == "":
return self
return NotFound
def render(self, request):
request.setResponseCode(403)
return b"403 - Denied access"
class CreatorIDResource(resource.Resource):
isLeaf = False
def __init__(self):
super().__init__()
self.CreatorIDFile = CreatorIDFileResource()
def getChild(self, name, request):
creator_id = request_path(request).split("/")[-2]
name = as_text(name)
filename = ".".join(name.split(".")[:-1])
if Database.FlipnoteExists(creator_id, filename):
return self.CreatorIDFile
if name == "":
return self
return NotFound
def render(self, request):
request.setResponseCode(403)
return b"403 - Denied access"
class CreatorIDFileResource(resource.Resource):
isLeaf = True
def render(self, request):
creator, file = request_path(request).split("/")[-2:]
filetype = file.rsplit(".", 1)[-1].lower()
path = "/".join(request_path(request).split("/")[3:])
if filetype == "ppm":
Log(request, path)
Database.AddView(creator, file[:-4])
request.setHeader(b"content-type", b"text/plain")
return Database.GetFlipnotePPM(creator, file[:-4])
if filetype == "info":
Log(request, path, True)
request.setHeader(b"content-type", b"text/plain")
return b"0\n0\n"
if filetype == "htm":
request.setHeader(b"content-type", b"text/html; charset=utf-8")
return self.GenerateDetailsPage(creator, ".".join(file.split(".")[:-1])).encode("UTF-8")
if filetype == "star":
args = request_args(request)
color = args.get("starcolor", ["yellow"])[0].split(",", 1)[0].lower()
headers = request_headers(request)
if "x-hatena-star-count" not in headers:
ServerLog.write(
"%s got 403 when requesting %s without a X-Hatena-Star-Count header" % (client_ip(request), path),
Silent,
)
request.setResponseCode(403)
return b"403 - Denied access\nRequest lacks a X-Hatena-Star-Count http header"
try:
amount = int(headers["x-hatena-star-count"])
except ValueError:
amount = 0
if amount < 1 or amount > 65535:
ServerLog.write(
"%s got 403 when requesting %s with an invalid X-Hatena-Star-Count header" % (client_ip(request), path),
Silent,
)
request.setResponseCode(403)
return b"403 - Denied access\nRequest has an invalid X-Hatena-Star-Count http header"
if not Database.AddStar(creator, file[:-5], amount, color):
ServerLog.write("%s got 500 when requesting %s" % (client_ip(request), path), Silent)
request.setResponseCode(500)
return b"500 - Internal server error\nAdding the stars seems to have failed."
ServerLog.write(
"%s added %i %s stars to %s/%s.ppm" % (client_ip(request), amount, color, creator, file[:-5]),
Silent,
)
return b"Success"
if filetype == "dl":
Log(request, path, True)
Database.AddDownload(creator, file[:-3])
return b"Noted ;)"
ServerLog.write("%s got 403 when requesting %s" % (client_ip(request), path), Silent)
request.setResponseCode(403)
return b"403 - Denied access"
def GenerateDetailsPage(self, CreatorID, filename):
flipnote = Database.GetFlipnote(CreatorID, filename)
if not flipnote:
return "This flipnote doesn't exist!"
tmb = TMB().Read(Database.GetFlipnoteTMB(CreatorID, filename))
if not tmb:
return "This flipnote is corrupt!"
spinoff = ""
if tmb.OriginalAuthorID != tmb.EditorAuthorID or tmb.OriginalFilename != tmb.CurrentFilename:
if Database.FlipnoteExists(tmb.OriginalAuthorID, tmb.OriginalFilename[:-4]):
spinoff = SpinoffTemplate1.replace("%%CreatorID%%", tmb.OriginalAuthorID).replace("%%Filename%%", tmb.OriginalFilename[:-4])
elif tmb.OriginalAuthorID != tmb.EditorAuthorID:
spinoff = SpinoffTemplate2
entries = []
content = '<a href="http://flipnote.hatena.com/ds/v2-xx/%s/profile.htm?t=260&pm=80">%s</a>' % (CreatorID, tmb.Username)
entries.append(PageEntryTemplate.replace("%%Name%%", "Creator").replace("%%Content%%", content))
content = '<a href="http://flipnote.hatena.com/ds/v2-xx/movie/%s/%s.htm?mode=stardetail"><span class="star0c">★</span> <span class="star0">%s</span></a>' % (CreatorID, filename, flipnote[2])
for css, value in zip(("1", "2", "3", "4"), flipnote[3:7]):
content += '<br/><a href="http://flipnote.hatena.com/ds/v2-xx/movie/%s/%s.htm?mode=stardetail"><span class="star%sc">★</span> <span class="star%s">%s</span></a>' % (CreatorID, filename, css, css, value)
entries.append(PageEntryTemplate.replace("%%Name%%", "Stars").replace("%%Content%%", content))
entries.append(PageEntryTemplate.replace("%%Name%%", "Views").replace("%%Content%%", str(flipnote[1])))
entries.append(PageEntryTemplate.replace("%%Name%%", "Downloads").replace("%%Content%%", str(flipnote[8])))
if flipnote[7]:
content = '<a href="http://flipnote.hatena.com/ds/v2-xx/ch/%s.uls">%s</a>' % (flipnote[7], flipnote[7])
entries.append(PageEntryTemplate.replace("%%Name%%", "Channel").replace("%%Content%%", content))
return (
DetailsPageTemplate.replace("%%CreatorID%%", CreatorID)
.replace("%%Filename%%", filename)
.replace("%%Username%%", tmb.Username)
.replace("%%CommentCount%%", "0")
.replace("%%Spinoff%%", spinoff)
.replace("%%PageEntries%%", PageEntrySeparator.join(entries))
)
#templates:
DetailsPageTemplate = """<html>
<head>
<title>Flipnote by %%Username%%</title>
<meta name="upperlink" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.ppm">
<meta name="starbutton" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.star">
<meta name="starbutton1" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.star?starcolor=green,9001">
<meta name="starbutton2" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.star?starcolor=red,9001">
<meta name="starbutton3" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.star?starcolor=blue,9001">
<meta name="starbutton4" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.star?starcolor=purple,9001">
<meta name="savebutton" content="http://flipnote.hatena.com/ds/v2-xx/movie/%%CreatorID%%/%%Filename%%.ppm">
<meta name="playcontrolbutton" content="">
<link rel="stylesheet" href="http://flipnote.hatena.com/css/ds/basic.css">
</head>
<body>
<table width="240" border="0" cellspacing="0" cellpadding="0" class="tab">
<tr>
<td class="border" width="5" align="center">
<div class="border"></div>
</td>
<td class="border" width="70" align="center">
<div class="border"></div>
</td>
<td class="border" width="95" align="center">
<div class="border"></div>
</td>
</tr>
<tr>
<td class="space"> </td>
<td class="tabon" align="center">
<div class="on" align="center">Description</div>
</td>
<td class="taboff" align="center">
<a class="taboff" href="http://flipnote.hatena.com/ds/v2-eu/movie/%%CreatorID%%/%%Filename%%.htm?mode=commentshalfsize">Comments(%%CommentCount%%)</a>
</td>
</tr>
</table>
<div class="pad5b"></div>%%Spinoff%%
<table width="226" border="0" cellspacing="0" cellpadding="0" class="detail">%%PageEntries%%
</table>
</body>
</html>"""
SpinoffTemplate1 = """
<div class="notice2" align="center">
This Flipnote is a spin-off.<br>
<a href="http://flipnote.hatena.com/ds/v2-eu/movie/%%CreatorID%%/%%Filename%%.htm">Original Flipnote</a>
</div>"""
SpinoffTemplate2 = """
<div class="notice2" align="center">
This Flipnote is a spin-off.
</div>"""
PageEntryTemplate = """
<tr>
<th width="90">
<div class="item-term" align="left">%%Name%%</div>
</th>
<td width="136">
<div class="item-value" align="right">
%%Content%%
</div>
</td>
</tr>"""
PageEntrySeparator="""
<tr> </tr>
<tr>
<td colspan="2">
<div class="hr"></div>
</td>
</tr>"""
+25
View File
@@ -0,0 +1,25 @@
from twisted.web import resource
from hatena import ServerLog, Silent, request_args, client_ip
from DB import Database
class PyResource(resource.Resource):
isLeaf = True
def render_GET(self, request):
ServerLog.write("%s got 403 when requesting post/flipnote.post with GET" % client_ip(request), Silent)
request.setResponseCode(405)
return b"405 - Method Not Allowed"
def render_POST(self, request):
data = request.content.read()
channel = request_args(request).get("channel", [""])[0]
add = Database.AddFlipnote(data, channel)
if add:
ServerLog.write('%s successfully uploaded "%s.ppm"' % (client_ip(request), add[1]), Silent)
request.setResponseCode(200)
else:
ServerLog.write("%s tried to upload a flipnote, but failed..." % client_ip(request), Silent)
request.setResponseCode(500)
return b""
+1
View File
@@ -0,0 +1 @@
���������3욎욎�3�������3욎욎�3�����������������������������������������������3욎욎�3�������3욎욎�3�����������������������������������������������3욎욎�3�������3욎욎�3��������������������������������������������3욎����3욎욎욎욎욎�3����3욎�����������������������������������������3욎����3욎욎욎욎욎�3����3욎�����������������������������������������3욎����3욎욎욎욎욎�3����3욎�����������������������������������������3욎����3욎욎욎욎욎�3����3욎�����������������������������������������3욎����3욎욎욎욎욎�3����3욎�����������������������������������������3욎����3욎욎욎욎욎�3����3욎�����������������������������������������3욎욎욎욎욎욎욎욎욎욎욎�3�����������������������������������������3욎욎욎욎욎욎욎욎욎욎욎�3�����������������������������������������3욎욎욎욎욎욎욎욎욎욎욎�3��������������������������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�����������������������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�����������������������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�����������������������������������3욎욎�3�������������������3욎욎�3�����������������������������������3욎욎�3�������������������3욎욎�3�����������������������������������3욎욎�3�������������������3욎욎�3��������������������������������3욎욎욎욎�������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3����3욎욎욎욎욎�3�������3욎욎욎욎욎�3�����������������������������������3욎욎욎욎욎�3�������3욎욎욎욎욎�3�����������������������������������3욎욎욎욎욎�3�������3욎욎욎욎욎�3��������������������������������3욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎욎�3�
+1
View File
@@ -0,0 +1 @@
������������������2�2�2�2����2�2�2�2�2�2��2�2�2���2�2�2�2�2�����2�2�2�2�����2�2�2�2�2���2�2�2��2�2�2�2�2�2����2�2�2�2�����2�2�2�2�����2�2�2�2�����2�2�2�2�����2�2�2�2�����2�2�2�2�����2�2�2�2���
Binary file not shown.
@@ -0,0 +1 @@
�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������
@@ -0,0 +1 @@
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Line truncated
+1
View File
@@ -0,0 +1 @@
���™�5�5�5�5�5�5�5�5�������™��γ?Οό�ό�?Ο?Ο?Ο?Ο?Ο5�5�����™��γ?Ο?Ο?Οό�?Ο�γ�γ?Ο?Ο?Ο5����ό�ό�ό�ό�ό�?Ο?Ο�γ�γ�γ�γ�γ?Ο5���™��γ?Ο?Ο?Οό�?Ο?Ο?Ο?Ο?Ο�γ�γ5���ό�ό�ό�ό�ό�?Ο?Οό�ό�ό�?Ο�γ�γ™���™��γ?Ο?Ο?Οό�ό�?Ο?Ο?Ο?Ο�γ�γ™���ό�ό�ό�ό�ό�?Ο?Ο?Ο�γ�γ�γ?Ο?Ο5���™��γ�γ�γ�γό�?Ο�γ�γ�γ?Ο?Ο5�����™�™�™�™�5�?Ο?Ο?Ο?Ο5�5����������5�?Ο?Ο?Ο5�����������™�?Ο?Ο?Ο5�����������™��γ?Ο?Ο5������������™��γ?Ο?Ο5������������™��γ�γ�γ™�������������™�™�™��������
+59
View File
@@ -0,0 +1,59 @@
from __future__ import annotations
import os
import shutil
import tempfile
import unittest
from pathlib import Path
import database as database_module
ROOT = Path(__file__).resolve().parents[1]
SAMPLE_CREATOR = "913304C0CC74CC1F"
SAMPLE_NAME = "74CC1F_0D3AA40F2B3A0_000"
class DatabaseTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.old_cwd = os.getcwd()
temp_root = Path(self.tmp.name)
shutil.copytree(ROOT / "database", temp_root / "database")
os.chdir(temp_root)
self.db = database_module.Database.__class__()
def tearDown(self):
# Prevent the per-test instance from having anything left to flush at interpreter exit.
self.db.Creator.clear()
self.db.new = False
os.chdir(self.old_cwd)
self.tmp.cleanup()
def test_read_sample(self):
self.assertTrue(self.db.CreatorExists(SAMPLE_CREATOR))
row = self.db.GetFlipnote(SAMPLE_CREATOR, SAMPLE_NAME)
self.assertTrue(row)
self.assertEqual(row[0], SAMPLE_NAME)
self.assertEqual(len(row), 9)
self.assertTrue(self.db.GetFlipnotePPM(SAMPLE_CREATOR, SAMPLE_NAME).startswith(b"PARA"))
self.assertEqual(len(self.db.GetFlipnoteTMB(SAMPLE_CREATOR, SAMPLE_NAME)), 0x6A0)
def test_counters_and_write(self):
self.assertTrue(self.db.AddView(SAMPLE_CREATOR, SAMPLE_NAME))
self.assertTrue(self.db.AddStar(SAMPLE_CREATOR, SAMPLE_NAME, 5, "green"))
self.assertTrue(self.db.AddDownload(SAMPLE_CREATOR, SAMPLE_NAME))
self.assertEqual(self.db.Views, 1)
self.assertEqual(self.db.Stars, 1)
self.assertEqual(self.db.Downloads, 1)
row = self.db.GetFlipnote(SAMPLE_CREATOR, SAMPLE_NAME, True)
self.assertEqual(int(row[1]), 3)
self.assertEqual(int(row[3]), 5)
self.assertEqual(int(row[8]), 1)
self.db.write()
text = Path("database/Creators") / SAMPLE_CREATOR / "flipnotes.dat"
self.assertIn("\t3\t", text.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()
+98
View File
@@ -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()
+124
View File
@@ -0,0 +1,124 @@
from __future__ import annotations
import importlib
import sys
import types
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def install_twisted_stub():
"""Install only the Twisted surface needed to smoke-test this project."""
if importlib.util.find_spec("twisted") is not None:
return
twisted = types.ModuleType("twisted")
web = types.ModuleType("twisted.web")
resource_mod = types.ModuleType("twisted.web.resource")
static_mod = types.ModuleType("twisted.web.static")
internet_mod = types.ModuleType("twisted.internet")
class Resource:
isLeaf = False
def __init__(self):
self.children = {}
def putChild(self, name, child):
if not isinstance(name, bytes):
raise TypeError("Twisted child names must be bytes")
self.children[name] = child
def getChild(self, name, request):
return self
def getChildWithDefault(self, name, request):
return self.children.get(name, self.getChild(name, request))
class File(Resource):
def __init__(self, path):
super().__init__()
self.path = path
def render(self, request):
p = Path(self.path)
return p.read_bytes() if p.is_file() else b""
class Reactor:
def __init__(self):
self.calls = []
def callLater(self, delay, func, *args, **kwargs):
self.calls.append((delay, func, args, kwargs))
return object()
def callInThread(self, func, *args, **kwargs):
return func(*args, **kwargs)
resource_mod.Resource = Resource
static_mod.File = File
web.resource = resource_mod
web.static = static_mod
internet_mod.reactor = Reactor()
twisted.web = web
twisted.internet = internet_mod
sys.modules.update(
{
"twisted": twisted,
"twisted.web": web,
"twisted.web.resource": resource_mod,
"twisted.web.static": static_mod,
"twisted.internet": internet_mod,
}
)
class ResourceTreeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
install_twisted_stub()
cls.old_cwd = Path.cwd()
import os
os.chdir(ROOT)
import hatena
cls.hatena = hatena
cls.root = hatena.Setup()
@classmethod
def tearDownClass(cls):
import os
os.chdir(cls.old_cwd)
def test_core_resource_tree_loads(self):
region = self.root.dsResource.region
expected = {
b"index.ugo",
b"inbox.ugo",
b"movie",
b"post",
b"frontpage",
b"help",
b"eula_list.tsv",
}
self.assertTrue(expected.issubset(region.children))
self.assertIn(b"flipnote.post", region.children[b"post"].children)
self.assertIn(b"hotmovies.ugo", region.children[b"frontpage"].children)
self.assertIn(b"likedmovies.ugo", region.children[b"frontpage"].children)
self.assertIn(b"newmovies.ugo", region.children[b"frontpage"].children)
def test_prepacked_ugo_resources_are_binary(self):
region = self.root.dsResource.region
self.assertTrue(region.children[b"index.ugo"].ugofile.startswith(b"UGAR"))
self.assertTrue(region.children[b"inbox.ugo"].ugofile.startswith(b"UGAR"))
def test_hot_page_builds_from_database(self):
hot = self.root.dsResource.region.children[b"frontpage"].children[b"hotmovies.ugo"]
self.assertGreaterEqual(len(hot.pages), 1)
self.assertTrue(hot.pages[0].startswith(b"UGAR"))
if __name__ == "__main__":
unittest.main()