diff --git a/boofilsic/urls.py b/boofilsic/urls.py index 31ad0a73..50c1acdc 100644 --- a/boofilsic/urls.py +++ b/boofilsic/urls.py @@ -24,6 +24,7 @@ urlpatterns = [ path('markdownx/', include('markdownx.urls')), path('users/', include('users.urls')), path('books/', include('books.urls')), + path('movies/', include('movies.urls')), path('', include('common.urls')), ] diff --git a/common/scraper.py b/common/scraper.py index 92903b62..894f8585 100644 --- a/common/scraper.py +++ b/common/scraper.py @@ -3,10 +3,13 @@ import random from lxml import html import re from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG +from movies.models import MovieGenreEnum RE_NUMBERS = re.compile(r"\d+\d*") RE_WHITESPACES = re.compile(r"\s+") -RE_DOUBAN_BOOK_URL = re.compile(r"https://book.douban.com/subject/\d+/") +# without slash at the end +RE_DOUBAN_BOOK_URL = re.compile(r"https://book.douban.com/subject/\d+") +RE_DOUBAN_MOVIE_URL = re.compile(r"https://movie.douban.com/subject/\d+") DEFAULT_REQUEST_HEADERS = { 'Host': 'book.douban.com', @@ -18,7 +21,7 @@ DEFAULT_REQUEST_HEADERS = { 'Connection': 'keep-alive', 'DNT': '1', 'Upgrade-Insecure-Requests': '1', - 'Cache-Control': 'max-age=0', + 'Cache-Control': 'no-cache', } # in seconds @@ -28,24 +31,67 @@ TIMEOUT = 10 PORT = 22225 -def scrape_douban_book(url): - if RE_DOUBAN_BOOK_URL.match(url) is None: - raise ValueError("not valid douban book url") +def download_page(url, regex, headers): + url = regex.findall(url) + if not url: + raise ValueError("not valid url") + else: + url = url[0] + '/' session_id = random.random() proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' % - (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT)) + (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT)) proxies = { 'http': proxy_url, - 'https': proxy_url, + 'https': proxy_url, } - if DEBUG: - proxies = None - r = requests.get(url, proxies=proxies, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT) + # if DEBUG: + # proxies = None + r = requests.get(url, proxies=proxies, headers=headers, timeout=TIMEOUT) # r = requests.get(url, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT) - - content = html.fromstring(r.content.decode('utf-8')) + return html.fromstring(r.content.decode('utf-8')) + + +def download_image(url): + if url is None: + return + raw_img = None + session_id = random.random() + proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' % + (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT)) + proxies = { + 'http': proxy_url, + 'https': proxy_url, + } + # if DEBUG: + # proxies = None + if url: + img_response = requests.get( + url, + headers={ + 'accept': 'image/webp,image/apng,image/*,*/*;q=0.8', + 'accept-encoding': 'gzip, deflate', + 'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,fr-FR;q=0.6,fr;q=0.5,zh-TW;q=0.4', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edg/81.0.416.72', + 'cache-control': 'no-cache', + 'dnt': '1', + }, + proxies=proxies, + timeout=TIMEOUT, + ) + if img_response.status_code == 200: + raw_img = img_response.content + return raw_img + + +def scrape_douban_book(url): + regex = RE_DOUBAN_BOOK_URL + headers = DEFAULT_REQUEST_HEADERS.copy() + headers['Host'] = 'book.douban.com' + content = download_page(url, regex, headers) + + # parsing starts here try: title = content.xpath("/html/body/div[3]/h1/span/text()")[0].strip() except IndexError: @@ -111,23 +157,7 @@ def scrape_douban_book(url): img_url_elem = content.xpath("//*[@id='mainpic']/a/img/@src") img_url = img_url_elem[0].strip() if img_url_elem else None - raw_img = None - if img_url: - img_response = requests.get( - img_url, - headers={ - 'accept': 'image/webp,image/apng,image/*,*/*;q=0.8', - 'accept-encoding': 'gzip, deflate', - 'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,fr-FR;q=0.6,fr;q=0.5,zh-TW;q=0.4', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edg/81.0.416.72', - 'cache-control': 'no-cache', - 'dnt': '1' , - }, - proxies=proxies, - timeout=TIMEOUT, - ) - if img_response.status_code == 200: - raw_img = img_response.content + raw_img = download_image(img_url) # there are two html formats for authors and translators authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/ @@ -182,3 +212,143 @@ def scrape_douban_book(url): 'other_info' : other } return data, raw_img + + +def scrape_douban_movie(url): + regex = RE_DOUBAN_MOVIE_URL + headers = DEFAULT_REQUEST_HEADERS.copy() + headers['Host'] = 'movie.douban.com' + content = download_page(url, regex, headers) + + # parsing starts here + try: + raw_title = content.xpath( + "//span[@property='v:itemreviewed']/text()")[0].strip() + except IndexError: + raise ValueError("given url contains no movie info") + + orig_title = content.xpath( + "//img[@rel='v:image']/@alt")[0].strip() + title = raw_title.split(orig_title)[0].strip() + # if has no chinese title + if title == '': + title = orig_title + + # there are two html formats for authors and translators + other_title_elem = content.xpath( + "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]") + other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None + + imbd_elem = content.xpath( + "//div[@id='info']//span[text()='IMDb链接:']/following-sibling::a[1]/text()") + imbd_code = imbd_elem[0].strip() if imbd_elem else None + + director_elem = content.xpath("//div[@id='info']//span[text()='导演']/following-sibling::span[1]/a/text()") + director = director_elem if director_elem else None + + playwright_elem = content.xpath("//div[@id='info']//span[text()='编剧']/following-sibling::span[1]/a/text()") + playwright = playwright_elem if playwright_elem else None + + actor_elem = content.xpath("//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()") + actor = actor_elem if actor_elem else None + + # construct genre translator + genre_translator = {} + attrs = [attr for attr in dir(MovieGenreEnum) if not '__' in attr] + for attr in attrs: + genre_translator[getattr(MovieGenreEnum, attr).label] = getattr( + MovieGenreEnum, attr).value + + genre_elem = content.xpath("//span[@property='v:genre']/text()") + if genre_elem: + genre = [] + for g in genre_elem: + genre.append(genre_translator[g]) + else: + genre = None + + showtime_elem = content.xpath("//span[@property='v:initialReleaseDate']/text()") + if showtime_elem: + showtime = [] + for st in showtime_elem: + time = st.split('(')[0] + region = st.split('(')[1][0:-1] + showtime.append({time: region}) + else: + showtime = None + + site_elem = content.xpath("//div[@id='info']//span[text()='官方网站:']/following-sibling::a[1]/@href") + site = site_elem[0].strip() if site_elem else None + + area_elem = content.xpath("//div[@id='info']//span[text()='制片国家/地区:']/following-sibling::text()[1]") + if area_elem: + area = [a.strip() for a in area_elem[0].split(' / ')] + else: + area = None + + language_elem = content.xpath("//div[@id='info']//span[text()='语言:']/following-sibling::text()[1]") + if language_elem: + language = [a.strip() for a in language_elem[0].split(' / ')] + else: + language = None + + year_elem = content.xpath("//span[@class='year']/text()") + year = int(year_elem[0][1:-1]) if year_elem else None + + duration_elem = content.xpath("//span[@property='v:runtime']/text()") + other_duration_elem = content.xpath("//span[@property='v:runtime']/following-sibling::text()[1]") + if duration_elem: + duration = duration_elem[0].strip() + if other_duration_elem: + duration += other_duration_elem[0] + else: + duration = None + + season_elem = content.xpath("//*[@id='season']/option[@selected='selected']/text()") + if not season_elem: + season_elem = content.xpath( + "//div[@id='info']//span[text()='季数:']/following-sibling::text()[1]") + season = int(season_elem[0].strip()) if season_elem else None + else: + season = int(season_elem[0].strip()) + + episodes_elem = content.xpath( + "//div[@id='info']//span[text()='集数:']/following-sibling::text()[1]") + episodes = int(episodes_elem[0].strip()) if episodes_elem else None + + single_episode_length_elem = content.xpath( + "//div[@id='info']//span[text()='单集片长:']/following-sibling::text()[1]") + single_episode_length = int(single_episode_length_elem[0].strip()) if single_episode_length_elem else None + + # if has field `episodes` not none then must be series + is_series = True if episodes else False + + brief_elem = content.xpath("//span[@property='v:summary']/text()") + brief = brief[0].strip() if brief_elem else None + + img_url_elem = content.xpath("//img[@rel='v:image']/@src") + img_url = img_url_elem[0].strip() if img_url_elem else None + raw_img = download_image(img_url) + + data = { + 'title': title, + 'orig_title': orig_title, + 'other_title': other_title, + 'imbd_code': imbd_code, + 'director': director, + 'playwright': playwright, + 'actor': actor, + 'genre': genre, + 'showtime': showtime, + 'site': site, + 'area': area, + 'language': language, + 'year': year, + 'duration': duration, + 'season': season, + 'episodes': episodes, + 'single_episode_length': single_episode_length, + 'brief': brief, + 'is_series': is_series, + } + return data, raw_img diff --git a/books/static/js/scrape.js b/common/static/js/scrape.js similarity index 100% rename from books/static/js/scrape.js rename to common/static/js/scrape.js diff --git a/movies/__init__.py b/movies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/movies/apps.py b/movies/apps.py new file mode 100644 index 00000000..bda16f08 --- /dev/null +++ b/movies/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MoviesConfig(AppConfig): + name = 'movies' diff --git a/movies/forms.py b/movies/forms.py new file mode 100644 index 00000000..51e28c23 --- /dev/null +++ b/movies/forms.py @@ -0,0 +1,179 @@ +from django import forms +from django.contrib.postgres.forms import SimpleArrayField +from django.utils.translation import gettext_lazy as _ +from .models import Movie, MovieMark, MovieReview +from common.models import MarkStatusEnum +from common.forms import RadioBooleanField, RatingValidator, TagField, TagInput +from common.forms import KeyValueInput +from common.forms import PreviewImageInput + + +def MovieMarkStatusTranslator(status): + trans_dict = { + MarkStatusEnum.DO.value: _("在看"), + MarkStatusEnum.WISH.value: _("想看"), + MarkStatusEnum.COLLECT.value: _("看过") + } + return trans_dict[status] + + +class MovieForm(forms.ModelForm): + # pub_year = forms.IntegerField( + # required=False, max_value=9999, min_value=0, label=_("出版年份")) + # pub_month = forms.IntegerField( + # required=False, max_value=12, min_value=1, label=_("出版月份")) + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + + class Meta: + model = Movie + fields = [ + 'id', + 'title', + 'orig_title', + 'other_title', + 'imbd_code', + 'director', + 'playwright', + 'actor', + 'genre', + 'showtime', + 'site', + 'area', + 'language', + 'year', + 'duration', + 'season', + 'episodes', + 'single_episode_length', + 'cover', + 'brief', + 'is_series', + ] + labels = { + 'title': _("标题"), + 'orig_title': _("原名"), + 'other_title': _("又名"), + 'imbd_code': _("IMBd编号"), + 'director': _("导演"), + 'playwright': _("编剧"), + 'actor': _("主演"), + 'genre': _("类型"), + 'showtime': _("上映时间"), + 'site': _("官方网站"), + 'area': _("国家/地区"), + 'language': _("语言"), + 'year': _("年份"), + 'duration': _("片长"), + 'season': _("季数"), + 'episodes': _("集数"), + 'single_episode_length': _("单集片长"), + 'cover': _("封面"), + 'brief': _("简介"), + 'is_series': _("是否是剧集"), + } + + # widgets = { + # 'author': forms.TextInput(attrs={'placeholder': _("多个作者使用英文逗号分隔")}), + # 'translator': forms.TextInput(attrs={'placeholder': _("多个译者使用英文逗号分隔")}), + # 'other_info': KeyValueInput(), + # # 'cover': forms.FileInput(), + # 'cover': PreviewImageInput(), + # } + + # def clean_isbn(self): + # isbn = self.cleaned_data.get('isbn') + # if isbn: + # isbn = isbn.strip() + # return isbn + + +class MovieMarkForm(forms.ModelForm): + IS_PRIVATE_CHOICES = [ + (True, _("仅关注者")), + (False, _("公开")), + ] + STATUS_CHOICES = [(v, MovieMarkStatusTranslator(v)) + for v in MarkStatusEnum.values] + + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + share_to_mastodon = forms.BooleanField( + label=_("分享到长毛象"), initial=True, required=False) + rating = forms.IntegerField( + validators=[RatingValidator()], widget=forms.HiddenInput(), required=False) + status = forms.ChoiceField( + label=_(""), + widget=forms.RadioSelect(), + choices=STATUS_CHOICES + ) + is_private = RadioBooleanField( + label=_("可见性"), + initial=True, + choices=IS_PRIVATE_CHOICES + ) + tags = TagField( + required=False, + widget=TagInput(attrs={'placeholder': _("回车增加标签")}), + label=_("标签") + ) + text = forms.CharField( + required=False, + widget=forms.Textarea( + attrs={ + "placeholder": _("最多只能写360字哦~"), + "maxlength": 360 + } + ), + + label=_("短评"), + ) + + class Meta: + model = MovieMark + fields = [ + 'id', + 'movie', + 'status', + 'rating', + 'text', + 'is_private', + ] + labels = { + 'rating': _("评分"), + } + widgets = { + 'movie': forms.TextInput(attrs={"hidden": ""}), + } + + +class MovieReviewForm(forms.ModelForm): + IS_PRIVATE_CHOICES = [ + (True, _("仅关注者")), + (False, _("公开")), + ] + share_to_mastodon = forms.BooleanField( + label=_("分享到长毛象"), initial=True, required=False) + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + is_private = RadioBooleanField( + label=_("可见性"), + initial=True, + choices=IS_PRIVATE_CHOICES + ) + + class Meta: + model = MovieReview + fields = [ + 'id', + 'movie', + 'title', + 'content', + 'is_private' + ] + labels = { + 'book': "", + 'title': _("标题"), + 'content': _("正文"), + 'share_to_mastodon': _("分享到长毛象") + } + widgets = { + 'movie': forms.TextInput(attrs={"hidden": ""}), + } diff --git a/movies/models.py b/movies/models.py index 118aa8f9..a84ff70a 100644 --- a/movies/models.py +++ b/movies/models.py @@ -4,15 +4,22 @@ from django.utils.translation import ugettext_lazy as _ from django.db import models from django.core.serializers.json import DjangoJSONEncoder from common.models import Resource, Mark, Review, Tag -from boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE +from boofilsic.settings import MOVIE_MEDIA_PATH_ROOT, DEFAULT_MOVIE_IMAGE from django.utils import timezone -def movie_cover_path(): - pass +def movie_cover_path(instance, filename): + ext = filename.split('.')[-1] + filename = "%s.%s" % (uuid.uuid4(), ext) + root = '' + if MOVIE_MEDIA_PATH_ROOT.endswith('/'): + root = MOVIE_MEDIA_PATH_ROOT + else: + root = MOVIE_MEDIA_PATH_ROOT + '/' + return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}' -class GenreEnum(models.TextChoices): +class MovieGenreEnum(models.TextChoices): DRAMA = 'Drama', _('剧情') KIDS = 'Kids', _('儿童') COMEDY = 'Comedy', _('喜剧') @@ -90,7 +97,8 @@ class Movie(Resource): _("genre"), blank=True, default='', - choices=GenreEnum.choices + choices=MovieGenreEnum.choices, + max_length=50 ) showtime = postgres.ArrayField( # HStoreField stores showtime-region pair @@ -101,9 +109,10 @@ class Movie(Resource): ) site = models.URLField(_('site url'), max_length=200) - # country or area + # country or region area = postgres.ArrayField( models.CharField( + _("country or region"), blank=True, default='', max_length=100, @@ -127,13 +136,17 @@ class Movie(Resource): year = models.PositiveIntegerField(null=True, blank=True) duration = models.CharField(blank=True, default='', max_length=100) + cover = models.ImageField(_("poster"), upload_to=movie_cover_path, default=DEFAULT_MOVIE_IMAGE, blank=True) + ############################################ # exclusive fields to series ############################################ season = models.PositiveSmallIntegerField(null=True, blank=True) # how many episodes in the season episodes = models.PositiveIntegerField(null=True, blank=True) - tv_station = models.CharField(blank=True, default='', max_length=200) + # deprecated + # tv_station = models.CharField(blank=True, default='', max_length=200) + single_episode_length = models.CharField(blank=True, default='', max_length=100) ############################################ # category identifier @@ -146,7 +159,7 @@ class Movie(Resource): def get_tags_manager(self): - raise NotImplementedError + return self.movie_tags class MovieMark(Mark): diff --git a/movies/templates/movies/create_update.html b/movies/templates/movies/create_update.html new file mode 100644 index 00000000..1d34c053 --- /dev/null +++ b/movies/templates/movies/create_update.html @@ -0,0 +1,58 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'Nicedb - ' %}{{ title }} + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+ {% csrf_token %} + {{ form }} + +
+
+ +
+
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/movies/templates/movies/create_update_review.html b/movies/templates/movies/create_update_review.html new file mode 100644 index 00000000..e376ee0d --- /dev/null +++ b/movies/templates/movies/create_update_review.html @@ -0,0 +1,112 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'Nicedb - ' %}{{ title }} + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+ + + +
+
+ +
{{ movie.title }}
+
{% if movie.isbn %}{% trans 'ISBN:' %}{{ movie.isbn }}{% endif %}
+
{% if movie.author %}{% trans '作者:' %} + {% for author in movie.author %} + {{ author }} + {% endfor %} + {% endif %}
+
{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}
+
{%if movie.pub_year %}{% trans '出版时间:' %}{{ movie.pub_year }}{% trans '年' %}{% if movie.pub_month %}{{ movie.pub_month }}{% trans '月' %}{% endif %}{% endif %}
+ + {% if movie.rating %} + {% trans '评分:' %} + {{ movie.rating }} + {% endif %} +
+
+
+ +
+ {% csrf_token %} + {{ form.movie }} +
+ {{ form.title.label }} +
+ {{ form.title }} +
+ + {{ form.content.label }} + + + {% trans '预览' %} + +
+
+ {{ form.content }} +
+
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
+
+
+ + {{ form.is_private.label }}{{ form.is_private }} +
+ +
+
+ +
+ {{ form.media }} +
+ +
+ +
+
+
+ {% include "partial/_footer.html" %} +
+ + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/movies/templates/movies/delete.html b/movies/templates/movies/delete.html new file mode 100644 index 00000000..c655ae27 --- /dev/null +++ b/movies/templates/movies/delete.html @@ -0,0 +1,92 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'Nicedb - 删除电影/剧集' %} + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
{% trans '确认删除这部电影/剧集吗?相关评论和标记将一并删除。' %}
+ +
+
+ + + +
+
+ +
+ {{ movie.title }} +
+
+ {% if movie.rating %} + {% trans '评分:' %} + + {{ movie.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} + + {% if movie.last_editor %} + +
{% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}
+
+ {% endif %} + +
{% trans '上次编辑时间:' %}{{ movie.edited_time }}
+
+
+
+
+
+ {% csrf_token %} + +
+ +
+
+
+
+ +
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/movies/templates/movies/delete_review.html b/movies/templates/movies/delete_review.html new file mode 100644 index 00000000..87538b7d --- /dev/null +++ b/movies/templates/movies/delete_review.html @@ -0,0 +1,110 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'Nicedb - 删除评论' %} + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
{% trans '确认删除这篇评论吗?' %}
+ +
+ + +
+ +
+ {{ review.title }} +
+ {% if review.is_private %} + + + + {% endif %} + +
+
+ + {{ review.owner.username }} + + {% if mark %} + + {% if mark.rating %} + + {% endif %} + + {% endif %} + + {{ review.edited_time }} + +
+
+ + +
+
+ {{ form.content }} +
+ {{ form.media }} + +
+ +
+
+ {% csrf_token %} + +
+ +
+
+
+
+ +
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html new file mode 100644 index 00000000..8a011e70 --- /dev/null +++ b/movies/templates/movies/detail.html @@ -0,0 +1,385 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + + + + + {% if movie.author %} + + {% endif %} + {% if movie.isbn %} + + {% endif %} + + {% trans 'Nicedb - 电影/剧集详情' %} | {{ movie.title }} + + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+ + {{ movie.title }} + +
+
+ {{ movie.title }} +
+ +
+
+ {% if movie.rating %} + + {{ movie.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} +
+
{% if movie.isbn %}{% trans 'ISBN:' %}{{ movie.isbn }}{% endif %}
+
{% if movie.author %}{% trans '作者:' %} + {% for author in movie.author %} + {{ author }} + {% endfor %} + {% endif %}
+
{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}
+
{% if movie.subtitle %}{% trans '副标题:' %}{{ movie.subtitle }}{% endif %}
+
{% if movie.translator %}{% trans '译者:' %} + {% for translator in movie.translator %} + {{ translator }} + {% endfor %} + {% endif %}
+
{% if movie.orig_title %}{% trans '原作名:' %}{{ movie.orig_title }}{% endif %}
+
{% if movie.language %}{% trans '语言:' %}{{ movie.language }}{% endif %}
+
{%if movie.pub_year %}{% trans '出版时间:' %}{{ movie.pub_year }}{% trans '年' %}{% if movie.pub_month %}{{ movie.pub_month }}{% trans '月' %}{% endif %}{% endif %}
+
+
+ +
{% if movie.binding %}{% trans '装帧:' %}{{ movie.binding }}{% endif %}
+
{% if movie.price %}{% trans '定价:' %}{{ movie.price }}{% endif %}
+
{% if movie.pages %}{% trans '页数:' %}{{ movie.pages }}{% endif %}
+ {% if movie.other_info %} + {% for k, v in movie.other_info.items %} +
+ {{k}}:{{v}} +
+ {% endfor %} + {% endif %} + + + {% if movie.last_editor %} +
{% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}
+ {% endif %} + +
+ {% trans '编辑这部电影/剧集' %} + {% if user.is_staff %} + / {% trans '删除' %} + {% endif %} +
+
+ +
+ + {% for tag_dict in movie_tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} + +
+
+
+
+
+
{% trans '简介' %}
+ {% if movie.brief %} + +

{{ movie.brief | linebreaksbr }}

+ + + {% else %} +
{% trans '暂无简介' %}
+ {% endif %} + +
+ + {% if movie.contents %} +
+
{% trans '目录' %}
+

{{ movie.contents | linebreaksbr }}

+ +
+ {% endif %} + +
+
{% trans '这部电影/剧集的标记' %}
+ {% if mark_list_more %} + {% trans '更多' %} + {% endif %} + {% if mark_list %} +
    + {% for others_mark in mark_list %} +
  • + {{ others_mark.owner.username }} + {{ others_mark.get_status_display }} + {% if others_mark.rating %} + + {% endif %} + {% if others_mark.is_private %} + + {% endif %} + {{ others_mark.edited_time }} + {% if others_mark.text %} +

    {{ others_mark.text }}

    + {% endif %} +
  • + {% endfor %} +
+ {% else %} +
{% trans '暂无标记' %}
+ {% endif %} +
+
+
{% trans '这部电影/剧集的评论' %}
+ {% if review_list_more %} + {% trans '更多' %} + {% endif %} + {% if review_list %} + + {% else %} +
{% trans '暂无评论' %}
+ {% endif %} +
+
+
+ +
+
+ + {% if mark %} +
+ + {% trans '我' %}{{ mark.get_status_display }} + {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} + {% if mark.rating %} + + {% endif %} + {% endif %} + {% if mark.is_private %} + + {% endif %} + + {% trans '修改' %} +
+ {% csrf_token %} + {% trans '删除' %} +
+
+
+ +
{{ mark.edited_time }}
+ + {% if mark.text %} +

{{ mark.text }}

+ {% endif %} +
+ + {% for tag in mark_tags %} + {{ tag }} + {% endfor %} + +
+
+ {% else %} +
+
{% trans '标记这部电影/剧集' %}
+
+ + + +
+
+ {% endif %} + +
+ +
+ {% if review %} +
+ + {% trans '我的评论' %} + {% if review.is_private %} + + {% endif %} + + + {% trans '编辑' %} + {% trans '删除' %} + + +
{{ review.edited_time }}
+ + + {{ review.title }} + +
+ {% else %} + +
+
{% trans '我的评论' %}
+ +
+ + {% endif %} +
+ +
+
+
+ +
+ {% include "partial/_footer.html" %} +
+ +
+ + + +
+
+ + + + + + diff --git a/movies/templates/movies/list.html b/movies/templates/movies/list.html new file mode 100644 index 00000000..7885b444 --- /dev/null +++ b/movies/templates/movies/list.html @@ -0,0 +1,277 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'Nicedb - ' %}{{ user.username }}{{ list_title }} + + + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+ +
+
+ {{ user.username }}{{ list_title }} +
+
+
    + + {% for mark in marks %} + +
  • +
    + + + +
    +
    + + {% comment %} + + {% endcomment %} + + {% if mark.movie.pub_year %} + {{ mark.movie.pub_year }}{% trans '年' %} / + {% if mark.movie.pub_month %} + {{ mark.movie.pub_month }}{% trans '月' %} / + {% endif %} + {% endif %} + + {% if mark.movie.author %} + {% trans '作者' %} + {% for author in mark.movie.author %} + {{ author }}{% if not forloop.last %},{% endif %} + {% endfor %}/ + {% endif %} + + {% if mark.movie.translator %} + {% trans '译者' %} + {% for translator in mark.movie.translator %} + {{ translator }}{% if not forloop.last %},{% endif %} + {% endfor %}/ + {% endif %} + + {% if mark.movie.orig_title %} +  {% trans '原名' %} + {{ mark.movie.orig_title }} + {% endif %} + +

    + {{ mark.movie.brief | truncate:170 }} +

    +
    + {% for tag_dict in mark.movie.tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} +
    +
    +
    +
    +
      +
    • + + {% if mark.rating %} + + {% endif %} + {% if mark.is_private %} + + + + {% endif %} + {% trans '于' %} {{ mark.edited_time }} {% trans '标记' %} + {% if mark.text %} +

      {{ mark.text }}

      + {% endif %} +
    • +
    +
    +
    + +
  • + {% empty %} +
    {% trans '无结果' %}
    + {% endfor %} + + + +
