comment podcast episodes

This commit is contained in:
Your Name 2023-01-31 21:21:50 -05:00 committed by Henri Dickson
parent ca2d26f57b
commit d891a58971
13 changed files with 329 additions and 46 deletions

View file

@ -54,7 +54,8 @@ class Podcast(Item):
# pass
class PodcastEpisode(Item):
category = ItemCategory.Podcast
url_path = "podcastepisode"
url_path = "podcast/episode"
demonstrative = _("这集节目")
# uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
program = models.ForeignKey(Podcast, models.CASCADE, related_name="episodes")
guid = models.CharField(null=True, max_length=1000)
@ -67,6 +68,17 @@ class PodcastEpisode(Item):
cover_url = models.CharField(null=True, max_length=1000)
duration = models.PositiveIntegerField(null=True)
@property
def parent_item(self):
return self.program
def get_absolute_url_with_position(self, position=None):
return (
self.absolute_url
if position is None or position == ""
else f"{self.absolute_url}?position={position}"
)
class Meta:
index_together = [
[

View file

@ -13,9 +13,9 @@
</script>
{% endif %}
{% if jquery %}
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/3.6.3/jquery.min.js"></script>
{% else %}
<script src="https://cdn.staticfile.org/cash/8.1.2/cash.min.js"></script>
<script src="https://cdn.staticfile.org/cash/8.1.3/cash.min.js"></script>
<script type="text/javascript">
$.fn.is_visible = function visible() {
return this.filter((_, elt) => (elt.offsetWidth || elt.offsetHeight || elt.getClientRects().length)).length > 0;
@ -29,7 +29,7 @@
};
</script>
{% endif %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.4/htmx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.5/htmx.min.js"></script>
<script src="{% static 'lib/js/hyperscript-0.9.7.min.js' %}"></script>
<script src="{% static 'lib/js/rating-star.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">

View file

@ -10,7 +10,7 @@
{% load strip_scheme %}
{% load thumb %}
{% block opengraph %}
{% block head %}
{% if item.author %}
<meta property="og:book:author" content="{% for author in item.author %}{{ author }}{% if not forloop.last %},{% endif %}{% endfor %}">
{% endif %}

View file

@ -21,7 +21,7 @@
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ item.cover.url }}">
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:description" content="{{ item.brief }}">
{% block opengraph %}
{% block head %}
{% endblock %}
<title>{{ site_name }} - {% trans item.category.label %} | {{ item.title }}</title>
{% include "common_libs.html" with jquery=0 %}

View file

@ -10,7 +10,7 @@
{% load strip_scheme %}
{% load thumb %}
{% block opengraph %}
{% block head %}
<script src=" https://cdn.jsdelivr.net/npm/shikwasa@2.2.0/dist/shikwasa.min.js "></script>
<link href=" https://cdn.jsdelivr.net/npm/shikwasa@2.2.0/dist/style.min.css " rel="stylesheet"></link>
{% endblock %}
@ -88,11 +88,12 @@
{% block sidebar %}
<div class="aside-section-wrapper">
<div class="action-panel" id="episodes">
<div class="action-panel__label">{% trans '最近更新' %}</div>
<div class="action-panel__label">{% trans '近期单集' %}</div>
<div >
{% for ep in item.recent_episodes %}
<p>
<a data-media="{{ ep.media_url }}" data-cover="{{ ep.cover_url|default:item.cover }}" class="episode" href="{{ep.link}}">{{ ep.title }}</a>
<a data-media="{{ ep.media_url }}" data-cover="{{ ep.cover_url|default:item.cover }}" class="episode" href="{{ep.url}}" data-uuid="{{ep.uuid}}">{{ ep.title }}</a>
<a href="#" hx-get="{% url 'journal:comment' item.uuid ep.uuid %}" hx-target="body" hx-swap="beforeend"><span class="comment icon"></span></a>
</p>
{% endfor %}
</div>
@ -122,37 +123,42 @@
</script>
<script type="text/javascript">
album = "{{ item.title|escapejs }}"
artists = "{{ item.hosts|join:' / '|escapejs }}"
function create_player(audio) {
window.player = new Shikwasa.Player({
container: () => document.querySelector('.player'),
preload: 'metadata',
autoplay: true,
themeColor: '#1190C0',
fixed: {
type: 'fixed',
position: 'bottom'
},
audio: audio
});
$('.shk-title').on('click', e=>{
window.location = "#episodes";
});
$('.footer').attr('style', 'margin-bottom: 120px !important');
}
$(()=>{
$('.episode').on('click', e=>{
e.preventDefault();
ele = e.target;
album = "{{ item.title|escapejs }}"
artists = "{{ item.hosts|join:' / '|escapejs }}"
title = $(ele).text();
cover_url = $(ele).data('cover');
media_url = $(ele).data('media');
if (!media_url) return;
window.current_item_uuid = $(ele).data('uuid');
if (!window.player) {
window.player = new Shikwasa.Player({
container: () => document.querySelector('.player'),
preload: 'metadata',
autoplay: true,
themeColor: '#1190C0',
fixed: {
type: 'fixed',
position: 'bottom'
},
audio: {
title: title,
cover: cover_url,
src: media_url,
album: album,
artist: artists
}
});
$('.shk-title').on('click', e=>{
window.location = "#episodes";
});
create_player({
title: title,
cover: cover_url,
src: media_url,
album: album,
artist: artists
})
} else {
window.player.update({
title: title,
@ -164,7 +170,18 @@
}
window.player.play()
});
$('.footer').attr('style', 'margin-bottom: 120px !important');
{% if focus_item %}
loc = 1 * "{{request.GET.position|escapejs}}";
window.current_item_uuid = "{{focus_item.uuid}}";
create_player({
title: "{{ focus_item.title | escapejs }}",
cover: "{{ focus_item.cover_url | default:'' | escapejs }}",
src: "{{ focus_item.media_url | escapejs }}",
album: album,
artist: artists
})
if (loc) window.player._initSeek = loc;
{% endif %}
});
</script>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "item_base.html" %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
{% block head %}
<meta http-equiv="refresh" content="0;url={{ item.program.absolute_url }}?focus={{ item.uuid }}{% if request.GET.position %}&position={{ request.GET.position }}{% endif %}" />
{% endblock %}

View file

@ -50,6 +50,11 @@ def retrieve(request, item_path, item_uuid):
return redirect(item.merged_to_item.url)
if not skipcheck and item.is_deleted:
return HttpResponseNotFound("item not found")
focus_item = None
if request.GET.get("focus"):
focus_item = get_object_or_404(
Item, uid=base62.decode(request.GET.get("focus"))
)
mark = None
review = None
mark_list = None
@ -87,6 +92,7 @@ def retrieve(request, item_path, item_uuid):
item.class_name + ".html",
{
"item": item,
"focus_item": focus_item,
"mark": mark,
"review": review,
"mark_list": mark_list,

View file

@ -195,3 +195,33 @@ progress {
.markdownx-editor {
font-family: monospace;
}
.icon {
color: lightgray;
}
.icon:hover {
color: #00a1cc;
}
.comment.icon {
box-sizing: unset;
position: absolute;
margin-left: 2px;
margin-top: 4px;
width: 15px;
height: 10px;
border: solid 1px currentColor;
border-radius: 2px;
}
.comment.icon:before {
content: '';
position: absolute;
left: 3px;
top: 8px;
width: 4px;
height: 4px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
background-color: white;
border-bottom: solid 1px currentColor;
border-right: solid 1px currentColor;
}

View file

@ -187,6 +187,9 @@ class Memo(Content):
class Comment(Content):
text = models.TextField(blank=False, null=False)
focus_item = models.ForeignKey(
Item, on_delete=models.PROTECT, null=True, related_name="focused_comments"
)
@property
def html(self):
@ -950,7 +953,9 @@ class Mark:
@cached_property
def comment(self):
return Comment.objects.filter(owner=self.owner, item=self.item).first()
return Comment.objects.filter(
owner=self.owner, item=self.item, focus_item__isnull=True
).first()
@property
def text(self):

View file

@ -0,0 +1,83 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load highlight %}
{% load thumb %}
<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
<div class="modal-underlay" _="on click trigger closeModal"></div>
<div class="modal-content">
<div class="add-to-list-modal__head">
<span class="add-to-list-modal__title">{% trans '分享' %} {{ item.title }}: {{ focus_item.title }}</span>
<span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="add-to-list-modal__body">
<form action="{% url 'journal:comment' item.uuid focus_item.uuid %}" method="post">
{% csrf_token %}
<textarea name="text" cols="40" rows="10" placeholder="超过360字部分实例可能无法显示" maxlength="360" id="id_text">{% if comment.text %}{{ comment.text }}{% endif %}</textarea>
<div class="mark-modal__share-checkbox float-right">
<label for="id_share_to_mastodon"><input type="checkbox" name="share_to_mastodon" id="id_share_to_mastodon" value="1" checked>分享到联邦网络</label>
</div>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>可见性:
<ul id="id_visibility">
<li><label for="id_visibility_0"><input type="radio" name="visibility" value="0" required="" id="id_visibility_0" {% if comment.visibility == 0 or not comment %}checked{% endif %}> 公开</label> </li>
<li><label for="id_visibility_1"><input type="radio" name="visibility" value="1" required="" id="id_visibility_1" {% if comment.visibility == 1 %}checked{% endif %}> 仅关注者</label> </li>
<li><label for="id_visibility_2"><input type="radio" name="visibility" value="2" required="" id="id_visibility_2" {% if comment.visibility == 2 %}checked{% endif %}> 仅自己</label> </li>
</ul>
</span>
</div>
</div>
<div class="mark-modal__confirm-button">
<input type="submit" class="button float-right" value="保存">
</div>
<style type="text/css">
input:invalid#position {
border: red dashed 1px;
}
</style>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>
<label for="share_position"><input type="checkbox" name="share_position" value="1" id="share_position"> 分享播放位置: </label>
<input type="input" name="position" id="position" value="00:00:00" class="html-duration-picker" pattern="[0-9]{2}:[0-9]{2}:[0-9]{2}" title="hh:mm:ss" style="display: none;">
</span>
</div>
</div>
</form>
</div>
</div>
<script type="text/javascript">
if (window.player && window.current_item_uuid == "{{ focus_item.uuid }}"){
$('#share_position').prop('checked', true);
$("#position").show()
var t = ~~ window.player.currentTime;
$('#position').val(('0' + ~~(t/3600)).substr(-2) + ':' + ('0' + ~~(t%3600/60)).substr(-2) + ':' + ("0" + t%60).substr(-2))
}
$('#share_position').on('click', ()=>{
if ($('#share_position').prop('checked')) {
$("#position").show()
} else {
$("#position").hide()
}
})
</script>

View file

@ -21,6 +21,7 @@ urlpatterns = [
path("like/<str:piece_uuid>", like, name="like"),
path("unlike/<str:piece_uuid>", unlike, name="unlike"),
path("mark/<str:item_uuid>", mark, name="mark"),
path("comment/<str:item_uuid>/<str:focus_item_uuid>", comment, name="comment"),
path(
"add_to_collection/<str:item_uuid>", add_to_collection, name="add_to_collection"
),

View file

@ -21,12 +21,19 @@ from django.db.models import Q
from management.models import Announcement
from django.utils.baseconv import base62
from .forms import *
from mastodon.api import share_review, share_collection
from mastodon.api import (
get_spoiler_text,
share_review,
share_collection,
get_status_id_by_url,
post_toot,
get_visibility,
)
from users.views import render_user_blocked, render_user_not_found
from users.models import User, Report, Preference
from common.utils import PageLinksGenerator
from user_messages import api as msg
from datetime import datetime
_logger = logging.getLogger(__name__)
PAGE_SIZE = 10
@ -172,6 +179,91 @@ def mark(request, item_uuid):
return HttpResponseBadRequest()
@login_required
def comment(request, item_uuid, focus_item_uuid):
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
focus_item = get_object_or_404(Item, uid=base62.decode(focus_item_uuid))
if focus_item.parent_item != item:
return HttpResponseNotFound()
comment = Comment.objects.filter(
owner=request.user, item=item, focus_item=focus_item
).first()
if request.method == "GET":
return render(
request,
"comment.html",
{
"item": item,
"focus_item": focus_item,
"comment": comment,
},
)
elif request.method == "POST":
if request.POST.get("delete", default=False):
if not comment:
return HttpResponseNotFound()
comment.delete()
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
visibility = int(request.POST.get("visibility", default=0))
text = request.POST.get("text")
position = (
request.POST.get("position")
if request.POST.get("share_position")
else "0:0:0"
)
try:
pos = datetime.strptime(position, "%H:%M:%S")
position = pos.hour * 3600 + pos.minute * 60 + pos.second
except:
raise
position = 0
share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False))
shared_link = None
post_error = False
if share_to_mastodon:
shared_link = comment.metadata.get("shared_link") if comment else None
status_id = get_status_id_by_url(shared_link)
link = focus_item.get_absolute_url_with_position(position or None)
status = f"分享{ItemCategory(item.category).label}{item.title}》的《{focus_item.title}\n{link}\n{text}"
spoiler, status = get_spoiler_text(status, item)
try:
response = post_toot(
request.user.mastodon_site,
status,
get_visibility(visibility, request.user),
request.user.mastodon_token,
False,
status_id,
spoiler,
)
if response and response.status_code in [200, 201]:
j = response.json()
if "url" in j:
shared_link = j["url"]
except Exception as e:
raise
post_error = True
if comment:
comment.visibility = visibility
comment.text = text
if shared_link:
comment.metadata["shared_link"] = shared_link
comment.save()
else:
comment = Comment.objects.create(
owner=request.user,
item=item,
focus_item=focus_item,
text=text,
visibility=visibility,
metadata={"shared_link": shared_link},
)
if post_error:
return render_relogin(request)
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
return HttpResponseBadRequest()
def collection_retrieve(request, collection_uuid):
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
if not collection.is_visible_to(request.user):
@ -712,7 +804,7 @@ def profile(request, user_name):
ItemCategory.Movie,
ItemCategory.TV,
ItemCategory.Music,
# ItemCategory.Podcast,
ItemCategory.Podcast,
ItemCategory.Game,
]
for category in visbile_categories:

View file

@ -117,6 +117,8 @@ def post_toot(
response.status_code = 200
if response.status_code != 200:
logger.error(f"Error {url} {response.status_code}")
print(payload)
print(response)
except Exception:
response = None
return response
@ -454,6 +456,34 @@ def revoke_token(site, token):
post(url, data=payload, headers={"User-Agent": USER_AGENT})
def get_status_id_by_url(url):
if not url:
return None
r = re.match(
r".+/(\w+)$", url
) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
return r[1] if r else None
def get_spoiler_text(text, item):
if text.find(">!") != -1:
spoiler_text = f"关于《{item.title}》 可能有关键情节等敏感内容"
return spoiler_text, text.replace(">!", "").replace("!<", "")
else:
return None, text
def get_visibility(visibility, user):
if visibility == 2:
return TootVisibilityEnum.DIRECT
elif visibility == 1:
return TootVisibilityEnum.PRIVATE
elif user.get_preference().mastodon_publish_public:
return TootVisibilityEnum.PUBLIC
else:
return TootVisibilityEnum.UNLISTED
def share_mark(mark):
from catalog.common import ItemCategory
@ -479,16 +509,8 @@ def share_mark(mark):
MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode,
)
content = f"{mark.translated_status}{mark.item.title}{stars}\n{mark.item.absolute_url}\n{mark.text or ''}{tags}"
update_id = None
if mark.shared_link: # "https://mastodon.social/@username/1234567890"
r = re.match(
r".+/(\w+)$", mark.shared_link
) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
update_id = r[1] if r else None
spoiler_text = None
if content.find(">!") != -1:
spoiler_text = f"关于《{mark.item.title}》 可能有关键情节等敏感内容"
content = content.replace(">!", "").replace("!<", "")
update_id = get_status_id_by_url(mark.shared_link)
spoiler_text, content = get_spoiler_text(content, mark.item)
response = post_toot(
user.mastodon_site,
content,