2024-06-15 18:26:20 -04:00
|
|
|
|
import re
|
2024-06-13 20:44:15 -04:00
|
|
|
|
from functools import cached_property
|
2024-12-30 01:51:19 -05:00
|
|
|
|
from typing import Any, override
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
from django.db import models
|
2024-06-15 18:26:20 -04:00
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
2024-06-15 18:26:20 -04:00
|
|
|
|
from catalog.models import Item
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
from .common import Content
|
|
|
|
|
from .renderers import render_text
|
|
|
|
|
from .shelf import ShelfMember
|
|
|
|
|
|
2024-06-15 18:26:20 -04:00
|
|
|
|
_progress = re.compile(
|
2024-06-17 15:10:34 -04:00
|
|
|
|
r"(.*\s)?(?P<prefix>(p|pg|page|ch|chapter|pt|part|e|ep|episode|trk|track|cycle))(\s|\.|#)*(?P<value>([\d\:\.\-]+))\s*(?P<postfix>(%))?(\s|\n|\.|。)?$",
|
2024-06-15 21:54:39 -04:00
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_progress2 = re.compile(
|
2024-06-17 15:10:34 -04:00
|
|
|
|
r"(.*\s)?(?P<value>([\d\:\.\-]+))\s*(?P<postfix>(%))?(\s|\n|\.|。)?$",
|
2024-06-15 18:26:20 -04:00
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_number = re.compile(r"^[\s\d\:\.]+$")
|
|
|
|
|
|
2024-06-17 09:33:50 -04:00
|
|
|
|
_separaters = {"–", "―", "−", "—", "-"}
|
|
|
|
|
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
class Note(Content):
|
2025-01-19 16:04:22 -05:00
|
|
|
|
post_when_save = True
|
|
|
|
|
index_when_save = True
|
|
|
|
|
|
2024-06-15 18:26:20 -04:00
|
|
|
|
class ProgressType(models.TextChoices):
|
|
|
|
|
PAGE = "page", _("Page")
|
|
|
|
|
CHAPTER = "chapter", _("Chapter")
|
|
|
|
|
# SECTION = "section", _("Section")
|
|
|
|
|
# VOLUME = "volume", _("Volume")
|
|
|
|
|
PART = "part", _("Part")
|
|
|
|
|
EPISODE = "episode", _("Episode")
|
|
|
|
|
TRACK = "track", _("Track")
|
|
|
|
|
CYCLE = "cycle", _("Cycle")
|
|
|
|
|
TIMESTAMP = "timestamp", _("Timestamp")
|
|
|
|
|
PERCENTAGE = "percentage", _("Percentage")
|
|
|
|
|
|
2024-06-13 20:44:15 -04:00
|
|
|
|
title = models.TextField(blank=True, null=True, default=None)
|
|
|
|
|
content = models.TextField(blank=False, null=False)
|
|
|
|
|
sensitive = models.BooleanField(default=False, null=False)
|
2024-06-13 22:03:35 -04:00
|
|
|
|
attachments = models.JSONField(default=list)
|
2024-06-15 18:26:20 -04:00
|
|
|
|
progress_type = models.CharField(
|
|
|
|
|
max_length=50,
|
|
|
|
|
choices=ProgressType.choices,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
default=None,
|
|
|
|
|
)
|
|
|
|
|
progress_value = models.CharField(
|
|
|
|
|
max_length=500, blank=True, null=True, default=None
|
|
|
|
|
)
|
|
|
|
|
_progress_display_template = {
|
|
|
|
|
ProgressType.PAGE: _("Page {value}"),
|
|
|
|
|
ProgressType.CHAPTER: _("Chapter {value}"),
|
|
|
|
|
# ProgressType.SECTION: _("Section {value}"),
|
|
|
|
|
# ProgressType.VOLUME: _("Volume {value}"),
|
|
|
|
|
ProgressType.PART: _("Part {value}"),
|
|
|
|
|
ProgressType.EPISODE: _("Episode {value}"),
|
|
|
|
|
ProgressType.TRACK: _("Track {value}"),
|
|
|
|
|
ProgressType.CYCLE: _("Cycle {value}"),
|
|
|
|
|
ProgressType.PERCENTAGE: "{value}%",
|
|
|
|
|
ProgressType.TIMESTAMP: "{value}",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
indexes = [models.Index(fields=["owner", "item", "created_time"])]
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def html(self):
|
|
|
|
|
return render_text(self.content)
|
|
|
|
|
|
2024-06-15 18:26:20 -04:00
|
|
|
|
@property
|
|
|
|
|
def progress_display(self) -> str:
|
|
|
|
|
if not self.progress_value:
|
|
|
|
|
return ""
|
|
|
|
|
if not self.progress_type:
|
|
|
|
|
return str(self.progress_value)
|
|
|
|
|
tpl = Note._progress_display_template.get(self.progress_type, None)
|
|
|
|
|
if not tpl:
|
|
|
|
|
return str(self.progress_value)
|
|
|
|
|
if _number.match(self.progress_value):
|
|
|
|
|
return tpl.format(value=self.progress_value)
|
2024-06-16 00:22:17 -04:00
|
|
|
|
return Note.ProgressType(self.progress_type).label + ": " + self.progress_value
|
2024-06-15 18:26:20 -04:00
|
|
|
|
|
2024-06-13 20:44:15 -04:00
|
|
|
|
@property
|
|
|
|
|
def ap_object(self):
|
|
|
|
|
d = {
|
|
|
|
|
"id": self.absolute_url,
|
|
|
|
|
"type": "Note",
|
|
|
|
|
"title": self.title,
|
|
|
|
|
"content": self.content,
|
|
|
|
|
"sensitive": self.sensitive,
|
|
|
|
|
"published": self.created_time.isoformat(),
|
|
|
|
|
"updated": self.edited_time.isoformat(),
|
|
|
|
|
"attributedTo": self.owner.actor_uri,
|
|
|
|
|
"withRegardTo": self.item.absolute_url,
|
|
|
|
|
"href": self.absolute_url,
|
|
|
|
|
}
|
2024-06-15 18:26:20 -04:00
|
|
|
|
if self.progress_value:
|
|
|
|
|
d["progress"] = {
|
2024-06-17 15:10:34 -04:00
|
|
|
|
"type": self.progress_type or "",
|
2024-06-15 18:26:20 -04:00
|
|
|
|
"value": self.progress_value,
|
|
|
|
|
}
|
2024-06-13 20:44:15 -04:00
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
@classmethod
|
|
|
|
|
def params_from_ap_object(cls, post, obj, piece):
|
2024-06-13 22:03:35 -04:00
|
|
|
|
params = {
|
2024-06-13 20:44:15 -04:00
|
|
|
|
"title": obj.get("title", post.summary),
|
2024-06-17 15:10:34 -04:00
|
|
|
|
"content": obj.get("content", "").strip(),
|
2024-06-13 20:44:15 -04:00
|
|
|
|
"sensitive": obj.get("sensitive", post.sensitive),
|
2024-06-13 22:03:35 -04:00
|
|
|
|
"attachments": [],
|
2024-06-13 20:44:15 -04:00
|
|
|
|
}
|
2024-06-17 15:10:34 -04:00
|
|
|
|
if post.local:
|
|
|
|
|
# for local post, strip footer and detect progress from content
|
|
|
|
|
# if not detected, keep default/original value by not including it in return val
|
|
|
|
|
params["content"], progress_type, progress_value = cls.strip_footer(
|
|
|
|
|
params["content"]
|
|
|
|
|
)
|
|
|
|
|
if progress_value is not None:
|
2024-06-15 18:26:20 -04:00
|
|
|
|
params["progress_type"] = progress_type
|
|
|
|
|
params["progress_value"] = progress_value
|
2024-06-17 15:10:34 -04:00
|
|
|
|
else:
|
|
|
|
|
# for remote post, progress is always in "progress" field
|
|
|
|
|
progress = obj.get("progress", {})
|
|
|
|
|
params["progress_value"] = progress.get("value", None)
|
|
|
|
|
params["progress_type"] = None
|
|
|
|
|
if params["progress_value"]:
|
|
|
|
|
t = progress.get("type", None)
|
|
|
|
|
try:
|
|
|
|
|
params["progress_type"] = Note.ProgressType(t)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
2024-06-13 22:03:35 -04:00
|
|
|
|
if post:
|
|
|
|
|
for atta in post.attachments.all():
|
|
|
|
|
params["attachments"].append(
|
|
|
|
|
{
|
|
|
|
|
"type": (atta.mimetype or "unknown").split("/")[0],
|
|
|
|
|
"mimetype": atta.mimetype,
|
|
|
|
|
"url": atta.full_url().absolute,
|
|
|
|
|
"preview_url": atta.thumbnail_url().absolute,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return params
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
@classmethod
|
2025-01-19 16:04:22 -05:00
|
|
|
|
def update_by_ap_object(cls, owner, item, obj, post, crosspost=None):
|
|
|
|
|
crosspost = (
|
|
|
|
|
owner.local
|
|
|
|
|
and owner.user.preference.mastodon_default_repost
|
|
|
|
|
and owner.user.mastodon is not None
|
|
|
|
|
)
|
|
|
|
|
return super().update_by_ap_object(owner, item, obj, post, crosspost)
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
def shelfmember(self) -> ShelfMember | None:
|
|
|
|
|
return ShelfMember.objects.filter(item=self.item, owner=self.owner).first()
|
|
|
|
|
|
2024-07-03 16:42:20 -04:00
|
|
|
|
def to_crosspost_params(self):
|
2024-06-15 21:54:39 -04:00
|
|
|
|
footer = f"\n—\n《{self.item.display_title}》 {self.progress_display}\n{self.item.absolute_url}"
|
2024-06-13 22:03:35 -04:00
|
|
|
|
params = {
|
2024-06-13 20:44:15 -04:00
|
|
|
|
"spoiler_text": self.title,
|
2024-06-15 21:54:39 -04:00
|
|
|
|
"content": self.content + footer,
|
2024-06-13 20:44:15 -04:00
|
|
|
|
"sensitive": self.sensitive,
|
2024-07-04 12:43:45 -04:00
|
|
|
|
"reply_to_ids": (
|
|
|
|
|
self.shelfmember.metadata.copy() if self.shelfmember else {}
|
2024-06-13 20:44:15 -04:00
|
|
|
|
),
|
|
|
|
|
}
|
2024-06-13 22:03:35 -04:00
|
|
|
|
if self.latest_post:
|
|
|
|
|
attachments = []
|
|
|
|
|
for atta in self.latest_post.attachments.all():
|
|
|
|
|
attachments.append((atta.file_display_name, atta.file, atta.mimetype))
|
|
|
|
|
if attachments:
|
|
|
|
|
params["attachments"] = attachments
|
|
|
|
|
return params
|
2024-06-13 20:44:15 -04:00
|
|
|
|
|
|
|
|
|
def to_post_params(self):
|
2024-06-15 21:54:39 -04:00
|
|
|
|
footer = f'\n<p>—<br><a href="{self.item.absolute_url}">{self.item.display_title}</a> {self.progress_display}\n</p>'
|
2024-06-16 15:35:46 -04:00
|
|
|
|
post = self.shelfmember.latest_post if self.shelfmember else None
|
2024-06-13 20:44:15 -04:00
|
|
|
|
return {
|
|
|
|
|
"summary": self.title,
|
|
|
|
|
"content": self.content,
|
2024-06-15 21:54:39 -04:00
|
|
|
|
"append_content": footer,
|
2024-06-13 20:44:15 -04:00
|
|
|
|
"sensitive": self.sensitive,
|
2024-06-16 15:35:46 -04:00
|
|
|
|
"reply_to_pk": post.pk if post else None,
|
2024-06-13 22:03:35 -04:00
|
|
|
|
# not passing "attachments" so it won't change
|
2024-06-13 20:44:15 -04:00
|
|
|
|
}
|
2024-06-15 18:26:20 -04:00
|
|
|
|
|
|
|
|
|
@classmethod
|
2024-06-17 15:10:34 -04:00
|
|
|
|
def strip_footer(cls, content: str) -> tuple[str, str | None, str | None]:
|
|
|
|
|
"""strip footer if 2nd last line is "-" or similar characters"""
|
|
|
|
|
lines = content.splitlines()
|
|
|
|
|
if len(lines) < 3 or lines[-2].strip() not in _separaters:
|
|
|
|
|
return content, None, None
|
|
|
|
|
progress_type, progress_value = cls.extract_progress(lines[-1])
|
2024-06-18 00:10:31 -04:00
|
|
|
|
# if progress_value is None and not lines[-1].startswith("https://"):
|
2024-06-17 15:10:34 -04:00
|
|
|
|
# return content, None, None
|
2024-06-18 00:10:31 -04:00
|
|
|
|
return ( # remove one extra empty line generated from <p> tags
|
|
|
|
|
"\n".join(lines[: (-3 if lines[-3] == "" else -2)]),
|
|
|
|
|
progress_type,
|
|
|
|
|
progress_value,
|
|
|
|
|
)
|
2024-06-17 15:10:34 -04:00
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def extract_progress(cls, content) -> tuple[str | None, str | None]:
|
2024-06-15 18:26:20 -04:00
|
|
|
|
m = _progress.match(content)
|
2024-06-15 21:54:39 -04:00
|
|
|
|
if not m:
|
|
|
|
|
m = _progress2.match(content)
|
2024-06-15 18:26:20 -04:00
|
|
|
|
if m and m["value"]:
|
2024-06-17 15:10:34 -04:00
|
|
|
|
if m["value"] == "-":
|
|
|
|
|
return None, ""
|
2024-06-15 21:54:39 -04:00
|
|
|
|
m = m.groupdict()
|
|
|
|
|
typ_ = "percentage" if m["postfix"] == "%" else m.get("prefix", "")
|
2024-06-18 00:10:31 -04:00
|
|
|
|
match typ_.lower():
|
2024-06-15 18:26:20 -04:00
|
|
|
|
case "p" | "pg" | "page":
|
|
|
|
|
typ = Note.ProgressType.PAGE
|
|
|
|
|
case "ch" | "chapter":
|
|
|
|
|
typ = Note.ProgressType.CHAPTER
|
|
|
|
|
# case "vol" | "volume":
|
|
|
|
|
# typ = ProgressType.VOLUME
|
|
|
|
|
# case "section":
|
|
|
|
|
# typ = ProgressType.SECTION
|
|
|
|
|
case "pt" | "part":
|
|
|
|
|
typ = Note.ProgressType.PART
|
|
|
|
|
case "e" | "ep" | "episode":
|
|
|
|
|
typ = Note.ProgressType.EPISODE
|
|
|
|
|
case "trk" | "track":
|
|
|
|
|
typ = Note.ProgressType.TRACK
|
|
|
|
|
case "cycle":
|
|
|
|
|
typ = Note.ProgressType.CYCLE
|
|
|
|
|
case "percentage":
|
|
|
|
|
typ = Note.ProgressType.PERCENTAGE
|
|
|
|
|
case _:
|
|
|
|
|
typ = "timestamp" if ":" in m["value"] else None
|
|
|
|
|
return typ, m["value"]
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_progress_types_by_item(cls, item: Item):
|
|
|
|
|
match item.__class__.__name__:
|
|
|
|
|
case "Edition":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.PAGE,
|
|
|
|
|
Note.ProgressType.CHAPTER,
|
|
|
|
|
Note.ProgressType.PERCENTAGE,
|
|
|
|
|
]
|
|
|
|
|
case "TVShow" | "TVSeason":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.PART,
|
|
|
|
|
Note.ProgressType.EPISODE,
|
|
|
|
|
Note.ProgressType.PERCENTAGE,
|
|
|
|
|
]
|
|
|
|
|
case "Movie":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.PART,
|
|
|
|
|
Note.ProgressType.TIMESTAMP,
|
|
|
|
|
Note.ProgressType.PERCENTAGE,
|
|
|
|
|
]
|
|
|
|
|
case "Podcast":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.EPISODE,
|
|
|
|
|
]
|
|
|
|
|
case "TVEpisode" | "PodcastEpisode":
|
|
|
|
|
v = []
|
|
|
|
|
case "Album":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.TRACK,
|
|
|
|
|
Note.ProgressType.TIMESTAMP,
|
|
|
|
|
Note.ProgressType.PERCENTAGE,
|
|
|
|
|
]
|
|
|
|
|
case "Game":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.CYCLE,
|
|
|
|
|
]
|
|
|
|
|
case "Performance" | "PerformanceProduction":
|
|
|
|
|
v = [
|
|
|
|
|
Note.ProgressType.PART,
|
|
|
|
|
Note.ProgressType.TIMESTAMP,
|
|
|
|
|
Note.ProgressType.PERCENTAGE,
|
|
|
|
|
]
|
|
|
|
|
case _:
|
|
|
|
|
v = []
|
|
|
|
|
return v
|
2024-12-30 01:51:19 -05:00
|
|
|
|
|
|
|
|
|
def to_indexable_doc(self) -> dict[str, Any]:
|
|
|
|
|
return {
|
|
|
|
|
"item_id": [self.item.id],
|
|
|
|
|
"item_class": [self.item.__class__.__name__],
|
|
|
|
|
"item_title": self.item.to_indexable_titles(),
|
|
|
|
|
"content": [self.title or "", self.content],
|
|
|
|
|
}
|