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" %}
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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" %}
+
+
+
+
+
+
+
+
+
+
{% 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 %}
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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 '确认删除这部电影/剧集吗?相关评论和标记将一并删除。' %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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 }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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 }}
+
+
+
+
+ {% 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 %}
+
+ {% 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 '修改' %}
+
+
+
+
+
{{ mark.edited_time }}
+
+ {% if mark.text %}
+
{{ mark.text }}
+ {% endif %}
+
+
+ {% for tag in mark_tags %}
+ {{ tag }}
+ {% endfor %}
+
+
+
+ {% else %}
+
+
{% trans '标记这部电影/剧集' %}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+ {% if review %}
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+
+
+
+ {% if not mark %}
+
+
+ {% else %}
+
{% trans '我的标记' %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans '确定要删除你的标记吗?' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+
{% url 'users:home' 0 %}?is_mastodon_id=true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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" %}
+
+
+
+
+
+
+
+
+
+ {% 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 %}
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+ {% 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 %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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 }}
+
+
+
+
+
+
+ {{ form.content }}
+
+ {{ form.media }}
+
+
+
+
+
+
+
+
+

+
+
+
+
+ {% 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 %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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" %}
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+ {% 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 %}
+
{% oauth_token %}
+
{% mastodon %}
+
+
{{ user.mastodon_id }}
+ {% 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 '根据豆瓣内容填写下方表单' %}
+
+
+
+
+
+
+
+
+
+
+
+ {% trans '复制详情页链接' %}
+
+
+
+
+
+
+
+
+
+ {% 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()