+
+ +
+
+ +
+
+ +
+ +
+
+ + + + + +
+
+
+ +
+
+ {% trans '关注的人' %} +
+ {% trans '更多' %} + +
+ +
+
+ {% trans '被他们关注' %} +
+ {% trans '更多' %} + +
+ +
+
+
+ +
+
+
+
+ {% include "partial/_footer.html" %} +
+ + + + + + + + + + + + + diff --git a/movies/templates/movies/mark_list.html b/movies/templates/movies/mark_list.html new file mode 100644 index 00000000..94eae1af --- /dev/null +++ b/movies/templates/movies/mark_list.html @@ -0,0 +1,145 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'Nicedb - ' %}{{ movie.title }}{% trans '的标记' %} + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+
+ {{ movie.title }}{% trans ' 的标记' %} +
+
    + + {% for mark in marks %} + +
  • + {{ mark.owner.username }} + {{ mark.get_status_display }} + {% if mark.rating %} + + {% endif %} + {% if mark.is_private %} + + + + {% endif %} + {{ mark.edited_time }} + {% if mark.text %} +

    {{ mark.text }}

    + {% endif %} +
  • + + {% empty %} +
    + {% trans '无结果' %} +
    + {% endfor %} + +
+
+ +
+
+ +
+
+
+
+ +
+
+
{{ movie.title }}
+ + {% if movie.isbn %} +
ISBN: {{ movie.isbn }}
+ {% endif %} + +
{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}
+ {% if movie.rating %} + {% trans '评分: ' %} + {{ movie.rating }} + {% endif %} +
+ +
+
+
+
+
+
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/movies/templates/movies/review_detail.html b/movies/templates/movies/review_detail.html new file mode 100644 index 00000000..887548ba --- /dev/null +++ b/movies/templates/movies/review_detail.html @@ -0,0 +1,127 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + + + + + + {% trans 'Nicedb - 评论详情' %} + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+
+ {{ review.title }} +
+ {% if review.is_private %} + + + + {% endif %} +
+
+ + {{ review.owner.username }} + + {% if mark %} + + {% if mark.rating %} + + {% endif %} + + {% endif %} + + {{ review.edited_time }} + +
+
+ {% if request.user == review.owner %} + {% trans '编辑' %} + {% trans '删除' %} + {% endif %} +
+
+ +
+ {{ form.content }} +
+ {{ form.media }} +
+
+ +
+
+
+
+
+ +
+
+
{{ movie.title }}
+ + {% if movie.isbn %} +
ISBN: {{ movie.isbn }}
+ {% endif %} + +
{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}
+ {% if movie.rating %} + {% trans '评分: ' %} + {{ movie.rating }} + {% endif %} +
+ +
+
+
+
+
+
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/movies/templates/movies/review_list.html b/movies/templates/movies/review_list.html new file mode 100644 index 00000000..9038f953 --- /dev/null +++ b/movies/templates/movies/review_list.html @@ -0,0 +1,132 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'Nicedb - ' %}{{ movie.title }}{% trans '的评论' %} + + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+
+ {{ movie.title }}{% trans ' 的评论' %} +
+
    + + {% for review in reviews %} + +
  • + + {{ review.owner.username }} + {% if review.is_private %} + + {% endif %} + {{ review.edited_time }} + + + {{ review.title }} + +
  • + {% empty %} +
    {% trans '无结果' %}
    + {% endfor %} + +
