diff --git a/boofilsic/urls.py b/boofilsic/urls.py index 43778b6f..ecc6d0a3 100644 --- a/boofilsic/urls.py +++ b/boofilsic/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ path('users/', include('users.urls')), path('books/', include('books.urls')), path('movies/', include('movies.urls')), + path('music/', include('music.urls')), path('announcement/', include('management.urls')), path('', include('common.urls')), diff --git a/books/forms.py b/books/forms.py index 88c58c17..da7ecee6 100644 --- a/books/forms.py +++ b/books/forms.py @@ -77,42 +77,16 @@ class BookForm(forms.ModelForm): return isbn -class BookMarkForm(forms.ModelForm): - IS_PRIVATE_CHOICES = [ - (True, _("仅关注者")), - (False, _("公开")), - ] - STATUS_CHOICES = [(v, BookMarkStatusTranslator(v)) for v in MarkStatusEnum.values] +class BookMarkForm(MarkForm): + + STATUS_CHOICES = [(v, BookMarkStatusTranslator(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 = BookMark @@ -132,18 +106,8 @@ class BookMarkForm(forms.ModelForm): } -class BookReviewForm(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 BookReviewForm(ReviewForm): + class Meta: model = BookReview fields = [ diff --git a/books/models.py b/books/models.py index 9eecb479..fcd387ad 100644 --- a/books/models.py +++ b/books/models.py @@ -47,7 +47,7 @@ class Book(Entity): # since data origin is not formatted and might be CNY USD or other currency, use char instead price = models.CharField(_("pricing"), blank=True, default='', max_length=50) pages = models.PositiveIntegerField(_("pages"), null=True, blank=True) - isbn = models.CharField(_("ISBN"), blank=True, null=True, max_length=20, db_index=True) + isbn = models.CharField(_("ISBN"), blank=True, null=False, max_length=20, db_index=True, default='') # to store previously scrapped data cover = models.ImageField(_("cover picture"), upload_to=book_cover_path, default=DEFAULT_BOOK_IMAGE, blank=True) contents = models.TextField(blank=True, default="") diff --git a/books/templates/books/delete.html b/books/templates/books/delete.html index 37d9520d..bde89c96 100644 --- a/books/templates/books/delete.html +++ b/books/templates/books/delete.html @@ -52,9 +52,12 @@ {% endif %} {% if book.last_editor %} - -
{% trans '最近编辑者:' %}{{ book.last_editor | default:"" }}
-
+
+ {% trans '最近编辑者:' %} + + {{ book.last_editor | default:"" }} + +
{% endif %}
{% trans '上次编辑时间:' %}{{ book.edited_time }}
diff --git a/books/templates/books/detail.html b/books/templates/books/detail.html index 0585a5cc..bb52705f 100644 --- a/books/templates/books/detail.html +++ b/books/templates/books/detail.html @@ -99,7 +99,7 @@
{% trans '编辑这本书' %} {% if user.is_staff %} - / {% trans '删除' %} + / {% trans '删除' %} {% endif %}
diff --git a/common/forms.py b/common/forms.py index 1ae0e2a6..e6a6bbaf 100644 --- a/common/forms.py +++ b/common/forms.py @@ -174,3 +174,55 @@ class HstoreField(forms.CharField): if len(pairs) == 1: pairs = (pairs,) return pairs + + +############################# +# Form +############################# + +class MarkForm(forms.ModelForm): + IS_PRIVATE_CHOICES = [ + (True, _("仅关注者")), + (False, _("公开")), + ] + + 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) + 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 ReviewForm(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 + ) diff --git a/common/models.py b/common/models.py index 3f0b742f..e20a761d 100644 --- a/common/models.py +++ b/common/models.py @@ -32,8 +32,8 @@ class Entity(models.Model): edited_time = models.DateTimeField(auto_now_add=True) last_editor = models.ForeignKey( User, on_delete=models.SET_NULL, related_name='%(class)s_last_editor', null=True, blank=False) - brief = models.TextField(blank=True, default="") - other_info = postgres.JSONField( + brief = models.TextField(_("简介"), blank=True, default="") + other_info = postgres.JSONField(_("其他信息"), blank=True, null=True, encoder=DjangoJSONEncoder, default=dict) # source_url should include shceme, which is normally https:// source_url = models.URLField(_("URL"), max_length=500, unique=True) @@ -114,7 +114,7 @@ class Entity(models.Model): """ raise NotImplementedError("Subclass should implement this method.") - def get_revies_manager(self): + def get_reviews_manager(self): """ Normally this won't be used. There is no ocassion where visitor can simply view all the reviews. diff --git a/common/scraper.py b/common/scraper.py index 02770ab2..5752aa2b 100644 --- a/common/scraper.py +++ b/common/scraper.py @@ -4,6 +4,7 @@ import random import logging from lxml import html import re +import dateparser from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG from django.utils.translation import ugettext_lazy as _ from common.models import SourceSiteEnum @@ -11,6 +12,8 @@ from movies.models import Movie, MovieGenreEnum from movies.forms import MovieForm from books.models import Book from books.forms import BookForm +from music.models import Album, Song +from music.forms import AlbumForm, SongForm RE_NUMBERS = re.compile(r"\d+\d*") @@ -18,7 +21,7 @@ RE_WHITESPACES = re.compile(r"\s+") DEFAULT_REQUEST_HEADERS = { - 'Host': 'book.douban.com', + 'Host': '', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:70.0) Gecko/20100101 Firefox/70.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', @@ -65,7 +68,7 @@ class AbstractScraper: # subclasses must specify those two variables # site means general sites, like amazon/douban etc - site = None + site_name = None # host means technically hostname host = None # corresponding data class @@ -78,18 +81,26 @@ class AbstractScraper: def __init_subclass__(cls, **kwargs): # this statement initialize the subclasses super().__init_subclass__(**kwargs) - assert cls.site is not None, "class variable `site` must be specified" - assert cls.host is not None, "class variable `host` must be specified" + assert cls.site_name is not None, "class variable `site_name` must be specified" + assert bool(cls.host), "class variable `host` must be specified" assert cls.data_class is not None, "class variable `data_class` must be specified" assert cls.form_class is not None, "class variable `form_class` must be specified" assert cls.regex is not None, "class variable `regex` must be specified" - assert isinstance(cls.host, str), "`host` must be type str" - assert cls.site in SourceSiteEnum, "`site` must be one of `SourceSiteEnum` value" - assert hasattr(cls, 'scrape') and callable(cls.scrape), "scaper must have method `.scrape()`" + assert isinstance(cls.host, str) or (isinstance(cls.host, list) and isinstance( + cls.host[0], str)), "`host` must be type str or list" + assert cls.site_name in SourceSiteEnum, "`site_name` must be one of `SourceSiteEnum` value" + assert hasattr(cls, 'scrape') and callable( + cls.scrape), "scaper must have method `.scrape()`" + # decorate the scrape method cls.scrape = classmethod(log_url(cls.scrape)) - scraper_registry[cls.host] = cls - + + # register scraper + if isinstance(cls.host, list): + for host in cls.host: + scraper_registry[host] = cls + else: + scraper_registry[cls.host] = cls def scrape(self, url): """ @@ -99,7 +110,6 @@ class AbstractScraper: """ raise NotImplementedError("Subclass should implement this method") - @classmethod def get_effective_url(cls, raw_url): url = cls.regex.findall(raw_url) @@ -113,14 +123,15 @@ class AbstractScraper: 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, } # if DEBUG: # proxies = None - r = requests.get(url, proxies=proxies, headers=headers, timeout=TIMEOUT) + r = requests.get(url, proxies=proxies, + headers=headers, timeout=TIMEOUT) # r = requests.get(url, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT) return html.fromstring(r.content.decode('utf-8')) @@ -132,7 +143,7 @@ class AbstractScraper: 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)) + (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT)) proxies = { 'http': proxy_url, 'https': proxy_url, @@ -159,7 +170,7 @@ class AbstractScraper: class DoubanBookScraper(AbstractScraper): - site = SourceSiteEnum.DOUBAN.value + site_name = SourceSiteEnum.DOUBAN.value host = "book.douban.com" data_class = Book form_class = BookForm @@ -234,7 +245,8 @@ class DoubanBookScraper(AbstractScraper): brief_elem = content.xpath( "//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()") - brief = '\n'.join(p.strip() for p in brief_elem) if brief_elem else None + brief = '\n'.join(p.strip() + for p in brief_elem) if brief_elem else None contents = None try: @@ -311,14 +323,14 @@ class DoubanBookScraper(AbstractScraper): 'brief': brief, 'contents': contents, 'other_info': other, - 'source_site': self.site, + 'source_site': self.site_name, 'source_url': self.get_effective_url(url), } return data, raw_img class DoubanMovieScraper(AbstractScraper): - site = SourceSiteEnum.DOUBAN.value + site_name = SourceSiteEnum.DOUBAN.value host = 'movie.douban.com' data_class = Movie form_class = MovieForm @@ -327,7 +339,7 @@ class DoubanMovieScraper(AbstractScraper): def scrape(self, url): headers = DEFAULT_REQUEST_HEADERS.copy() - headers['Host'] = 'movie.douban.com' + headers['Host'] = self.host content = self.download_page(url, headers) # parsing starts here @@ -483,8 +495,105 @@ class DoubanMovieScraper(AbstractScraper): 'single_episode_length': single_episode_length, 'brief': brief, 'is_series': is_series, - 'source_site': self.site, + 'source_site': self.site_name, + 'source_url': self.get_effective_url(url), + } + return data, raw_img + + +class DoubanAlbumScraper(AbstractScraper): + site_name = SourceSiteEnum.DOUBAN.value + host = 'music.douban.com' + data_class = Album + form_class = AlbumForm + + regex = re.compile(r"https://music.douban.com/subject/\d+/{0,1}") + + def scrape(self, url): + headers = DEFAULT_REQUEST_HEADERS.copy() + headers['Host'] = self.host + content = self.download_page(url, headers) + + # parsing starts here + try: + title = content.xpath("//h1/span/text()")[0].strip() + except IndexError: + raise ValueError("given url contains no album info") + if not title: + raise ValueError("given url contains no album info") + + + artists_elem = content.xpath("""//div[@id='info']/span/span[@class='pl']/a/text()""") + artist = None if not artists_elem else artists_elem + + genre_elem = content.xpath( + "//div[@id='info']//span[text()='流派:']/following::text()[1]") + genre = genre_elem[0].strip() if genre_elem else None + + date_elem = content.xpath( + "//div[@id='info']//span[text()='发行时间:']/following::text()[1]") + release_date = dateparser.parse(date_elem[0].strip(), settings={ + 'PREFER_DAY_OF_MONTH': 'first'}) if date_elem else None + + company_elem = content.xpath( + "//div[@id='info']//span[text()='出版者:']/following::text()[1]") + company = company_elem[0].strip() if company_elem else None + + track_list_elem = content.xpath( + "//div[@class='track-list']/div[@class='indent']/div/text()" + ) + if track_list_elem: + track_list = '\n'.join([track.strip() for track in track_list_elem]) + else: + track_list = None + + brief_elem = content.xpath("//span[@class='all hidden']") + if not brief_elem: + brief_elem = content.xpath("//span[@property='v:summary']") + brief = '\n'.join([e.strip() for e in brief_elem[0].xpath( + './text()')]) if brief_elem else None + + other_info = {} + other_elem = content.xpath( + "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]") + if other_elem: + other_info['又名'] = other_elem[0].strip() + other_elem = content.xpath( + "//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]") + if other_elem: + other_info['专辑类型'] = other_elem[0].strip() + other_elem = content.xpath( + "//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]") + if other_elem: + other_info['介质'] = other_elem[0].strip() + other_elem = content.xpath( + "//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]") + if other_elem: + other_info['ISRC'] = other_elem[0].strip() + other_elem = content.xpath( + "//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]") + if other_elem: + other_info['条形码'] = other_elem[0].strip() + other_elem = content.xpath( + "//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]") + if other_elem: + other_info['碟片数'] = other_elem[0].strip() + + img_url_elem = content.xpath("//div[@id='mainpic']//img/@src") + img_url = img_url_elem[0].strip() if img_url_elem else None + raw_img = self.download_image(img_url) + + data = { + 'title': title, + 'artist': artist, + 'genre': genre, + 'release_date': release_date, + 'duration': None, + 'company': company, + 'track_list': track_list, + 'brief': brief, + 'other_info': other_info, + 'source_site': self.site_name, 'source_url': self.get_effective_url(url), } return data, raw_img - diff --git a/common/static/css/boofilsic.css b/common/static/css/boofilsic.css index 77113497..0ceb6283 100644 --- a/common/static/css/boofilsic.css +++ b/common/static/css/boofilsic.css @@ -374,6 +374,9 @@ input[type='search'], input[type='tel'], input[type='text'], input[type='url'], +input[type='date'], +input[type='time'], +input[type='color'], textarea, select { -webkit-appearance: none; @@ -396,6 +399,9 @@ input[type='search']:focus, input[type='tel']:focus, input[type='text']:focus, input[type='url']:focus, +input[type='date']:focus, +input[type='time']:focus, +input[type='color']:focus, textarea:focus, select:focus { border-color: #00a1cc; @@ -409,6 +415,9 @@ input[type='search']::-webkit-input-placeholder, input[type='tel']::-webkit-input-placeholder, input[type='text']::-webkit-input-placeholder, input[type='url']::-webkit-input-placeholder, +input[type='date']::-webkit-input-placeholder, +input[type='time']::-webkit-input-placeholder, +input[type='color']::-webkit-input-placeholder, textarea::-webkit-input-placeholder, select::-webkit-input-placeholder { color: #ccc; @@ -421,6 +430,9 @@ input[type='search']:-ms-input-placeholder, input[type='tel']:-ms-input-placeholder, input[type='text']:-ms-input-placeholder, input[type='url']:-ms-input-placeholder, +input[type='date']:-ms-input-placeholder, +input[type='time']:-ms-input-placeholder, +input[type='color']:-ms-input-placeholder, textarea:-ms-input-placeholder, select:-ms-input-placeholder { color: #ccc; @@ -433,6 +445,9 @@ input[type='search']::-ms-input-placeholder, input[type='tel']::-ms-input-placeholder, input[type='text']::-ms-input-placeholder, input[type='url']::-ms-input-placeholder, +input[type='date']::-ms-input-placeholder, +input[type='time']::-ms-input-placeholder, +input[type='color']::-ms-input-placeholder, textarea::-ms-input-placeholder, select::-ms-input-placeholder { color: #ccc; @@ -445,6 +460,9 @@ input[type='search']::placeholder, input[type='tel']::placeholder, input[type='text']::placeholder, input[type='url']::placeholder, +input[type='date']::placeholder, +input[type='time']::placeholder, +input[type='color']::placeholder, textarea::placeholder, select::placeholder { color: #ccc; @@ -1267,7 +1285,9 @@ select::placeholder { } .entity-list .entity-list__entity-info--full-length { + display: block; max-width: 100%; + margin-bottom: 12px; } .entity-list .entity-list__entity-brief { @@ -1771,7 +1791,7 @@ select::placeholder { line-height: unset; height: unset; padding: 4px 15px; - margin: 0 5px; + margin: 5px; } .action-panel { diff --git a/common/static/css/boofilsic.min.css b/common/static/css/boofilsic.min.css index 8106a5e1..21fe4c81 100644 --- a/common/static/css/boofilsic.min.css +++ b/common/static/css/boofilsic.min.css @@ -1 +1 @@ -@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;-o-object-fit:contain;object-fit:contain}img.emoji{height:14px;-webkit-box-sizing:border-box;box-sizing:border-box;-o-object-fit:contain;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::-webkit-input-placeholder,input[type='number']::-webkit-input-placeholder,input[type='password']::-webkit-input-placeholder,input[type='search']::-webkit-input-placeholder,input[type='tel']::-webkit-input-placeholder,input[type='text']::-webkit-input-placeholder,input[type='url']::-webkit-input-placeholder,textarea::-webkit-input-placeholder,select::-webkit-input-placeholder{color:#ccc}input[type='email']:-ms-input-placeholder,input[type='number']:-ms-input-placeholder,input[type='password']:-ms-input-placeholder,input[type='search']:-ms-input-placeholder,input[type='tel']:-ms-input-placeholder,input[type='text']:-ms-input-placeholder,input[type='url']:-ms-input-placeholder,textarea:-ms-input-placeholder,select:-ms-input-placeholder{color:#ccc}input[type='email']::-ms-input-placeholder,input[type='number']::-ms-input-placeholder,input[type='password']::-ms-input-placeholder,input[type='search']::-ms-input-placeholder,input[type='tel']::-ms-input-placeholder,input[type='text']::-ms-input-placeholder,input[type='url']::-ms-input-placeholder,textarea::-ms-input-placeholder,select::-ms-input-placeholder{color:#ccc}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::-moz-selection{color:white;background-color:#00a1cc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative}.navbar .navbar__logo{-ms-flex-preferred-size:100px;flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;-webkit-transform:scale(0.7);transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.grid .grid__aside--tablet-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.grid--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__main--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__aside--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;-webkit-transform:translateX(-50%) scale(0.4);transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:spinner 1.2s linear infinite;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){-webkit-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg);-webkit-animation-delay:-1s;animation-delay:-1s}.spinner div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation-delay:-.8s;animation-delay:-.8s}.spinner div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg);-webkit-animation-delay:-.7s;animation-delay:-.7s}.spinner div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg);-webkit-animation-delay:-.6s;animation-delay:-.6s}.spinner div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation-delay:-.5s;animation-delay:-.5s}.spinner div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg);-webkit-animation-delay:-.4s;animation-delay:-.4s}.spinner div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg);-webkit-animation-delay:-.3s;animation-delay:-.3s}.spinner div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg);-webkit-animation-delay:-.2s;animation-delay:-.2s}.spinner div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg);-webkit-animation-delay:-.1s;animation-delay:-.1s}.spinner div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg);-webkit-animation-delay:0s;animation-delay:0s}@-webkit-keyframes spinner{0%{opacity:1}100%{opacity:0}}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;-webkit-filter:opacity(20%);filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border-color:#319840;color:#319840}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{-o-object-fit:contain;object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;-o-object-fit:contain;object-fit:contain;float:left;max-width:150px}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:200px}.entity-desc .entity-desc__unfold-button{display:-webkit-box;display:-ms-flexbox;display:flex;color:#00a1cc;background-color:transparent;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:5px}.entity-sort .entity-sort__entity-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;-ms-flex-preferred-size:20%;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}@media (max-width: 575.98px){.entity-list .entity-list__entity{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{-ms-flex-preferred-size:50%;flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:-webkit-box;display:-ms-flexbox;display:flex}}.aside-section-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.action-panel .action-panel__button-group--center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.user-relation .user-relation__related-user{-ms-flex-preferred-size:25%;flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{width:48px}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-card--horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{-ms-flex-preferred-size:100px;flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card--horizontal{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.action-panel .action-panel__button-group{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.relation-dropdown .relation-dropdown__button>.icon-arrow{-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button:hover+.relation-dropdown__body{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.entity-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset} +@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;-o-object-fit:contain;object-fit:contain}img.emoji{height:14px;-webkit-box-sizing:border-box;box-sizing:border-box;-o-object-fit:contain;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::-webkit-input-placeholder,input[type='number']::-webkit-input-placeholder,input[type='password']::-webkit-input-placeholder,input[type='search']::-webkit-input-placeholder,input[type='tel']::-webkit-input-placeholder,input[type='text']::-webkit-input-placeholder,input[type='url']::-webkit-input-placeholder,input[type='date']::-webkit-input-placeholder,input[type='time']::-webkit-input-placeholder,input[type='color']::-webkit-input-placeholder,textarea::-webkit-input-placeholder,select::-webkit-input-placeholder{color:#ccc}input[type='email']:-ms-input-placeholder,input[type='number']:-ms-input-placeholder,input[type='password']:-ms-input-placeholder,input[type='search']:-ms-input-placeholder,input[type='tel']:-ms-input-placeholder,input[type='text']:-ms-input-placeholder,input[type='url']:-ms-input-placeholder,input[type='date']:-ms-input-placeholder,input[type='time']:-ms-input-placeholder,input[type='color']:-ms-input-placeholder,textarea:-ms-input-placeholder,select:-ms-input-placeholder{color:#ccc}input[type='email']::-ms-input-placeholder,input[type='number']::-ms-input-placeholder,input[type='password']::-ms-input-placeholder,input[type='search']::-ms-input-placeholder,input[type='tel']::-ms-input-placeholder,input[type='text']::-ms-input-placeholder,input[type='url']::-ms-input-placeholder,input[type='date']::-ms-input-placeholder,input[type='time']::-ms-input-placeholder,input[type='color']::-ms-input-placeholder,textarea::-ms-input-placeholder,select::-ms-input-placeholder{color:#ccc}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::-moz-selection{color:white;background-color:#00a1cc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative}.navbar .navbar__logo{-ms-flex-preferred-size:100px;flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;-webkit-transform:scale(0.7);transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.grid .grid__aside--tablet-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.grid--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__main--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__aside--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;-webkit-transform:translateX(-50%) scale(0.4);transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:spinner 1.2s linear infinite;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){-webkit-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg);-webkit-animation-delay:-1s;animation-delay:-1s}.spinner div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation-delay:-.8s;animation-delay:-.8s}.spinner div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg);-webkit-animation-delay:-.7s;animation-delay:-.7s}.spinner div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg);-webkit-animation-delay:-.6s;animation-delay:-.6s}.spinner div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation-delay:-.5s;animation-delay:-.5s}.spinner div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg);-webkit-animation-delay:-.4s;animation-delay:-.4s}.spinner div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg);-webkit-animation-delay:-.3s;animation-delay:-.3s}.spinner div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg);-webkit-animation-delay:-.2s;animation-delay:-.2s}.spinner div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg);-webkit-animation-delay:-.1s;animation-delay:-.1s}.spinner div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg);-webkit-animation-delay:0s;animation-delay:0s}@-webkit-keyframes spinner{0%{opacity:1}100%{opacity:0}}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;-webkit-filter:opacity(20%);filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border-color:#319840;color:#319840}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{-o-object-fit:contain;object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{display:block;max-width:100%;margin-bottom:12px}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;-o-object-fit:contain;object-fit:contain;float:left;max-width:150px}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:200px}.entity-desc .entity-desc__unfold-button{display:-webkit-box;display:-ms-flexbox;display:flex;color:#00a1cc;background-color:transparent;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:5px}.entity-sort .entity-sort__entity-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;-ms-flex-preferred-size:20%;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}@media (max-width: 575.98px){.entity-list .entity-list__entity{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{-ms-flex-preferred-size:50%;flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:-webkit-box;display:-ms-flexbox;display:flex}}.aside-section-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.action-panel .action-panel__button-group--center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.user-relation .user-relation__related-user{-ms-flex-preferred-size:25%;flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{width:48px}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-card--horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{-ms-flex-preferred-size:100px;flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card--horizontal{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.action-panel .action-panel__button-group{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.relation-dropdown .relation-dropdown__button>.icon-arrow{-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button:hover+.relation-dropdown__body{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.entity-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset} diff --git a/common/static/js/key_value_input.js b/common/static/js/key_value_input.js index 29fc9dda..8b15b1b0 100644 --- a/common/static/js/key_value_input.js +++ b/common/static/js/key_value_input.js @@ -8,6 +8,8 @@ function keyValueInput(valueKeyWidget, hiddenInput) { if (placeholderValue == null) { placeholderValue = ''; } + // assign existing pairs to hidden input + setHiddenInput(valueKeyWidget); let newInputPair = $(''); valueKeyWidget.append(newInputPair.clone()); @@ -27,7 +29,7 @@ function keyValueInput(valueKeyWidget, hiddenInput) { $(this).next().remove(); $(this).remove(); } - }); + }); valueKeyWidget.on('input', ':nth-last-child(3)', function () { if (!$(this).val() && !$(this).prev().val() && valueKeyWidget.children("input").length > 2) { @@ -37,12 +39,16 @@ function keyValueInput(valueKeyWidget, hiddenInput) { }); valueKeyWidget.on('input', function () { - let keys = $(this).children(":nth-child(odd)").map(function () { + setHiddenInput(this); + }); + + function setHiddenInput(elem) { + let keys = $(elem).children(":nth-child(odd)").map(function () { if ($(this).val()) { return $(this).val(); } }).get(); - let values = $(this).children(":nth-child(even)").map(function () { + let values = $(elem).children(":nth-child(even)").map(function () { if ($(this).val()) { return $(this).val(); } @@ -55,7 +61,7 @@ function keyValueInput(valueKeyWidget, hiddenInput) { finalValue.push(JSON.stringify(json)) }); hiddenInput.val(finalValue.toString()); - } else if(keys.length - values.length == 1) { + } else if (keys.length - values.length == 1) { let finalValue = []; keys.forEach(function (key, i) { let json = new Object; @@ -66,8 +72,10 @@ function keyValueInput(valueKeyWidget, hiddenInput) { } finalValue.push(JSON.stringify(json)) }); - hiddenInput.val(finalValue.toString()); + hiddenInput.val(finalValue.toString()); } - }); + } + + } diff --git a/common/static/sass/_AsideSection.sass b/common/static/sass/_AsideSection.sass index a32728f0..7da9ff0f 100644 --- a/common/static/sass/_AsideSection.sass +++ b/common/static/sass/_AsideSection.sass @@ -30,7 +30,7 @@ $aside-section-padding-mobile: 24px 25px 10px 25px line-height: unset; height: unset; padding: 4px 15px; - margin: 0 5px; + margin: 5px; .action-panel margin-bottom: 20px diff --git a/common/static/sass/_Global.sass b/common/static/sass/_Global.sass index be8a7597..4476f845 100644 --- a/common/static/sass/_Global.sass +++ b/common/static/sass/_Global.sass @@ -68,6 +68,9 @@ input[type='search'], input[type='tel'], input[type='text'], input[type='url'], +input[type='date'], +input[type='time'], +input[type='color'], textarea, select appearance: none // Removes awkward default styles on some inputs for iOS diff --git a/common/static/sass/_MainSection.sass b/common/static/sass/_MainSection.sass index 4675f738..83f642db 100644 --- a/common/static/sass/_MainSection.sass +++ b/common/static/sass/_MainSection.sass @@ -66,7 +66,9 @@ $sub-section-title-margin: 8px position: relative top: 0.52em &--full-length + display: block max-width: 100% + margin-bottom: 12px & &__entity-brief margin-top: 8px diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html index dd090c33..f362cbd4 100644 --- a/common/templates/common/search_result.html +++ b/common/templates/common/search_result.html @@ -175,7 +175,7 @@
{% trans '暂无评分' %}
{% endif %} - + {% if movie.director %}{% trans '导演' %} @@ -196,7 +196,7 @@ {% for actor in movie.actor %} 5 %}style="display: none;" {% endif %}>{{ actor }} {% if forloop.counter <= 5 %} - {% if not forloop.counter == 5 %} / {% endif %} + {% if not forloop.counter == 5 and not forloop.last %} / {% endif %} {% endif %} {% endfor %} {% endif %} @@ -219,6 +219,90 @@ {% endwith %} + + {% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %} + + {% with music=item %} +
  • +
    + + {% comment %} + + + + + {% endcomment %} + +
    +
    +
    + + {% comment %} + + {% if item.category_name == 'album' %} + + {{ music.title | highlight:request.GET.q }} + + {% elif item.category_name = 'song' %} + + {{ music.title | highlight:request.GET.q }} + + {% endif %} + {% endcomment %} + + + + {% if not request.GET.c or request.GET.c != 'music' and request.GET.c != 'book' and request.GET.c != 'music' %} + [{{item.verbose_category_name}}] + {% endif %} + + {{ music.get_source_site_display }} + +
    + + {% if music.rating %} +
    + {{ music.rating }} + {% else %} +
    {% trans '暂无评分' %}
    + {% endif %} + + + {% if music.genre %}{% trans '流派' %} + {{ music.genre }} / + {% endif %} + + {% if music.release_date %} {% trans '发行日期' %} + {{ music.release_date }} + {% endif %} + + + {% if music.artist %}{% trans '艺术家' %} + {% for artist in music.artist %} + 5 %}style="display: none;" {% endif %}>{{ artist }} + {% if forloop.counter <= 5 %} {% if not forloop.counter == 5 and not forloop.last %} / {% endif %} {% endif %} + {% endfor %} + {% endif %} + + +

    + {{ music.brief | truncate:170 }} +

    +
    + {% for tag_dict in music.tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} +
    +
    + +
  • + {% endwith %} {% endif %} @@ -278,6 +362,12 @@ + + {% elif request.GET.c|lower == 'music' %} + + + + {% else %} @@ -286,6 +376,12 @@ + + + + + + {% endif %} @@ -296,6 +392,12 @@ + + + + + + {% endif %}
    @@ -319,6 +421,15 @@ + {% elif request.GET.c|lower == 'music' %} + +
    + {% trans '或者(≖ ◡ ≖)✧' %} +
    + + + + {% else %} diff --git a/common/templates/partial/_navbar.html b/common/templates/partial/_navbar.html index a129a7ab..2ec9eb7d 100644 --- a/common/templates/partial/_navbar.html +++ b/common/templates/partial/_navbar.html @@ -17,6 +17,7 @@ +
    diff --git a/common/views.py b/common/views.py index 08b2245a..1ab37c6a 100644 --- a/common/views.py +++ b/common/views.py @@ -13,6 +13,7 @@ from django.db.models import Q, Count from django.http import HttpResponseBadRequest from books.models import Book from movies.models import Movie +from music.models import Album, Song from users.models import Report, User from mastodon.decorators import mastodon_request_included from common.models import MarkStatusEnum @@ -122,22 +123,25 @@ def search(request): except ValidationError as e: pass - # category, book/movie/record etc + # category, book/movie/music etc category = request.GET.get("c", default='').strip().lower() def book_param_handler(): q = Q() query_args = [] + # keywords keywords = request.GET.get("q", default='').strip() + # tag + tag = request.GET.get("tag", default='') + + if not (keywords or tag): + return [] for keyword in [keywords]: q = q | Q(title__icontains=keyword) q = q | Q(subtitle__icontains=keyword) q = q | Q(orig_title__icontains=keyword) - - # tag - tag = request.GET.get("tag", default='') if tag: q = q & Q(book_tags__content__iexact=tag) @@ -158,6 +162,8 @@ def search(request): elif tag: # search by single tag book.similarity = 0 if book.rating_number is None else book.rating_number + else: + book.similarity = 0 return book.similarity if len(queryset) > 0: ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True) @@ -168,16 +174,19 @@ def search(request): def movie_param_handler(): q = Q() query_args = [] + # keywords keywords = request.GET.get("q", default='').strip() + # tag + tag = request.GET.get("tag", default='') + + if not (keywords or tag): + return [] for keyword in [keywords]: q = q | Q(title__icontains=keyword) q = q | Q(other_title__icontains=keyword) q = q | Q(orig_title__icontains=keyword) - - # tag - tag = request.GET.get("tag", default='') if tag: q = q & Q(movie_tags__content__iexact=tag) @@ -197,6 +206,8 @@ def search(request): elif tag: # search by single tag movie.similarity = 0 if movie.rating_number is None else movie.rating_number + else: + movie.similarity = 0 return movie.similarity if len(queryset) > 0: ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True) @@ -204,11 +215,53 @@ def search(request): ordered_queryset = list(queryset) return ordered_queryset + def music_param_handler(): + q = Q() + query_args = [] + + # keywords + keywords = request.GET.get("q", default='').strip() + # tag + tag = request.GET.get("tag", default='') + + if not (keywords or tag): + return [] + + # search albums + for keyword in [keywords]: + q = q | Q(title__icontains=keyword) + if tag: + q = q & Q(album_tags__content__iexact=tag) + + query_args.append(q) + queryset = Album.objects.filter(*query_args).distinct() + + def calculate_similarity(music): + if keywords: + # search by name + similarity, n = 0, 0 + for keyword in keywords: + similarity += SequenceMatcher(None, keyword, music.title).quick_ratio() + n += 1 + music.similarity = similarity / n + elif tag: + # search by single tag + music.similarity = 0 if music.rating_number is None else music.rating_number + else: + music.similarity = 0 + return music.similarity + if len(queryset) > 0: + ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True) + else: + ordered_queryset = list(queryset) + return ordered_queryset + def all_param_handler(): book_queryset = book_param_handler() movie_queryset = movie_param_handler() + music_queryset = music_param_handler() ordered_queryset = sorted( - book_queryset + movie_queryset, + book_queryset + movie_queryset + music_queryset, key=operator.attrgetter('similarity'), reverse=True ) @@ -217,6 +270,7 @@ def search(request): param_handler = { 'book': book_param_handler, 'movie': movie_param_handler, + 'music': music_param_handler, 'all': all_param_handler, '': all_param_handler } diff --git a/movies/forms.py b/movies/forms.py index caf59955..aa320bd5 100644 --- a/movies/forms.py +++ b/movies/forms.py @@ -16,11 +16,6 @@ def MovieMarkStatusTranslator(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()) genre = forms.MultipleChoiceField( required=False, @@ -103,52 +98,18 @@ class MovieForm(forms.ModelForm): 'is_series': forms.CheckboxInput(attrs={'style': 'width: auto; position: relative; top: 2px'}) } - # def clean_isbn(self): - # isbn = self.cleaned_data.get('isbn') - # if isbn: - # isbn = isbn.strip() - # return isbn +class MovieMarkForm(MarkForm): -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 @@ -168,19 +129,7 @@ class MovieMarkForm(forms.ModelForm): } -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 MovieReviewForm(ReviewForm): class Meta: model = MovieReview diff --git a/movies/models.py b/movies/models.py index 563eea0d..abb97aac 100644 --- a/movies/models.py +++ b/movies/models.py @@ -54,6 +54,7 @@ class MovieGenreEnum(models.TextChoices): REALITY_TV = 'Reality-TV', _('真人秀') FAMILY = 'Family', _('家庭') TALK_SHOW = 'Talk-Show', _('脱口秀') + OTHER = 'Other', _('其他') MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum) @@ -76,7 +77,7 @@ class Movie(Entity): default=list, ) imdb_code = models.CharField( - blank=True, max_length=10, null=True, db_index=True) + blank=True, max_length=10, null=False, db_index=True, default='') director = postgres.ArrayField( models.CharField(_("director"), blank=True, default='', max_length=100), diff --git a/movies/templates/movies/delete.html b/movies/templates/movies/delete.html index 1b8a54ad..9fec90df 100644 --- a/movies/templates/movies/delete.html +++ b/movies/templates/movies/delete.html @@ -59,9 +59,12 @@ {% endif %} {% if movie.last_editor %} - -
    {% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}
    -
    +
    + {% trans '最近编辑者:' %} + + {{ movie.last_editor | default:"" }} + +
    {% endif %}
    {% trans '上次编辑时间:' %}{{ movie.edited_time }}
    diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html index cce63619..f4e7a400 100644 --- a/movies/templates/movies/detail.html +++ b/movies/templates/movies/detail.html @@ -204,7 +204,7 @@ {% trans '编辑这部电影' %} {% endif %} {% if user.is_staff %} - / {% trans '删除' %} + / {% trans '删除' %} {% endif %} @@ -240,15 +240,6 @@ - {% if movie.contents %} -
    -
    {% trans '目录' %}
    -

    {{ movie.contents | linebreaksbr }}

    - -
    - {% endif %}
    diff --git a/movies/views.py b/movies/views.py index a54bfe3e..dc2ef6e2 100644 --- a/movies/views.py +++ b/movies/views.py @@ -17,7 +17,6 @@ from common.views import PAGE_LINK_NUMBER, jump_or_scrape from common.models import SourceSiteEnum from .models import * from .forms import * -from .forms import MovieMarkStatusTranslator from boofilsic.settings import MASTODON_TAGS @@ -225,7 +224,7 @@ def retrieve(request, id): } ) else: - logger.warning('non-GET method at /movie/') + logger.warning('non-GET method at /movies/') return HttpResponseBadRequest() diff --git a/music/__init__.py b/music/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/music/admin.py b/music/admin.py new file mode 100644 index 00000000..eb770458 --- /dev/null +++ b/music/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from .models import * + +admin.site.register(Song) +admin.site.register(SongMark) +admin.site.register(SongReview) +admin.site.register(SongTag) +admin.site.register(Album) +admin.site.register(AlbumMark) +admin.site.register(AlbumReview) +admin.site.register(AlbumTag) diff --git a/music/apps.py b/music/apps.py new file mode 100644 index 00000000..d909c7fb --- /dev/null +++ b/music/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MusicConfig(AppConfig): + name = 'music' diff --git a/music/forms.py b/music/forms.py new file mode 100644 index 00000000..24d423b7 --- /dev/null +++ b/music/forms.py @@ -0,0 +1,178 @@ +from django import forms +from django.contrib.postgres.forms import SimpleArrayField +from django.utils.translation import gettext_lazy as _ +from .models import * +from common.models import MarkStatusEnum +from common.forms import * + + +def MusicMarkStatusTranslator(status): + trans_dict = { + MarkStatusEnum.DO.value: _("在听"), + MarkStatusEnum.WISH.value: _("想听"), + MarkStatusEnum.COLLECT.value: _("听过") + } + return trans_dict[status] + + +class SongForm(forms.ModelForm): + + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + other_info = JSONField(required=False, label=_("其他信息")) + + class Meta: + model = Song + # fields = '__all__' + fields = [ + 'id', + 'title', + 'source_site', + 'source_url', + 'artist', + 'release_date', + 'duration', + 'isrc', + 'genre', + 'cover', + 'album', + 'brief', + 'other_info', + ] + widgets = { + 'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}), + 'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}), + 'cover': PreviewImageInput(), + } + + +class SongMarkForm(MarkForm): + + STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v)) + for v in MarkStatusEnum.values] + + status = forms.ChoiceField( + label=_(""), + widget=forms.RadioSelect(), + choices=STATUS_CHOICES + ) + + class Meta: + model = SongMark + fields = [ + 'id', + 'song', + 'status', + 'rating', + 'text', + 'is_private', + ] + labels = { + 'rating': _("评分"), + } + widgets = { + 'song': forms.TextInput(attrs={"hidden": ""}), + } + + +class SongReviewForm(ReviewForm): + + class Meta: + model = SongReview + fields = [ + 'id', + 'song', + 'title', + 'content', + 'is_private' + ] + labels = { + 'song': "", + 'title': _("标题"), + 'content': _("正文"), + 'share_to_mastodon': _("分享到长毛象") + } + widgets = { + 'song': forms.TextInput(attrs={"hidden": ""}), + } + + +class AlbumForm(forms.ModelForm): + + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + other_info = JSONField(required=False, label=_("其他信息")) + + class Meta: + model = Album + # fields = '__all__' + fields = [ + 'id', + 'title', + 'source_site', + 'source_url', + 'artist', + 'company', + 'release_date', + 'duration', + 'genre', + 'cover', + 'brief', + 'track_list', + 'other_info', + ] + widgets = { + 'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}), + 'company': forms.TextInput(attrs={'placeholder': _("多个发行方使用英文逗号分隔")}), + 'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}), + 'cover': PreviewImageInput(), + } + + +class AlbumMarkForm(MarkForm): + + STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v)) + for v in MarkStatusEnum.values] + + status = forms.ChoiceField( + label=_(""), + widget=forms.RadioSelect(), + choices=STATUS_CHOICES + ) + + class Meta: + model = AlbumMark + fields = [ + 'id', + 'album', + 'status', + 'rating', + 'text', + 'is_private', + ] + labels = { + 'rating': _("评分"), + } + widgets = { + 'album': forms.TextInput(attrs={"hidden": ""}), + } + + +class AlbumReviewForm(ReviewForm): + + class Meta: + model = AlbumReview + fields = [ + 'id', + 'album', + 'title', + 'content', + 'is_private' + ] + labels = { + 'album': "", + 'title': _("标题"), + 'content': _("正文"), + 'share_to_mastodon': _("分享到长毛象") + } + widgets = { + 'album': forms.TextInput(attrs={"hidden": ""}), + } diff --git a/music/models.py b/music/models.py new file mode 100644 index 00000000..8e489c7c --- /dev/null +++ b/music/models.py @@ -0,0 +1,181 @@ +import uuid +import django.contrib.postgres.fields as postgres +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from django.core.serializers.json import DjangoJSONEncoder +from django.shortcuts import reverse +from common.models import Entity, Mark, Review, Tag +from common.utils import ChoicesDictGenerator +from boofilsic.settings import SONG_MEDIA_PATH_ROOT, DEFAULT_SONG_IMAGE, ALBUM_MEDIA_PATH_ROOT, DEFAULT_ALBUM_IMAGE +from django.utils import timezone + + +def song_cover_path(instance, filename): + ext = filename.split('.')[-1] + filename = "%s.%s" % (uuid.uuid4(), ext) + root = '' + if SONG_MEDIA_PATH_ROOT.endswith('/'): + root = SONG_MEDIA_PATH_ROOT + else: + root = SONG_MEDIA_PATH_ROOT + '/' + return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}' + + +def album_cover_path(instance, filename): + ext = filename.split('.')[-1] + filename = "%s.%s" % (uuid.uuid4(), ext) + root = '' + if ALBUM_MEDIA_PATH_ROOT.endswith('/'): + root = ALBUM_MEDIA_PATH_ROOT + else: + root = ALBUM_MEDIA_PATH_ROOT + '/' + return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}' + + +class Album(Entity): + title = models.CharField(_("标题"), max_length=500) + release_date = models.DateField( + _('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True) + cover = models.ImageField( + _("封面"), upload_to=album_cover_path, default=DEFAULT_ALBUM_IMAGE, blank=True) + duration = models.PositiveIntegerField(_("时长"), null=True, blank=True) + artist = postgres.ArrayField( + models.CharField(_("artist"), blank=True, + default='', max_length=100), + null=True, + blank=True, + default=list, + verbose_name=_("艺术家") + ) + genre = models.CharField(_("流派"), blank=True, + default='', max_length=100) + company = postgres.ArrayField( + models.CharField(blank=True, + default='', max_length=100), + null=True, + blank=True, + default=list, + verbose_name=_("发行方") + ) + track_list = models.TextField(_("曲目"), blank=True, default="") + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("music:retrieve_album", args=[self.id]) + + def get_tags_manager(self): + return self.album_tags + + @property + def verbose_category_name(self): + return _("专辑") + + +class Song(Entity): + ''' + Song(track) entity, can point to entity Album + ''' + title = models.CharField(_("标题"), max_length=500) + release_date = models.DateField(_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True) + isrc = models.CharField(_("ISRC"), + blank=True, max_length=15, db_index=True, default='') + # duration in ms + duration = models.PositiveIntegerField(_("时长"), null=True, blank=True) + cover = models.ImageField( + _("封面"), upload_to=song_cover_path, default=DEFAULT_SONG_IMAGE, blank=True) + artist = postgres.ArrayField( + models.CharField(blank=True, + default='', max_length=100), + null=True, + blank=True, + default=list, + verbose_name=_("艺术家") + ) + genre = models.CharField(_("流派"), blank=True, default='', max_length=100) + + album = models.ForeignKey( + Album, models.CASCADE, "album_songs", null=True, blank=True, verbose_name=_("所属专辑")) + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("music:retrieve_song", args=[self.id]) + + def get_tags_manager(self): + return self.song_tags + + @property + def verbose_category_name(self): + return _("单曲") + +class SongMark(Mark): + song = models.ForeignKey( + Song, on_delete=models.CASCADE, related_name='song_marks', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['owner', 'song'], name='unique_song_mark') + ] + + +class SongReview(Review): + song = models.ForeignKey( + Song, on_delete=models.CASCADE, related_name='song_reviews', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['owner', 'song'], name='unique_song_review') + ] + + +class SongTag(Tag): + song = models.ForeignKey( + Song, on_delete=models.CASCADE, related_name='song_tags', null=True) + mark = models.ForeignKey( + SongMark, on_delete=models.CASCADE, related_name='songmark_tags', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['content', 'mark'], name="unique_songmark_tag") + ] + + +class AlbumMark(Mark): + album = models.ForeignKey( + Album, on_delete=models.CASCADE, related_name='album_marks', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['owner', 'album'], name='unique_album_mark') + ] + + +class AlbumReview(Review): + album = models.ForeignKey( + Album, on_delete=models.CASCADE, related_name='album_reviews', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['owner', 'album'], name='unique_album_review') + ] + + +class AlbumTag(Tag): + album = models.ForeignKey( + Album, on_delete=models.CASCADE, related_name='album_tags', null=True) + mark = models.ForeignKey( + AlbumMark, on_delete=models.CASCADE, related_name='albummark_tags', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['content', 'mark'], name="unique_albummark_tag") + ] diff --git a/music/templates/music/album_detail.html b/music/templates/music/album_detail.html new file mode 100644 index 00000000..50b287cf --- /dev/null +++ b/music/templates/music/album_detail.html @@ -0,0 +1,435 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} + + + + + + + + + + + + + + {% trans 'NiceDB - 音乐详情' %} | {{ album.title }} + + + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + + {{ album.title }} + +
    +
    + + {{ album.title }} + {{ album.get_source_site_display }} +
    + +
    +
    + {% if album.rating %} + + {{ album.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} +
    +
    {% if album.artist %}{% trans '艺术家:' %} + {% for artist in album.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if album.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %}
    +
    {% if album.company %}{% trans '发行方:' %} + {% for company in album.company %} + 5 %}style="display: none;" {% endif %}> + {{ company }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if album.company|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %}
    +
    {% if album.release_date %} + {% trans '发行日期:' %}{{ album.release_date }} + {% endif %} +
    +
    {% if album.duration %} + {% trans '时长:' %}{{ album.get_duration_display }} + {% endif %} +
    +
    {% if album.genre %} + {% trans '流派:' %}{{ album.genre }} + {% endif %} +
    + + +
    +
    + {% if album.other_info %} + {% for k, v in album.other_info.items %} +
    + {{k}}:{{v}} +
    + {% endfor %} + {% endif %} + + + {% if album.last_editor %} +
    {% trans '最近编辑者:' %}{{ album.last_editor | default:"" }}
    + {% endif %} + +
    + {% trans '编辑这个作品' %} + {% if user.is_staff %} + / {% trans '删除' %} + {% endif %} +
    +
    + +
    + + {% for tag_dict in album_tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} + +
    +
    +
    +
    +
    +
    {% trans '简介' %}
    + {% if album.brief %} + +

    {{ album.brief | linebreaksbr }}

    + + + {% else %} +
    {% trans '暂无简介' %}
    + {% endif %} +
    + + {% if album.track_list %} +
    +
    {% trans '曲目' %}
    +

    {{ album.track_list | linebreaksbr }}

    + +
    + {% endif %} + + {% if album.album_songs.count %} +
    +
    {% trans '关联单曲' %}
    + + {% for song in album.album_songs.all %} + + {% endfor %} + +
    + {% 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/music/templates/music/album_mark_list.html b/music/templates/music/album_mark_list.html new file mode 100644 index 00000000..8832f71b --- /dev/null +++ b/music/templates/music/album_mark_list.html @@ -0,0 +1,170 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'NiceDB - ' %}{{ album.title }}{% trans '的标记' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ album.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 %} + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + {{ album.title }} + + + {{ album.get_source_site_display }} +
    + +
    {% if album.artist %}{% trans '艺术家:' %} + {% for artist in album.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if album.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
    + +
    {% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
    + {% if album.rating %} + {% trans '评分: ' %} + {{ album.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/music/templates/music/album_review_detail.html b/music/templates/music/album_review_detail.html new file mode 100644 index 00000000..7ba9da40 --- /dev/null +++ b/music/templates/music/album_review_detail.html @@ -0,0 +1,157 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% 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 }} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + {{ album.title }} + + + + {{ album.get_source_site_display }} + + +
    + +
    {% if album.artist %}{% trans '艺术家:' %} + {% for artist in album.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if album.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
    + +
    {% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
    + {% if album.rating %} + {% trans '评分: ' %} + {{ album.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/music/templates/music/album_review_list.html b/music/templates/music/album_review_list.html new file mode 100644 index 00000000..23edaa48 --- /dev/null +++ b/music/templates/music/album_review_list.html @@ -0,0 +1,167 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'NiceDB - ' %}{{ album.title }}{% trans '的评论' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ album.title }}{% trans '的评论' %} +
    +
      + + {% for review in reviews %} + +
    • + + {{ review.owner.username }} + {% if review.is_private %} + + + + {% endif %} + {{ review.edited_time }} + + + {{ review.title + }} + +
    • + {% empty %} +
      {% trans '无结果' %}
      + {% endfor %} + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + {{ album.title }} + + + {{ album.get_source_site_display }} +
    + +
    {% if album.artist %}{% trans '艺术家:' %} + {% for artist in album.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if album.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
    + +
    {% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
    + {% if album.rating %} + {% trans '评分: ' %} + {{ album.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/music/templates/music/create_update_album.html b/music/templates/music/create_update_album.html new file mode 100644 index 00000000..f2a12288 --- /dev/null +++ b/music/templates/music/create_update_album.html @@ -0,0 +1,91 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - ' %}{{ title }} + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    + {% trans '>>> 试试一键剽取~ <<<' %} +
    + {% csrf_token %} + {{ form.media }} + {% for field in form %} + {% if field.name == 'release_date' %} + {{ field.label_tag }} + + {% else %} + {% if field.name != 'id' %} + {{ field.label_tag }} + {% endif %} + {{ field }} + {% endif %} + + {% endfor %} + + +
    +
    + +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/music/templates/music/create_update_album_review.html b/music/templates/music/create_update_album_review.html new file mode 100644 index 00000000..c12d6276 --- /dev/null +++ b/music/templates/music/create_update_album_review.html @@ -0,0 +1,131 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - ' %}{{ title }} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + {{ album.title }} + + {{ album.get_source_site_display }} +
    +
    {% if album.artist %}{% trans '艺术家:' %} + {% for artist in album.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if album.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
    + +
    {% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
    + + {% if album.rating %} + {% trans '评分:' %} + {{ album.rating }} + {% endif %} +
    +
    +
    + +
    + {% csrf_token %} + {{ form.album }} +
    + {{ 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/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html new file mode 100644 index 00000000..ed68d489 --- /dev/null +++ b/music/templates/music/create_update_song.html @@ -0,0 +1,96 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - ' %}{{ title }} + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    + + {% comment %} + {% trans '>>> 试试一键剽取~ <<<' %} + + {% endcomment %} + +
    + {% csrf_token %} + {{ form.media }} + {% for field in form %} + {% if field.name == 'release_date' %} + {{ field.label_tag }} + + {% else %} + {% if field.name != 'id' %} + {{ field.label_tag }} + {% endif %} + {{ field }} + {% endif %} + + {% endfor %} + + +
    +
    + +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/music/templates/music/create_update_song_review.html b/music/templates/music/create_update_song_review.html new file mode 100644 index 00000000..52ee274d --- /dev/null +++ b/music/templates/music/create_update_song_review.html @@ -0,0 +1,135 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - ' %}{{ title }} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + {{ song.title }} + + {{ song.get_source_site_display }} +
    +
    {% if song.artist %}{% trans '艺术家:' %} + {% for artist in song.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if song.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
    +
    {% if song.album %}{% trans '所属专辑:' %} + {{ song.album }} + {% endif %} +
    + +
    {% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
    + + {% if song.rating %} + {% trans '评分:' %} + {{ song.rating }} + {% endif %} +
    +
    +
    + +
    + {% csrf_token %} + {{ form.song }} +
    + {{ 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/music/templates/music/delete_album.html b/music/templates/music/delete_album.html new file mode 100644 index 00000000..36593eba --- /dev/null +++ b/music/templates/music/delete_album.html @@ -0,0 +1,104 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - 删除音乐' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    {% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}
    + +
    +
    + + + +
    +
    + +
    + {{ album.title }} + {{ album.get_source_site_display }} +
    + + {% if album.rating %} + {% trans '评分:' %} + + {{ album.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} + + {% if album.last_editor %} +
    + {% trans '最近编辑者:' %} + + {{ album.last_editor | default:"" }} + +
    + {% endif %} + +
    {% trans '上次编辑时间:' %}{{ album.edited_time }}
    + + {% if album.album_marks.all %} +
    {% trans '这个条目有' %} {{ album.album_marks.count }} 个标记
    + {% endif %} + {% if album.album_reviews.all %} +
    {% trans '这个条目有' %} {{ album.album_reviews.count }} 个评论
    + {% endif %} + +
    +
    +
    +
    +
    + {% csrf_token %} + +
    + +
    +
    +
    +
    + +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html new file mode 100644 index 00000000..f31c71a3 --- /dev/null +++ b/music/templates/music/delete_album_review.html @@ -0,0 +1,108 @@ +{% 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/music/templates/music/delete_song.html b/music/templates/music/delete_song.html new file mode 100644 index 00000000..ace5bc50 --- /dev/null +++ b/music/templates/music/delete_song.html @@ -0,0 +1,104 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - 删除音乐' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    {% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}
    + +
    +
    + + + +
    +
    + +
    + {{ song.title }} + {{ song.get_source_site_display }} +
    + + {% if song.rating %} + {% trans '评分:' %} + + {{ song.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} + + {% if song.last_editor %} +
    + {% trans '最近编辑者:' %} + + {{ song.last_editor | default:"" }} + +
    + {% endif %} + +
    {% trans '上次编辑时间:' %}{{ song.edited_time }}
    + + {% if song.song_marks.all %} +
    {% trans '这个条目有' %} {{ song.song_marks.count }} 个标记
    + {% endif %} + {% if song.song_reviews.all %} +
    {% trans '这个条目有' %} {{ song.song_reviews.count }} 个评论
    + {% endif %} + +
    +
    +
    +
    +
    + {% csrf_token %} + +
    + +
    +
    +
    +
    + +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html new file mode 100644 index 00000000..84019488 --- /dev/null +++ b/music/templates/music/delete_song_review.html @@ -0,0 +1,108 @@ +{% 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/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html new file mode 100644 index 00000000..01d48925 --- /dev/null +++ b/music/templates/music/scrape_album.html @@ -0,0 +1,109 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - 从豆瓣获取数据' %} + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {% trans '根据豆瓣内容填写下方表单' %} +
    + +
    +
    +
    + {% csrf_token %} + + {{ form.media }} + + {% for field in form %} + + {% if field.id_for_label == 'id_is_series' %} + + {{ field }} + {% else %} + {% if field.id_for_label != 'id_id' %} + + {% endif %} + {{ field }} + {% endif %} + + {% endfor %} + +
    + {% trans '剽取!' %} +
    +
    +
    +
    + +
    +
    +
    + {% trans '复制详情页链接' %} +
    +
    + {% csrf_token %} + + +
    +
    + +
    +
    +
    + +
    + {% include "partial/_footer.html" %} + +
    + + + + + diff --git a/music/templates/music/scrape_song.html b/music/templates/music/scrape_song.html new file mode 100644 index 00000000..e2cd4730 --- /dev/null +++ b/music/templates/music/scrape_song.html @@ -0,0 +1,109 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - 从豆瓣获取数据' %} + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {% trans '根据豆瓣内容填写下方表单' %} +
    + +
    +
    +
    + {% csrf_token %} + + {{ form.media }} + + {% for field in form %} + + {% if field.id_for_label == 'id_is_series' %} + + {{ field }} + {% else %} + {% if field.id_for_label != 'id_id' %} + + {% endif %} + {{ field }} + {% endif %} + + {% endfor %} + +
    + {% trans '剽取!' %} +
    +
    +
    +
    + +
    +
    +
    + {% trans '复制详情页链接' %} +
    +
    + {% csrf_token %} + + +
    +
    + +
    +
    +
    + +
    + {% include "partial/_footer.html" %} + +
    + + + + + diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html new file mode 100644 index 00000000..068c277c --- /dev/null +++ b/music/templates/music/song_detail.html @@ -0,0 +1,402 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} + + + + + + + + + + + + + + {% trans 'NiceDB - 音乐详情' %} | {{ song.title }} + + + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + + {{ song.title }} + +
    +
    + + {{ song.title }} + {{ song.get_source_site_display }} +
    + +
    +
    + {% if song.rating %} + + {{ song.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} +
    +
    {% if song.artist %}{% trans '艺术家:' %} + {% for artist in song.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if song.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %}
    +
    {% if song.release_date %} + {% trans '发行日期:' %}{{ song.release_date }} + {% endif %} +
    +
    {% if song.duration %} + {% trans '时长:' %}{{ song.get_duration_display }} + {% endif %} +
    +
    {% if song.genre %} + {% trans '流派:' %}{{ song.genre }} + {% endif %} +
    + + +
    +
    +
    {% if song.isrc %} + {% trans 'ISRC:' %}{{ song.isrc }} + {% endif %} +
    +
    {% if song.album %} + {% trans '所属专辑:' %}{{ song.album }} + {% endif %} +
    + {% if song.other_info %} + {% for k, v in song.other_info.items %} +
    + {{k}}:{{v}} +
    + {% endfor %} + {% endif %} + + + {% if song.last_editor %} +
    {% trans '最近编辑者:' %}{{ song.last_editor | default:"" }}
    + {% endif %} + +
    + {% trans '编辑这个作品' %} + {% if user.is_staff %} + / {% trans '删除' %} + {% endif %} +
    +
    + +
    + + {% for tag_dict in song_tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} + +
    +
    +
    +
    +
    +
    {% trans '简介' %}
    + {% if song.brief %} + +

    {{ song.brief | linebreaksbr }}

    + + + {% else %} +
    {% trans '暂无简介' %}
    + {% 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/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html new file mode 100644 index 00000000..0c1b8fae --- /dev/null +++ b/music/templates/music/song_mark_list.html @@ -0,0 +1,176 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'NiceDB - ' %}{{ song.title }}{% trans '的标记' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ song.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 %} + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + {{ song.title }} + + {{ + song.get_source_site_display }} +
    + +
    {% if song.artist %}{% trans '艺术家:' %} + {% for artist in song.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if song.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
    +
    {% if song.album %}{% trans '所属专辑:' %} + {{ song.album }} + {% endif %} +
    + +
    {% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %} +
    + {% if song.rating %} + {% trans '评分: ' %} + {{ song.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html new file mode 100644 index 00000000..806d56d5 --- /dev/null +++ b/music/templates/music/song_review_detail.html @@ -0,0 +1,153 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% 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 }} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + {{ song.title }} + + {{ song.get_source_site_display }} +
    + +
    {% if song.artist %}{% trans '艺术家:' %} + {% for artist in song.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if song.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
    +
    {% if song.album %}{% trans '所属专辑:' %} + {{ song.album }} + {% endif %} +
    + +
    {% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
    + {% if song.rating %} + {% trans '评分: ' %} + {{ song.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html new file mode 100644 index 00000000..c231ec08 --- /dev/null +++ b/music/templates/music/song_review_list.html @@ -0,0 +1,158 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'NiceDB - ' %}{{ song.title }}{% trans '的评论' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ song.title }}{% trans ' 的评论' %} +
    +
      + + {% for review in reviews %} + +
    • + + {{ review.owner.username }} + {% if review.is_private %} + + {% endif %} + {{ review.edited_time }} + + + {{ review.title }} + +
    • + {% empty %} +
      {% trans '无结果' %}
      + {% endfor %} + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + {{ song.title }} + + {{ song.get_source_site_display }} +
    + +
    {% if song.artist %}{% trans '艺术家:' %} + {% for artist in song.artist %} + 5 %}style="display: none;" {% endif %}> + {{ artist }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if song.artist|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    +
    {% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
    +
    {% if song.album %}{% trans '所属专辑:' %} + {{ song.album }} + {% endif %} +
    + +
    {% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
    + {% if song.rating %} + {% trans '评分: ' %} + {{ song.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/music/tests.py b/music/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/music/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/music/urls.py b/music/urls.py new file mode 100644 index 00000000..4acab46d --- /dev/null +++ b/music/urls.py @@ -0,0 +1,40 @@ +from django.urls import path +from .views import * + + +app_name = 'music' +urlpatterns = [ + path('song/create/', create_song, name='create_song'), + path('song//', retrieve_song, name='retrieve_song'), + path('song/update//', update_song, name='update_song'), + path('song/delete//', delete_song, name='delete_song'), + path('song/mark/', create_update_song_mark, name='create_update_song_mark'), + path('song//mark/list/', + retrieve_song_mark_list, name='retrieve_song_mark_list'), + path('song/mark/delete//', delete_song_mark, name='delete_song_mark'), + path('song//review/create/', create_song_review, name='create_song_review'), + path('song/review/update//', update_song_review, name='update_song_review'), + path('song/review/delete//', delete_song_review, name='delete_song_review'), + path('song/review//', retrieve_song_review, name='retrieve_song_review'), + path('song//review/list/', + retrieve_song_review_list, name='retrieve_song_review_list'), +# path('song/scrape/', scrape_song, name='scrape_song'), + path('song/click_to_scrape/', click_to_scrape_song, name='click_to_scrape_song'), + + path('album/create/', create_album, name='create_album'), + path('album//', retrieve_album, name='retrieve_album'), + path('album/update//', update_album, name='update_album'), + path('album/delete//', delete_album, name='delete_album'), + path('album/mark/', create_update_album_mark, name='create_update_album_mark'), + path('album//mark/list/', + retrieve_album_mark_list, name='retrieve_album_mark_list'), + path('album/mark/delete//', delete_album_mark, name='delete_album_mark'), + path('album//review/create/', create_album_review, name='create_album_review'), + path('album/review/update//', update_album_review, name='update_album_review'), + path('album/review/delete//', delete_album_review, name='delete_album_review'), + path('album/review//', retrieve_album_review, name='retrieve_album_review'), + path('album//review/list/', + retrieve_album_review_list, name='retrieve_album_review_list'), + path('album/scrape/', scrape_album, name='scrape_album'), + path('album/click_to_scrape/', click_to_scrape_album, name='click_to_scrape_album'), +] diff --git a/music/views.py b/music/views.py new file mode 100644 index 00000000..5065f15f --- /dev/null +++ b/music/views.py @@ -0,0 +1,1183 @@ +# from boofilsic.settings import MASTODON_TAGS +from .forms import * +from .models import * +from common.models import SourceSiteEnum +from common.views import PAGE_LINK_NUMBER, jump_or_scrape +from common.utils import PageLinksGenerator +from mastodon.utils import rating_to_emoji +from mastodon.api import check_visibility, post_toot, TootVisibilityEnum +from mastodon import mastodon_request_included +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.paginator import Paginator +from django.utils import timezone +from django.db.models import Count +from django.db import IntegrityError, transaction +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.http import HttpResponseBadRequest, HttpResponseServerError +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import render, get_object_or_404, redirect, reverse +import logging +from django.shortcuts import render + + + +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_song(request): + if request.method == 'GET': + form = SongForm() + return render( + request, + 'music/create_update_song.html', + { + 'form': form, + 'title': _('添加音乐'), + 'submit_url': reverse("music:create_song"), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + elif request.method == 'POST': + if request.user.is_authenticated: + # only local user can alter public data + form = SongForm(request.POST, request.FILES) + if form.is_valid(): + form.instance.last_editor = request.user + try: + with transaction.atomic(): + form.save() + if form.instance.source_site == SourceSiteEnum.IN_SITE.value: + real_url = form.instance.get_absolute_url() + form.instance.source_url = real_url + form.instance.save() + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + return redirect(reverse("music:retrieve_song", args=[form.instance.id])) + else: + return render( + request, + 'music/create_update_song.html', + { + 'form': form, + 'title': _('添加音乐'), + 'submit_url': reverse("music:create_song"), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + else: + return redirect(reverse("users:login")) + else: + return HttpResponseBadRequest() + + +@login_required +def update_song(request, id): + if request.method == 'GET': + song = get_object_or_404(Song, pk=id) + form = SongForm(instance=song) + page_title = _('修改音乐') + return render( + request, + 'music/create_update_song.html', + { + 'form': form, + 'title': page_title, + 'submit_url': reverse("music:update_song", args=[song.id]), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + elif request.method == 'POST': + song = get_object_or_404(Song, pk=id) + form = SongForm(request.POST, request.FILES, instance=song) + page_title = _('修改音乐') + if form.is_valid(): + form.instance.last_editor = request.user + form.instance.edited_time = timezone.now() + try: + with transaction.atomic(): + form.save() + if form.instance.source_site == SourceSiteEnum.IN_SITE.value: + real_url = form.instance.get_absolute_url() + form.instance.source_url = real_url + form.instance.save() + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + else: + return render( + request, + 'music/create_update_song.html', + { + 'form': form, + 'title': page_title, + 'submit_url': reverse("music:update_song", args=[song.id]), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + return redirect(reverse("music:retrieve_song", args=[form.instance.id])) + + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +# @login_required +def retrieve_song(request, id): + if request.method == 'GET': + song = get_object_or_404(Song, pk=id) + mark = None + mark_tags = None + review = None + + def ms_to_readable(ms): + if not ms: + return + x = ms // 1000 + seconds = x % 60 + x //= 60 + if x == 0: + return f"{seconds}" + minutes = x % 60 + x //= 60 + if x == 0: + return f"{minutes}:{seconds}" + hours = x % 24 + return f"{hours}:{minutes}:{seconds}" + + song.get_duration_display = ms_to_readable(song.duration) + + + # retrieve tags + song_tag_list = song.song_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 = SongMark.objects.get(owner=request.user, song=song) + except ObjectDoesNotExist: + mark = None + if mark: + mark_tags = mark.songmark_tags.all() + mark.get_status_display = MusicMarkStatusTranslator(mark.status) + mark_form = SongMarkForm(instance=mark, initial={ + 'tags': mark_tags + }) + else: + mark_form = SongMarkForm(initial={ + 'song': song, + 'tags': mark_tags + }) + + # retrieve user review + try: + if request.user.is_authenticated: + review = SongReview.objects.get( + owner=request.user, song=song) + 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 = SongMark.get_available( + song, request.user, request.session['oauth_token']) + review_list = SongReview.get_available( + song, 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 = MusicMarkStatusTranslator(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, + 'music/song_detail.html', + { + 'song': song, + '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, + 'song_tag_list': song_tag_list, + 'mark_tags': mark_tags, + } + ) + else: + logger.warning('non-GET method at /song/') + return HttpResponseBadRequest() + + +@permission_required("music.delete_song") +@login_required +def delete_song(request, id): + if request.method == 'GET': + song = get_object_or_404(Song, pk=id) + return render( + request, + 'music/delete_song.html', + { + 'song': song, + } + ) + elif request.method == 'POST': + if request.user.is_staff: + # only staff has right to delete + song = get_object_or_404(Song, pk=id) + song.delete() + return redirect(reverse("common:home")) + else: + raise PermissionDenied() + else: + return HttpResponseBadRequest() + + +# user owned entites +########################### +@mastodon_request_included +@login_required +def create_update_song_mark(request): + # check list: + # clean rating if is wish + # transaction on updating song 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(SongMark, pk=pk) + if request.user != mark.owner: + return HttpResponseBadRequest() + old_rating = mark.rating + old_tags = mark.songmark_tags.all() + # update + form = SongMarkForm(request.POST, instance=mark) + else: + # create + form = SongMarkForm(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() + song = form.instance.song + + try: + with transaction.atomic(): + # update song rating + song.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']: + SongTag.objects.create( + content=tag, + song=song, + mark=form.instance + ) + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + + 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("music:retrieve_song", + args=[song.id]) + words = MusicMarkStatusTranslator(form.cleaned_data['status']) +\ + f"《{song.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(request.user.mastodon_site, 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("music:retrieve_song", args=[form.instance.song.id])) + else: + return HttpResponseBadRequest("invalid method") + + +@mastodon_request_included +@login_required +def retrieve_song_mark_list(request, song_id): + if request.method == 'GET': + song = get_object_or_404(Song, pk=song_id) + queryset = SongMark.get_available( + song, 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 = MusicMarkStatusTranslator(m.status) + return render( + request, + 'music/song_mark_list.html', + { + 'marks': marks, + 'song': song, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def delete_song_mark(request, id): + if request.method == 'POST': + mark = get_object_or_404(SongMark, pk=id) + if request.user != mark.owner: + return HttpResponseBadRequest() + song_id = mark.song.id + try: + with transaction.atomic(): + # update song rating + mark.song.update_rating(mark.rating, None) + mark.delete() + except IntegrityError as e: + return HttpResponseServerError() + return redirect(reverse("music:retrieve_song", args=[song_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def create_song_review(request, song_id): + if request.method == 'GET': + form = SongReviewForm(initial={'song': song_id}) + song = get_object_or_404(Song, pk=song_id) + return render( + request, + 'music/create_update_song_review.html', + { + 'form': form, + 'title': _("添加评论"), + 'song': song, + 'submit_url': reverse("music:create_song_review", args=[song_id]), + } + ) + elif request.method == 'POST': + form = SongReviewForm(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("music:retrieve_song_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.song.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(request.user.mastodon_site, 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("music:retrieve_song_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def update_song_review(request, id): + if request.method == 'GET': + review = get_object_or_404(SongReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = SongReviewForm(instance=review) + song = review.song + return render( + request, + 'music/create_update_song_review.html', + { + 'form': form, + 'title': _("编辑评论"), + 'song': song, + 'submit_url': reverse("music:update_song_review", args=[review.id]), + } + ) + elif request.method == 'POST': + review = get_object_or_404(SongReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = SongReviewForm(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("music:retrieve_song_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.song.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(request.user.mastodon_site, 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("music:retrieve_song_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@login_required +def delete_song_review(request, id): + if request.method == 'GET': + review = get_object_or_404(SongReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + review_form = SongReviewForm(instance=review) + return render( + request, + 'music/delete_song_review.html', + { + 'form': review_form, + 'review': review, + } + ) + elif request.method == 'POST': + review = get_object_or_404(SongReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + song_id = review.song.id + review.delete() + return redirect(reverse("music:retrieve_song", args=[song_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_song_review(request, id): + if request.method == 'GET': + review = get_object_or_404(SongReview, pk=id) + if not check_visibility(review, request.session['oauth_token'], request.user): + msg = _("你没有访问这个页面的权限😥") + return render( + request, + 'common/error.html', + { + 'msg': msg, + } + ) + review_form = SongReviewForm(instance=review) + song = review.song + try: + mark = SongMark.objects.get(owner=review.owner, song=song) + mark.get_status_display = MusicMarkStatusTranslator(mark.status) + except ObjectDoesNotExist: + mark = None + return render( + request, + 'music/song_review_detail.html', + { + 'form': review_form, + 'review': review, + 'song': song, + 'mark': mark, + } + ) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_song_review_list(request, song_id): + if request.method == 'GET': + song = get_object_or_404(Song, pk=song_id) + queryset = SongReview.get_available( + song, 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, + 'music/song_review_list.html', + { + 'reviews': reviews, + 'song': song, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def scrape_song(request): + if request.method == 'GET': + keywords = request.GET.get('q') + form = SongForm() + return render( + request, + 'music/scrape_song.html', + { + 'q': keywords, + 'form': form, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def click_to_scrape_song(request): + if request.method == "POST": + url = request.POST.get("url") + if url: + return jump_or_scrape(request, url) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@login_required +def create_album(request): + if request.method == 'GET': + form = AlbumForm() + return render( + request, + 'music/create_update_album.html', + { + 'form': form, + 'title': _('添加音乐'), + 'submit_url': reverse("music:create_album"), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + elif request.method == 'POST': + if request.user.is_authenticated: + # only local user can alter public data + form = AlbumForm(request.POST, request.FILES) + if form.is_valid(): + form.instance.last_editor = request.user + try: + with transaction.atomic(): + form.save() + if form.instance.source_site == SourceSiteEnum.IN_SITE.value: + real_url = form.instance.get_absolute_url() + form.instance.source_url = real_url + form.instance.save() + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + return redirect(reverse("music:retrieve_album", args=[form.instance.id])) + else: + return render( + request, + 'music/create_update_album.html', + { + 'form': form, + 'title': _('添加音乐'), + 'submit_url': reverse("music:create_album"), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + else: + return redirect(reverse("users:login")) + else: + return HttpResponseBadRequest() + + +@login_required +def update_album(request, id): + if request.method == 'GET': + album = get_object_or_404(Album, pk=id) + form = AlbumForm(instance=album) + page_title = _('修改音乐') + return render( + request, + 'music/create_update_album.html', + { + 'form': form, + 'title': page_title, + 'submit_url': reverse("music:update_album", args=[album.id]), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + elif request.method == 'POST': + album = get_object_or_404(Album, pk=id) + form = AlbumForm(request.POST, request.FILES, instance=album) + page_title = _('修改音乐') + if form.is_valid(): + form.instance.last_editor = request.user + form.instance.edited_time = timezone.now() + try: + with transaction.atomic(): + form.save() + if form.instance.source_site == SourceSiteEnum.IN_SITE.value: + real_url = form.instance.get_absolute_url() + form.instance.source_url = real_url + form.instance.save() + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + else: + return render( + request, + 'music/create_update_album.html', + { + 'form': form, + 'title': page_title, + 'submit_url': reverse("music:update_album", args=[album.id]), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + return redirect(reverse("music:retrieve_album", args=[form.instance.id])) + + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +# @login_required +def retrieve_album(request, id): + if request.method == 'GET': + album = get_object_or_404(Album, pk=id) + mark = None + mark_tags = None + review = None + + def ms_to_readable(ms): + if not ms: + return + x = ms // 1000 + seconds = x % 60 + x //= 60 + if x == 0: + return f"{seconds}" + minutes = x % 60 + x //= 60 + if x == 0: + return f"{minutes}:{seconds}" + hours = x % 24 + return f"{hours}:{minutes}:{seconds}" + + album.get_duration_display = ms_to_readable(album.duration) + + # retrieve tags + album_tag_list = album.album_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 = AlbumMark.objects.get(owner=request.user, album=album) + except ObjectDoesNotExist: + mark = None + if mark: + mark_tags = mark.albummark_tags.all() + mark.get_status_display = MusicMarkStatusTranslator(mark.status) + mark_form = AlbumMarkForm(instance=mark, initial={ + 'tags': mark_tags + }) + else: + mark_form = AlbumMarkForm(initial={ + 'album': album, + 'tags': mark_tags + }) + + # retrieve user review + try: + if request.user.is_authenticated: + review = AlbumReview.objects.get( + owner=request.user, album=album) + 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 = AlbumMark.get_available( + album, request.user, request.session['oauth_token']) + review_list = AlbumReview.get_available( + album, 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 = MusicMarkStatusTranslator(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, + 'music/album_detail.html', + { + 'album': album, + '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, + 'album_tag_list': album_tag_list, + 'mark_tags': mark_tags, + } + ) + else: + logger.warning('non-GET method at /album/') + return HttpResponseBadRequest() + + +@permission_required("music.delete_album") +@login_required +def delete_album(request, id): + if request.method == 'GET': + album = get_object_or_404(Album, pk=id) + return render( + request, + 'music/delete_album.html', + { + 'album': album, + } + ) + elif request.method == 'POST': + if request.user.is_staff: + # only staff has right to delete + album = get_object_or_404(Album, pk=id) + album.delete() + return redirect(reverse("common:home")) + else: + raise PermissionDenied() + else: + return HttpResponseBadRequest() + + +# user owned entites +########################### +@mastodon_request_included +@login_required +def create_update_album_mark(request): + # check list: + # clean rating if is wish + # transaction on updating album 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(AlbumMark, pk=pk) + if request.user != mark.owner: + return HttpResponseBadRequest() + old_rating = mark.rating + old_tags = mark.albummark_tags.all() + # update + form = AlbumMarkForm(request.POST, instance=mark) + else: + # create + form = AlbumMarkForm(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() + album = form.instance.album + + try: + with transaction.atomic(): + # update album rating + album.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']: + AlbumTag.objects.create( + content=tag, + album=album, + mark=form.instance + ) + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + + 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("music:retrieve_album", + args=[album.id]) + words = MusicMarkStatusTranslator(form.cleaned_data['status']) +\ + f"《{album.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(request.user.mastodon_site, 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("music:retrieve_album", args=[form.instance.album.id])) + else: + return HttpResponseBadRequest("invalid method") + + +@mastodon_request_included +@login_required +def retrieve_album_mark_list(request, album_id): + if request.method == 'GET': + album = get_object_or_404(Album, pk=album_id) + queryset = AlbumMark.get_available( + album, 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 = MusicMarkStatusTranslator(m.status) + return render( + request, + 'music/album_mark_list.html', + { + 'marks': marks, + 'album': album, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def delete_album_mark(request, id): + if request.method == 'POST': + mark = get_object_or_404(AlbumMark, pk=id) + if request.user != mark.owner: + return HttpResponseBadRequest() + album_id = mark.album.id + try: + with transaction.atomic(): + # update album rating + mark.album.update_rating(mark.rating, None) + mark.delete() + except IntegrityError as e: + return HttpResponseServerError() + return redirect(reverse("music:retrieve_album", args=[album_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def create_album_review(request, album_id): + if request.method == 'GET': + form = AlbumReviewForm(initial={'album': album_id}) + album = get_object_or_404(Album, pk=album_id) + return render( + request, + 'music/create_update_album_review.html', + { + 'form': form, + 'title': _("添加评论"), + 'album': album, + 'submit_url': reverse("music:create_album_review", args=[album_id]), + } + ) + elif request.method == 'POST': + form = AlbumReviewForm(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("music:retrieve_album_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.album.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(request.user.mastodon_site, 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("music:retrieve_album_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def update_album_review(request, id): + if request.method == 'GET': + review = get_object_or_404(AlbumReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = AlbumReviewForm(instance=review) + album = review.album + return render( + request, + 'music/create_update_album_review.html', + { + 'form': form, + 'title': _("编辑评论"), + 'album': album, + 'submit_url': reverse("music:update_album_review", args=[review.id]), + } + ) + elif request.method == 'POST': + review = get_object_or_404(AlbumReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = AlbumReviewForm(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("music:retrieve_album_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.album.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(request.user.mastodon_site, 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("music:retrieve_album_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@login_required +def delete_album_review(request, id): + if request.method == 'GET': + review = get_object_or_404(AlbumReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + review_form = AlbumReviewForm(instance=review) + return render( + request, + 'music/delete_album_review.html', + { + 'form': review_form, + 'review': review, + } + ) + elif request.method == 'POST': + review = get_object_or_404(AlbumReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + album_id = review.album.id + review.delete() + return redirect(reverse("music:retrieve_album", args=[album_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_album_review(request, id): + if request.method == 'GET': + review = get_object_or_404(AlbumReview, pk=id) + if not check_visibility(review, request.session['oauth_token'], request.user): + msg = _("你没有访问这个页面的权限😥") + return render( + request, + 'common/error.html', + { + 'msg': msg, + } + ) + review_form = AlbumReviewForm(instance=review) + album = review.album + try: + mark = AlbumMark.objects.get(owner=review.owner, album=album) + mark.get_status_display = MusicMarkStatusTranslator(mark.status) + except ObjectDoesNotExist: + mark = None + return render( + request, + 'music/album_review_detail.html', + { + 'form': review_form, + 'review': review, + 'album': album, + 'mark': mark, + } + ) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_album_review_list(request, album_id): + if request.method == 'GET': + album = get_object_or_404(Album, pk=album_id) + queryset = AlbumReview.get_available( + album, 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, + 'music/album_review_list.html', + { + 'reviews': reviews, + 'album': album, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def scrape_album(request): + if request.method == 'GET': + keywords = request.GET.get('q') + form = AlbumForm() + return render( + request, + 'music/scrape_album.html', + { + 'q': keywords, + 'form': form, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def click_to_scrape_album(request): + if request.method == "POST": + url = request.POST.get("url") + if url: + return jump_or_scrape(request, url) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() diff --git a/users/views.py b/users/views.py index 96e183ab..ca7e0191 100644 --- a/users/views.py +++ b/users/views.py @@ -108,6 +108,14 @@ def register(request): elif request.method == 'POST': token = request.session['new_user_token'] user_data = get_user_data(request.COOKIES['mastodon_domain'], token) + if user_data is None: + return render( + request, + 'common/error.html', + { + 'msg': _("长毛象访问失败😫") + } + ) new_user = User( username=user_data['username'], mastodon_id=user_data['id'],