+
+ +
+
+ +
+
+
+
+ +
+
+
{{ movie.title }}
+ + {% if movie.isbn %} +
ISBN: {{ movie.isbn }}
+ {% endif %} + +
{% if movie.pub_house %}{% trans '出版社:' %}{{ movie.pub_house }}{% endif %}
+ {% if movie.rating %} + {% trans '评分: ' %} + {{ movie.rating }} + {% endif %} +
+ +
+
+
+
+
+
+ {% include "partial/_footer.html" %} +
+ + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/movies/templates/movies/scrape.html b/movies/templates/movies/scrape.html new file mode 100644 index 00000000..68ae04fe --- /dev/null +++ b/movies/templates/movies/scrape.html @@ -0,0 +1,88 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'Nicedb - 从豆瓣获取数据' %} + + + + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+
+ {% trans '根据豆瓣内容填写下方表单' %} +
+ +
+
+
+ {% csrf_token %} + {{ form }} +
+ {% trans '剽取!' %} +
+
+
+
+ +
+
+
+ {% trans '复制详情页链接' %} +
+
+ {% csrf_token %} + + +
+
+ +
+
+
+ +
+ {% include "partial/_footer.html" %} + +
+ + + + + diff --git a/movies/urls.py b/movies/urls.py new file mode 100644 index 00000000..9f359e9e --- /dev/null +++ b/movies/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from .views import * + + +app_name = 'movies' +urlpatterns = [ + path('create/', create, name='create'), + path('/', retrieve, name='retrieve'), + path('update//', update, name='update'), + path('delete//', delete, name='delete'), + path('mark/', create_update_mark, name='create_update_mark'), + path('/mark/list/', retrieve_mark_list, name='retrieve_mark_list'), + path('mark/delete//', delete_mark, name='delete_mark'), + path('/review/create/', create_review, name='create_review'), + path('review/update//', update_review, name='update_review'), + path('review/delete//', delete_review, name='delete_review'), + path('review//', retrieve_review, name='retrieve_review'), + path('/review/list/', + retrieve_review_list, name='retrieve_review_list'), + path('scrape/', scrape, name='scrape'), + path('click_to_scrape/', click_to_scrape, name='click_to_scrape'), +] diff --git a/movies/views.py b/movies/views.py new file mode 100644 index 00000000..86469073 --- /dev/null +++ b/movies/views.py @@ -0,0 +1,579 @@ +import logging +from django.shortcuts import render, get_object_or_404, redirect, reverse +from django.contrib.auth.decorators import login_required +from django.utils.translation import gettext_lazy as _ +from django.http import HttpResponseBadRequest, HttpResponseServerError +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import IntegrityError, transaction +from django.db.models import Count +from django.utils import timezone +from django.core.paginator import Paginator +from django.core.files.uploadedfile import SimpleUploadedFile +from common.mastodon import mastodon_request_included +from common.mastodon.api import check_visibility, post_toot, TootVisibilityEnum +from common.mastodon.utils import rating_to_emoji +from common.utils import PageLinksGenerator +from common.views import PAGE_LINK_NUMBER +from .models import * +from .forms import * +from .forms import MovieMarkStatusTranslator +from boofilsic.settings import MASTODON_TAGS + + +logger = logging.getLogger(__name__) +mastodon_logger = logging.getLogger("django.mastodon") + + +# how many marks showed on the detail page +MARK_NUMBER = 5 +# how many marks at the mark page +MARK_PER_PAGE = 20 +# how many reviews showed on the detail page +REVIEW_NUMBER = 5 +# how many reviews at the mark page +REVIEW_PER_PAGE = 20 +# max tags on detail page +TAG_NUMBER = 10 + + +# public data +########################### +@login_required +def create(request): + if request.method == 'GET': + form = MovieForm() + return render( + request, + 'movies/create_update.html', + { + 'form': form, + 'title': _('添加书籍'), + 'submit_url': reverse("movies:create") + } + ) + elif request.method == 'POST': + if request.user.is_authenticated: + # only local user can alter public data + form = MovieForm(request.POST, request.FILES) + if form.is_valid(): + form.instance.last_editor = request.user + form.save() + return redirect(reverse("movies:retrieve", args=[form.instance.id])) + else: + return render( + request, + 'movies/create_update.html', + { + 'form': form, + 'title': _('添加书籍'), + 'submit_url': reverse("movies:create") + } + ) + else: + return redirect(reverse("users:login")) + else: + return HttpResponseBadRequest() + + +@login_required +def update(request, id): + if request.method == 'GET': + movie = get_object_or_404(Movie, pk=id) + form = MovieForm(instance=movie) + return render( + request, + 'movies/create_update.html', + { + 'form': form, + 'title': _('修改书籍'), + 'submit_url': reverse("movies:update", args=[movie.id]) + } + ) + elif request.method == 'POST': + movie = get_object_or_404(Movie, pk=id) + form = MovieForm(request.POST, request.FILES, instance=movie) + if form.is_valid(): + form.instance.last_editor = request.user + form.instance.edited_time = timezone.now() + form.save() + else: + return render( + request, + 'movies/create_update.html', + { + 'form': form, + 'title': _('修改书籍'), + 'submit_url': reverse("movies:update", args=[movie.id]) + } + ) + return redirect(reverse("movies:retrieve", args=[form.instance.id])) + + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +# @login_required +def retrieve(request, id): + if request.method == 'GET': + movie = get_object_or_404(Movie, pk=id) + mark = None + mark_tags = None + review = None + + # retreive tags + movie_tag_list = movie.movie_tags.values('content').annotate( + tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER] + + # retrieve user mark and initialize mark form + try: + if request.user.is_authenticated: + mark = MovieMark.objects.get(owner=request.user, movie=movie) + except ObjectDoesNotExist: + mark = None + if mark: + mark_tags = mark.mark_tags.all() + mark.get_status_display = MovieMarkStatusTranslator(mark.status) + mark_form = MovieMarkForm(instance=mark, initial={ + 'tags': mark_tags + }) + else: + mark_form = MovieMarkForm(initial={ + 'movie': movie, + 'tags': mark_tags + }) + + # retrieve user review + try: + if request.user.is_authenticated: + review = MovieReview.objects.get(owner=request.user, movie=movie) + except ObjectDoesNotExist: + review = None + + # retrieve other related reviews and marks + if request.user.is_anonymous: + # hide all marks and reviews for anonymous user + mark_list = None + review_list = None + mark_list_more = None + review_list_more = None + else: + mark_list = MovieMark.get_available( + movie, request.user, request.session['oauth_token']) + review_list = MovieReview.get_available( + movie, request.user, request.session['oauth_token']) + mark_list_more = True if len(mark_list) > MARK_NUMBER else False + mark_list = mark_list[:MARK_NUMBER] + for m in mark_list: + m.get_status_display = MovieMarkStatusTranslator(m.status) + review_list_more = True if len( + review_list) > REVIEW_NUMBER else False + review_list = review_list[:REVIEW_NUMBER] + + # def strip_html_tags(text): + # import re + # regex = re.compile('<.*?>') + # return re.sub(regex, '', text) + + # for r in review_list: + # r.content = strip_html_tags(r.content) + + return render( + request, + 'movies/detail.html', + { + 'movie': movie, + 'mark': mark, + 'review': review, + 'status_enum': MarkStatusEnum, + 'mark_form': mark_form, + 'mark_list': mark_list, + 'mark_list_more': mark_list_more, + 'review_list': review_list, + 'review_list_more': review_list_more, + 'movie_tag_list': movie_tag_list, + 'mark_tags': mark_tags, + } + ) + else: + return HttpResponseBadRequest() + logger.warning('non-GET method at /movie/') + + +@login_required +def delete(request, id): + if request.method == 'GET': + movie = get_object_or_404(Movie, pk=id) + return render( + request, + 'movies/delete.html', + { + 'movie': movie, + } + ) + elif request.method == 'POST': + if request.user.is_staff: + # only staff has right to delete + movie = get_object_or_404(Movie, pk=id) + movie.delete() + return redirect(reverse("common:search")) + else: + raise PermissionDenied() + else: + return HttpResponseBadRequest() + + +# user owned entites +########################### +@mastodon_request_included +@login_required +def create_update_mark(request): + # check list: + # clean rating if is wish + # transaction on updating movie rating + # owner check(guarantee) + if request.method == 'POST': + pk = request.POST.get('id') + old_rating = None + old_tags = None + if pk: + mark = get_object_or_404(MovieMark, pk=pk) + old_rating = mark.rating + old_tags = mark.mark_tags.all() + # update + form = MovieMarkForm(request.POST, instance=mark) + else: + # create + form = MovieMarkForm(request.POST) + + if form.is_valid(): + if form.instance.status == MarkStatusEnum.WISH.value: + form.instance.rating = None + form.cleaned_data['rating'] = None + form.instance.owner = request.user + form.instance.edited_time = timezone.now() + movie = form.instance.movie + + try: + with transaction.atomic(): + # update movie rating + movie.update_rating(old_rating, form.instance.rating) + form.save() + # update tags + if old_tags: + for tag in old_tags: + tag.delete() + if form.cleaned_data['tags']: + for tag in form.cleaned_data['tags']: + MovieTag.objects.create( + content=tag, + movie=movie, + mark=form.instance + ) + except IntegrityError as e: + return HttpResponseServerError("integrity error") + logger.error(e.__str__()) + + if form.cleaned_data['share_to_mastodon']: + if form.cleaned_data['is_private']: + visibility = TootVisibilityEnum.PRIVATE + else: + visibility = TootVisibilityEnum.UNLISTED + url = "https://" + request.get_host() + reverse("movies:retrieve", + args=[movie.id]) + words = MovieMarkStatusTranslator(int(form.cleaned_data['status'])) +\ + f"《{movie.title}》" + \ + rating_to_emoji(form.cleaned_data['rating']) + + # tags = MASTODON_TAGS % {'category': '书', 'type': '标记'} + tags = '' + content = words + '\n' + url + '\n' + \ + form.cleaned_data['text'] + '\n' + tags + response = post_toot(content, visibility, + request.session['oauth_token']) + if response.status_code != 200: + mastodon_logger.error( + f"CODE:{response.status_code} {response.text}") + return HttpResponseServerError("publishing mastodon status failed") + else: + return HttpResponseBadRequest("invalid form data") + + return redirect(reverse("movies:retrieve", args=[form.instance.movie.id])) + else: + return HttpResponseBadRequest("invalid method") + + +@mastodon_request_included +@login_required +def retrieve_mark_list(request, movie_id): + if request.method == 'GET': + movie = get_object_or_404(Movie, pk=movie_id) + queryset = MovieMark.get_available( + movie, request.user, request.session['oauth_token']) + paginator = Paginator(queryset, MARK_PER_PAGE) + page_number = request.GET.get('page', default=1) + marks = paginator.get_page(page_number) + marks.pagination = PageLinksGenerator( + PAGE_LINK_NUMBER, page_number, paginator.num_pages) + for m in marks: + m.get_status_display = MovieMarkStatusTranslator(m.status) + return render( + request, + 'movies/mark_list.html', + { + 'marks': marks, + 'movie': movie, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def delete_mark(request, id): + if request.method == 'POST': + mark = get_object_or_404(MovieMark, pk=id) + movie_id = mark.movie.id + try: + with transaction.atomic(): + # update movie rating + mark.movie.update_rating(mark.rating, None) + mark.delete() + except IntegrityError as e: + return HttpResponseServerError() + return redirect(reverse("movies:retrieve", args=[movie_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def create_review(request, movie_id): + if request.method == 'GET': + form = MovieReviewForm(initial={'movie': movie_id}) + movie = get_object_or_404(Movie, pk=movie_id) + return render( + request, + 'movies/create_update_review.html', + { + 'form': form, + 'title': _("添加评论"), + 'movie': movie, + 'submit_url': reverse("movies:create_review", args=[movie_id]), + } + ) + elif request.method == 'POST': + form = MovieReviewForm(request.POST) + if form.is_valid(): + form.instance.owner = request.user + form.save() + if form.cleaned_data['share_to_mastodon']: + if form.cleaned_data['is_private']: + visibility = TootVisibilityEnum.PRIVATE + else: + visibility = TootVisibilityEnum.UNLISTED + url = "https://" + request.get_host() + reverse("movies:retrieve_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.movie.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(content, visibility, + request.session['oauth_token']) + if response.status_code != 200: + mastodon_logger.error( + f"CODE:{response.status_code} {response.text}") + return HttpResponseServerError("publishing mastodon status failed") + return redirect(reverse("movies:retrieve_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def update_review(request, id): + # owner check + # edited time + if request.method == 'GET': + review = get_object_or_404(MovieReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = MovieReviewForm(instance=review) + movie = review.movie + return render( + request, + 'movies/create_update_review.html', + { + 'form': form, + 'title': _("编辑评论"), + 'movie': movie, + 'submit_url': reverse("movies:update_review", args=[review.id]), + } + ) + elif request.method == 'POST': + review = get_object_or_404(MovieReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = MovieReviewForm(request.POST, instance=review) + if form.is_valid(): + form.instance.edited_time = timezone.now() + form.save() + if form.cleaned_data['share_to_mastodon']: + if form.cleaned_data['is_private']: + visibility = TootVisibilityEnum.PRIVATE + else: + visibility = TootVisibilityEnum.UNLISTED + url = "https://" + request.get_host() + reverse("movies:retrieve_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.movie.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(content, visibility, + request.session['oauth_token']) + if response.status_code != 200: + mastodon_logger.error( + f"CODE:{response.status_code} {response.text}") + return HttpResponseServerError("publishing mastodon status failed") + return redirect(reverse("movies:retrieve_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@login_required +def delete_review(request, id): + if request.method == 'GET': + review = get_object_or_404(MovieReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + review_form = MovieReviewForm(instance=review) + return render( + request, + 'movies/delete_review.html', + { + 'form': review_form, + 'review': review, + } + ) + elif request.method == 'POST': + review = get_object_or_404(MovieReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + movie_id = review.movie.id + review.delete() + return redirect(reverse("movies:retrieve", args=[movie_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_review(request, id): + if request.method == 'GET': + review = get_object_or_404(MovieReview, pk=id) + if not check_visibility(review, request.session['oauth_token'], request.user): + msg = _("你没有访问这个页面的权限😥") + return render( + request, + 'common/error.html', + { + 'msg': msg, + } + ) + review_form = MovieReviewForm(instance=review) + movie = review.movie + try: + mark = MovieMark.objects.get(owner=review.owner, movie=movie) + mark.get_status_display = MovieMarkStatusTranslator(mark.status) + except ObjectDoesNotExist: + mark = None + return render( + request, + 'movies/review_detail.html', + { + 'form': review_form, + 'review': review, + 'movie': movie, + 'mark': mark, + } + ) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_review_list(request, movie_id): + if request.method == 'GET': + movie = get_object_or_404(Movie, pk=movie_id) + queryset = MovieReview.get_available( + movie, request.user, request.session['oauth_token']) + paginator = Paginator(queryset, REVIEW_PER_PAGE) + page_number = request.GET.get('page', default=1) + reviews = paginator.get_page(page_number) + reviews.pagination = PageLinksGenerator( + PAGE_LINK_NUMBER, page_number, paginator.num_pages) + return render( + request, + 'movies/review_list.html', + { + 'reviews': reviews, + 'movie': movie, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def scrape(request): + if request.method == 'GET': + keywords = request.GET.get('q') + form = MovieForm() + return render( + request, + 'movies/scrape.html', + { + 'q': keywords, + 'form': form, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def click_to_scrape(request): + if request.method == "POST": + url = request.POST.get("url") + if url: + from common.scraper import scrape_douban_movie + try: + scraped_movie, raw_cover = scrape_douban_movie(url) + except TimeoutError: + return render(request, 'common/error.html', {'msg': _("爬取数据失败😫,请重试")}) + except ValueError: + return render(request, 'common/error.html', {'msg': _("链接非法,爬取失败")}) + scraped_cover = { + 'cover': SimpleUploadedFile('temp.jpg', raw_cover)} + form = MovieForm(scraped_movie, scraped_cover) + if form.is_valid(): + form.instance.last_editor = request.user + form.save() + return redirect(reverse('movies:retrieve', args=[form.instance.id])) + else: + if 'isbn' in form.errors: + msg = _("ISBN与现有图书重复") + else: + msg = _("爬取数据失败😫") + return render(request, 'common/error.html', {'msg': msg}) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest()