diff --git a/boofilsic/urls.py b/boofilsic/urls.py index ecc6d0a3..1c9cec13 100644 --- a/boofilsic/urls.py +++ b/boofilsic/urls.py @@ -30,3 +30,8 @@ urlpatterns = [ path('', include('common.urls')), ] + +if settings.DEBUG: + from django.conf.urls.static import static + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/books/templates/books/detail.html b/books/templates/books/detail.html index bb52705f..0a21c58f 100644 --- a/books/templates/books/detail.html +++ b/books/templates/books/detail.html @@ -120,20 +120,18 @@
+ {% if book.brief %}
{% trans '简介' %}
- {% if book.brief %}

{{ book.brief | linebreaksbr }}

展开全部
- {% else %} -
{% trans '暂无简介' %}
- {% endif %} - +
+ {% endif %} {% if book.contents %}
@@ -160,7 +158,7 @@ {% endif %} {% if others_mark.is_private %} - + {% endif %} {{ others_mark.edited_time }} {% if others_mark.text %} @@ -184,7 +182,7 @@
  • {{ others_review.owner.username }} {% if others_review.is_private %} - + {% endif %} {{ others_review.edited_time }} {{ others_review.title }} @@ -212,7 +210,7 @@ {% endif %} {% endif %} {% if mark.is_private %} - + {% endif %} {% trans '修改' %} @@ -255,7 +253,7 @@ {% trans '我的评论' %} {% if review.is_private %} - + {% endif %} diff --git a/books/templates/books/mark_list.html b/books/templates/books/mark_list.html index 8ca02ad8..99c390ea 100644 --- a/books/templates/books/mark_list.html +++ b/books/templates/books/mark_list.html @@ -45,8 +45,8 @@ data-rating-score="{{ mark.rating | floatformat:"0" }}"> {% endif %} {% if mark.is_private %} - + + diff --git a/books/templates/books/review_detail.html b/books/templates/books/review_detail.html index 9f294ca1..bfde5434 100644 --- a/books/templates/books/review_detail.html +++ b/books/templates/books/review_detail.html @@ -37,8 +37,7 @@ {{ review.title }} {% if review.is_private %} - + diff --git a/books/templates/books/review_list.html b/books/templates/books/review_list.html index d0ccc31f..646a67fb 100644 --- a/books/templates/books/review_list.html +++ b/books/templates/books/review_list.html @@ -40,7 +40,7 @@ {{ review.owner.username }} {% if review.is_private %} - + {% endif %} {{ review.edited_time }} diff --git a/books/views.py b/books/views.py index 721918f6..a06c31cf 100644 --- a/books/views.py +++ b/books/views.py @@ -8,7 +8,6 @@ from django.db import IntegrityError, transaction from django.db.models import Count from django.utils import timezone from django.core.paginator import Paginator -from django.core.files.uploadedfile import SimpleUploadedFile from mastodon import mastodon_request_included from mastodon.api import check_visibility, post_toot, TootVisibilityEnum from mastodon.utils import rating_to_emoji diff --git a/common/forms.py b/common/forms.py index e6a6bbaf..57a3d7ff 100644 --- a/common/forms.py +++ b/common/forms.py @@ -7,30 +7,42 @@ import json class KeyValueInput(forms.Widget): - template_name = 'widgets/key_value.html' - + """ + Input widget for Json field + """ + template_name = 'widgets/hstore.html' + def get_context(self, name, value, attrs): - """ called when rendering """ - context = {} - context['widget'] = { - 'name': name, - 'is_hidden': self.is_hidden, - 'required': self.is_required, - 'value': self.format_value(value), - 'attrs': self.build_attrs(self.attrs, attrs), - 'template_name': self.template_name, - 'keyvalue_pairs': {}, - } - if context['widget']['value']: - key_value_pairs = json.loads(context['widget']['value']) - # for kv in key_value_pairs: - context['widget']['keyvalue_pairs'] = key_value_pairs + context = super().get_context(name, value, attrs) + data = json.loads(context['widget']['value']) + context['widget']['value'] = [ {p[0]: p[1]} for p in data.items()] return context class Media: js = ('js/key_value_input.js',) +class HstoreInput(forms.Widget): + """ + Input widget for Hstore field + """ + template_name = 'widgets/hstore.html' + + def format_value(self, value): + """ + Return a value as it should appear when rendered in a template. + """ + if value == '' or value is None: + return None + if self.is_localized: + return formats.localize_input(value) + # do not return str + return value + + class Media: + js = ('js/key_value_input.js',) + + class JSONField(postgres.JSONField): widget = KeyValueInput def to_python(self, value): @@ -145,23 +157,6 @@ class MultiSelect(forms.SelectMultiple): js = ('lib/js/multiple-select.min.js',) -class HstoreInput(forms.Widget): - template_name = 'widgets/hstore.html' - - def format_value(self, value): - """ - Return a value as it should appear when rendered in a template. - """ - if value == '' or value is None: - return None - if self.is_localized: - return formats.localize_input(value) - return value - - class Media: - js = ('js/key_value_input.js',) - - class HstoreField(forms.CharField): widget = HstoreInput def to_python(self, value): @@ -176,6 +171,55 @@ class HstoreField(forms.CharField): return pairs +class DurationInput(forms.TextInput): + """ + HH:mm:ss input widget + """ + input_type = "time" + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + # context['widget']['type'] = self.input_type + context['widget']['attrs']['step'] = "1" + return context + + def format_value(self, value): + """ + Given `value` is an integer in ms + """ + ms = value + if not ms: + return super().format_value(None) + x = ms // 1000 + seconds = x % 60 + x //= 60 + if x == 0: + return super().format_value(f"00:00:{seconds:0>2}") + minutes = x % 60 + x //= 60 + if x == 0: + return super().format_value(f"00:{minutes:0>2}:{seconds:0>2}") + hours = x % 24 + return super().format_value(f"{hours:0>2}:{minutes:0>2}:{seconds:0>2}") + + +class DurationField(forms.TimeField): + widget = DurationInput + def to_python(self, value): + + # empty value + if value is None or value == '': + return + + # if value is integer in ms + if isinstance(value, int): + return value + + # if value is string in time format + h, m, s = value.split(':') + return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000 + + ############################# # Form ############################# diff --git a/common/models.py b/common/models.py index e20a761d..4ac502e2 100644 --- a/common/models.py +++ b/common/models.py @@ -20,6 +20,7 @@ RE_HTML_TAG = re.compile(r"<[^>]*>") class SourceSiteEnum(models.TextChoices): IN_SITE = "in-site", CLIENT_NAME DOUBAN = "douban", _("豆瓣") + SPOTIFY = "spotify", _("Spotify") class Entity(models.Model): diff --git a/common/scraper.py b/common/scraper.py index 5752aa2b..033cffcb 100644 --- a/common/scraper.py +++ b/common/scraper.py @@ -2,11 +2,19 @@ import requests import functools import random import logging -from lxml import html import re import dateparser +import datetime +import time +from lxml import html +from mimetypes import guess_extension +from threading import Thread from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG +from boofilsic.settings import SPOTIFY_CREDENTIAL +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.files.uploadedfile import SimpleUploadedFile from common.models import SourceSiteEnum from movies.models import Movie, MovieGenreEnum from movies.forms import MovieForm @@ -56,15 +64,19 @@ def log_url(func): try: return func(*args, **kwargs) except Exception as e: - # log the url + # log the url and trace stack logger.error(f"Scrape Failed URL: {args[1]}") - logger.error(str(e)) + logger.error("Expections during scraping:", exc_info=e) raise e return wrapper class AbstractScraper: + """ + Scrape entities. The entities means those defined in the models.py file, + like Book, Movie...... + """ # subclasses must specify those two variables # site means general sites, like amazon/douban etc @@ -77,6 +89,10 @@ class AbstractScraper: form_class = None # used to extract effective url regex = None + # scraped raw image + raw_img = None + # scraped raw data + raw_data = {} def __init_subclass__(cls, **kwargs): # this statement initialize the subclasses @@ -107,11 +123,15 @@ class AbstractScraper: Scrape/request model schema specified data from given url and return it. Implementations of subclasses to this method would be decorated as class method. return (data_dict, image) + Should set the `raw_data` and the `raw_img` """ raise NotImplementedError("Subclass should implement this method") @classmethod def get_effective_url(cls, raw_url): + """ + The return value should be identical with that saved in DB as `source_url` + """ url = cls.regex.findall(raw_url) if not url: raise ValueError("not valid url") @@ -166,7 +186,25 @@ class AbstractScraper: ) if img_response.status_code == 200: raw_img = img_response.content - return raw_img + content_type = img_response.headers.get('Content-Type') + ext = guess_extension(content_type.partition(';')[0].strip()) + return raw_img, ext + + + @classmethod + def save(cls, request_user): + entity_cover = { + 'cover': SimpleUploadedFile('temp' + cls.img_ext, cls.raw_img) + } + form = cls.form_class(cls.raw_data, entity_cover) + if form.is_valid(): + form.instance.last_editor = request_user + form.save() + cls.instance = form.instance + else: + logger.error(str(form.errors)) + raise ValidationError("Form invalid.") + return form class DoubanBookScraper(AbstractScraper): @@ -175,7 +213,7 @@ class DoubanBookScraper(AbstractScraper): data_class = Book form_class = BookForm - regex = re.compile(r"https://book.douban.com/subject/\d+/{0,1}") + regex = re.compile(r"https://book\.douban\.com/subject/\d+/{0,1}") def scrape(self, url): headers = DEFAULT_REQUEST_HEADERS.copy() @@ -265,7 +303,7 @@ class DoubanBookScraper(AbstractScraper): img_url_elem = content.xpath("//*[@id='mainpic']/a/img/@src") img_url = img_url_elem[0].strip() if img_url_elem else None - raw_img = self.download_image(img_url) + raw_img, ext = self.download_image(img_url) # there are two html formats for authors and translators authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/ @@ -326,6 +364,7 @@ class DoubanBookScraper(AbstractScraper): 'source_site': self.site_name, 'source_url': self.get_effective_url(url), } + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext return data, raw_img @@ -335,7 +374,7 @@ class DoubanMovieScraper(AbstractScraper): data_class = Movie form_class = MovieForm - regex = re.compile(r"https://movie.douban.com/subject/\d+/{0,1}") + regex = re.compile(r"https://movie\.douban\.com/subject/\d+/{0,1}") def scrape(self, url): headers = DEFAULT_REQUEST_HEADERS.copy() @@ -473,7 +512,7 @@ class DoubanMovieScraper(AbstractScraper): img_url_elem = content.xpath("//img[@rel='v:image']/@src") img_url = img_url_elem[0].strip() if img_url_elem else None - raw_img = self.download_image(img_url) + raw_img, ext = self.download_image(img_url) data = { 'title': title, @@ -498,6 +537,7 @@ class DoubanMovieScraper(AbstractScraper): 'source_site': self.site_name, 'source_url': self.get_effective_url(url), } + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext return data, raw_img @@ -507,7 +547,7 @@ class DoubanAlbumScraper(AbstractScraper): data_class = Album form_class = AlbumForm - regex = re.compile(r"https://music.douban.com/subject/\d+/{0,1}") + regex = re.compile(r"https://music\.douban\.com/subject/\d+/{0,1}") def scrape(self, url): headers = DEFAULT_REQUEST_HEADERS.copy() @@ -533,7 +573,7 @@ class DoubanAlbumScraper(AbstractScraper): 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 + "RELATIVE_BASE": datetime.datetime(1900, 1, 1)}) if date_elem else None company_elem = content.xpath( "//div[@id='info']//span[text()='出版者:']/following::text()[1]") @@ -581,7 +621,7 @@ class DoubanAlbumScraper(AbstractScraper): 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) + raw_img, ext = self.download_image(img_url) data = { 'title': title, @@ -596,4 +636,285 @@ class DoubanAlbumScraper(AbstractScraper): 'source_site': self.site_name, 'source_url': self.get_effective_url(url), } + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext return data, raw_img + + +spotify_token = None +spotify_token_expire_time = time.time() + +class SpotifyTrackScraper(AbstractScraper): + site_name = SourceSiteEnum.SPOTIFY.value + # API URL + host = 'https://open.spotify.com/track/' + data_class = Song + form_class = SongForm + + regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+") + + def scrape(self, url): + """ + Request from API, not really scraping + """ + global spotify_token, spotify_token_expire_time + + if spotify_token is None or is_spotify_token_expired(): + invoke_spotify_token() + effective_url = self.get_effective_url(url) + if effective_url is None: + raise ValueError("not valid url") + + api_url = self.get_api_url(effective_url) + headers = { + 'Authorization': f"Bearer {spotify_token}" + } + r = requests.get(api_url, headers=headers) + res_data = r.json() + + artist = [] + for artist_dict in res_data['artists']: + artist.append(artist_dict['name']) + if not artist: + artist = None + + title = res_data['name'] + + release_date = dateparser.parse( + res_data['album']['release_date'], + settings={ + "RELATIVE_BASE": datetime.datetime(1900, 1, 1) + } + ) + + duration = res_data['duration_ms'] + + if res_data['external_ids'].get('isrc'): + isrc = res_data['external_ids']['isrc'] + else: + isrc = None + + raw_img, ext = self.download_image(res_data['album']['images'][0]['url']) + + data = { + 'title': title, + 'artist': artist, + 'genre': None, + 'release_date': release_date, + 'duration': duration, + 'isrc': isrc, + 'album': None, + 'brief': None, + 'other_info': None, + 'source_site': self.site_name, + 'source_url': effective_url, + } + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext + return data, raw_img + + @classmethod + def get_effective_url(cls, raw_url): + code = cls.regex.findall(raw_url) + if code: + return f"https://open.spotify.com/track/{code[0]}" + else: + return None + + @classmethod + def get_api_url(cls, url): + return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0] + + +class SpotifyAlbumScraper(AbstractScraper): + site_name = SourceSiteEnum.SPOTIFY.value + # API URL + host = 'https://open.spotify.com/album/' + data_class = Album + form_class = AlbumForm + + regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+") + + def scrape(self, url): + """ + Request from API, not really scraping + """ + global spotify_token, spotify_token_expire_time + + if spotify_token is None or is_spotify_token_expired(): + invoke_spotify_token() + effective_url = self.get_effective_url(url) + if effective_url is None: + raise ValueError("not valid url") + + api_url = self.get_api_url(effective_url) + headers = { + 'Authorization': f"Bearer {spotify_token}" + } + r = requests.get(api_url, headers=headers) + res_data = r.json() + + artist = [] + for artist_dict in res_data['artists']: + artist.append(artist_dict['name']) + + title = res_data['name'] + + genre = ', '.join(res_data['genres']) + + company = [] + for com in res_data['copyrights']: + company.append(com['text']) + + duration = 0 + track_list = [] + track_urls = [] + for track in res_data['tracks']['items']: + track_urls.append(track['external_urls']['spotify']) + duration += track['duration_ms'] + if res_data['tracks']['items'][-1]['disc_number'] > 1: + # more than one disc + track_list.append(str( + track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name']) + else: + track_list.append(str(track['track_number']) + '. ' + track['name']) + track_list = '\n'.join(track_list) + + + release_date = dateparser.parse( + res_data['release_date'], + settings={ + "RELATIVE_BASE": datetime.datetime(1900, 1, 1) + } + ) + + other_info = {} + if res_data['external_ids'].get('upc'): + # bar code + other_info['UPC'] = res_data['external_ids']['upc'] + + raw_img, ext = self.download_image(res_data['images'][0]['url']) + + data = { + 'title': title, + 'artist': artist, + 'genre': genre, + 'track_list': track_list, + 'release_date': release_date, + 'duration': duration, + 'company': company, + 'brief': None, + 'other_info': other_info, + 'source_site': self.site_name, + 'source_url': effective_url, + } + + # set tracks_data, used for adding tracks + self.track_urls = track_urls + + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext + return data, raw_img + + @classmethod + def get_effective_url(cls, raw_url): + code = cls.regex.findall(raw_url) + if code: + return f"https://open.spotify.com/album/{code[0]}" + else: + return None + + @classmethod + def save(cls, request_user): + form = super().save(request_user) + task = Thread( + target=cls.add_tracks, + args=(form.instance, request_user), + daemon=True + ) + task.start() + return form + + @classmethod + def get_api_url(cls, url): + return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0] + + @classmethod + def add_tracks(cls, album: Album, request_user): + to_be_updated_tracks = [] + for track_url in cls.track_urls: + track = cls.get_track_or_none(track_url) + # seems lik if fire too many requests at the same time + # spotify would limit access + if track is None: + task = Thread( + target=cls.scrape_and_save_track, + args=(track_url, album, request_user), + daemon=True + ) + task.start() + task.join() + else: + to_be_updated_tracks.append(track) + cls.bulk_update_track_album(to_be_updated_tracks, album, request_user) + + @classmethod + def get_track_or_none(cls, track_url: str): + try: + instance = Song.objects.get(source_url=track_url) + return instance + except ObjectDoesNotExist: + return None + + @classmethod + def scrape_and_save_track(cls, url: str, album: Album, request_user): + data, img = SpotifyTrackScraper.scrape(url) + SpotifyTrackScraper.raw_data['album'] = album + SpotifyTrackScraper.save(request_user) + + @classmethod + def bulk_update_track_album(cls, tracks, album, request_user): + for track in tracks: + track.last_editor = request_user + track.edited_time = timezone.now() + track.album = album + Song.objects.bulk_update(tracks, [ + 'last_editor', + 'edited_time', + 'album' + ]) + + +def is_spotify_token_expired(): + global spotify_token_expire_time + return True if spotify_token_expire_time <= time.time() else False + + +def invoke_spotify_token(): + global spotify_token, spotify_token_expire_time + r = requests.post( + "https://accounts.spotify.com/api/token", + data={ + "grant_type": "client_credentials" + }, + headers={ + "Authorization": f"Basic {SPOTIFY_CREDENTIAL}" + } + ) + data = r.json() + if r.status_code == 401: + # token expired, try one more time + # this maybe caused by external operations, + # for example debugging using a http client + r = requests.post( + "https://accounts.spotify.com/api/token", + data={ + "grant_type": "client_credentials" + }, + headers={ + "Authorization": f"Basic {SPOTIFY_CREDENTIAL}" + } + ) + data = r.json() + elif r.status_code != 200: + raise Exception(f"Request to spotify API fails. Reason: {r.reason}") + # minus 2 for execution time error + spotify_token_expire_time = int(data['expires_in']) + time.time() - 2 + spotify_token = data['access_token'] diff --git a/common/static/css/boofilsic.css b/common/static/css/boofilsic.css index 0ceb6283..5afcfa0a 100644 --- a/common/static/css/boofilsic.css +++ b/common/static/css/boofilsic.css @@ -1201,6 +1201,8 @@ select::placeholder { padding-top: 2px; font-weight: lighter; letter-spacing: 0.1rem; + word-break: keep-all; + opacity: 0.8; position: relative; top: -1px; } @@ -1211,8 +1213,16 @@ select::placeholder { } .source-label.source-label__douban { - border-color: #319840; - color: #319840; + border: none; + color: white; + background-color: #319840; +} + +.source-label.source-label__spotify { + background-color: #1ed760; + color: black; + border: none; + font-weight: bold; } .main-section-wrapper { @@ -1285,9 +1295,7 @@ select::placeholder { } .entity-list .entity-list__entity-info--full-length { - display: block; max-width: 100%; - margin-bottom: 12px; } .entity-list .entity-list__entity-brief { @@ -1327,6 +1335,8 @@ select::placeholder { object-fit: contain; float: left; max-width: 150px; + -o-object-position: top; + object-position: top; } .entity-detail .entity-detail__info { @@ -1396,7 +1406,7 @@ select::placeholder { } .entity-desc .entity-desc__content--folded { - max-height: 200px; + max-height: 202px; } .entity-desc .entity-desc__unfold-button { @@ -1666,6 +1676,77 @@ select::placeholder { color: #00a1cc; } +.track-carousel { + position: relative; + overflow: auto; + scroll-behavior: smooth; + scrollbar-width: none; + margin-top: 5px; +} + +.track-carousel::-webkit-scrollbar { + height: 0px; +} + +.track-carousel__content { + display: -ms-grid; + display: grid; + grid-gap: 16px; + margin: auto; + -webkit-box-sizing: border-box; + box-sizing: border-box; + grid-auto-flow: column; +} + +.track-carousel__track { + width: 10vw; + height: 13vw; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; +} + +.track-carousel__track img { + -o-object-fit: contain; + object-fit: contain; +} + +.track-carousel__track-title { + white-space: nowrap; +} + +.track-carousel__button { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -ms-flex-line-pack: center; + align-content: center; + background: white; + border: none; + padding: 8px; + border-radius: 50%; + outline: 0; + cursor: pointer; + position: absolute; +} + +.track-carousel__button--prev { + top: 50%; + left: 0; + -webkit-transform: translate(50%, -50%); + transform: translate(50%, -50%); +} + +.track-carousel__button--next { + top: 50%; + right: 0; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} + @media (max-width: 575.98px) { .entity-list .entity-list__entity { -webkit-box-orient: vertical; @@ -1739,6 +1820,10 @@ select::placeholder { .review-head .review-head__actions { float: unset; } + .track-carousel__track { + width: 32vw; + height: 40vw; + } } @media (max-width: 991.98px) { diff --git a/common/static/css/boofilsic.min.css b/common/static/css/boofilsic.min.css index 21fe4c81..23f019a0 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'],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} +@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;word-break:keep-all;opacity:0.8;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.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;-o-object-position:top;object-position:top}.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:202px}.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}.track-carousel{position:relative;overflow:auto;scroll-behavior:smooth;scrollbar-width:none;margin-top:5px}.track-carousel::-webkit-scrollbar{height:0px}.track-carousel__content{display:-ms-grid;display:grid;grid-gap:16px;margin:auto;-webkit-box-sizing:border-box;box-sizing:border-box;grid-auto-flow:column}.track-carousel__track{width:10vw;height:13vw;text-align:center;overflow:hidden;text-overflow:ellipsis}.track-carousel__track img{-o-object-fit:contain;object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-line-pack:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute}.track-carousel__button--prev{top:50%;left:0;-webkit-transform:translate(50%, -50%);transform:translate(50%, -50%)}.track-carousel__button--next{top:50%;right:0;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}@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}.track-carousel__track{width:32vw;height:40vw}}@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/sass/_Label.sass b/common/static/sass/_Label.sass index 4db3b9f9..2db9475a 100644 --- a/common/static/sass/_Label.sass +++ b/common/static/sass/_Label.sass @@ -1,7 +1,10 @@ // source label name should match the enum value in `common.models.SourceSiteEnum` -$douban-color: #319840 +$douban-color-primary: #319840 +$douban-color-secondary: white $in-site-color: $color-primary +$spotify-color-primary: #1ed760 +$spotify-color-secondary: black .source-label display: inline @@ -13,9 +16,12 @@ $in-site-color: $color-primary font-size: 1.1rem margin: 3px padding: 1px 3px - padding-top: 2px; - font-weight: lighter; - letter-spacing: 0.1rem; + padding-top: 2px + font-weight: lighter + letter-spacing: 0.1rem + word-break: keep-all + + opacity: 0.8 position: relative; top: -1px; @@ -24,6 +30,12 @@ $in-site-color: $color-primary border-color: $in-site-color color: $in-site-color &.source-label__douban - border-color: $douban-color - color: $douban-color - &.source-label__amazon \ No newline at end of file + border: none + color: $douban-color-secondary + background-color: $douban-color-primary + &.source-label__amazon + &.source-label__spotify + background-color: $spotify-color-primary + color: $spotify-color-secondary + border: none + font-weight: bold \ No newline at end of file diff --git a/common/static/sass/_MainSection.sass b/common/static/sass/_MainSection.sass index 83f642db..52f22e29 100644 --- a/common/static/sass/_MainSection.sass +++ b/common/static/sass/_MainSection.sass @@ -66,9 +66,9 @@ $sub-section-title-margin: 8px position: relative top: 0.52em &--full-length - display: block + // display: block max-width: 100% - margin-bottom: 12px + // margin-bottom: 12px & &__entity-brief margin-top: 8px @@ -102,10 +102,11 @@ $sub-section-title-margin: 8px .entity-detail & &__img - height: 210px; - object-fit: contain; - float: left; - max-width: 150px; + height: 210px + object-fit: contain + float: left + max-width: 150px + object-position: top & &__info float: left @@ -160,7 +161,7 @@ $mark-review-padding-wider: 6px 0 & &__content overflow: hidden &--folded - max-height: 200px + max-height: 202px & &__unfold-button display: flex @@ -375,6 +376,59 @@ $mark-review-padding-wider: 6px 0 &:hover color: $color-primary + +.track-carousel + position: relative + overflow: auto + scroll-behavior: smooth + scrollbar-width: none + margin-top: 5px + // padding: 0 + + &::-webkit-scrollbar + height: 0px + &__content + display: grid + grid-gap: 16px + margin: auto + box-sizing: border-box + grid-auto-flow: column + // grid-template-columns: max-content + &__track + width: 10vw + height: 13vw + // grid-column: 1 + text-align: center + overflow: hidden + text-overflow: ellipsis + & img + object-fit: contain + &__track-title + // word-break: keep-all + // overflow-wrap: anywhere + white-space: nowrap + + &__button + display: flex + justify-content: center + align-content: center + background: white + border: none + padding: 8px + border-radius: 50% + outline: 0 + cursor: pointer + position: absolute + &--prev + top: 50% + left: 0 + transform: translate(50%, -50%) + &--next + top: 50% + right: 0 + transform: translate(-50%, -50%) + + // Small devices (landscape phones, 576px and up) @media (max-width: $small-devices) .entity-list @@ -439,6 +493,11 @@ $mark-review-padding-wider: 6px 0 float: unset & &__actions float: unset + + .track-carousel + &__track + width: 32vw + height: 40vw // Medium devices (tablets, 768px and up) diff --git a/common/templates/common/home.html b/common/templates/common/home.html index 3da7c29e..490a8de1 100644 --- a/common/templates/common/home.html +++ b/common/templates/common/home.html @@ -61,7 +61,7 @@
  • -
    +
    {% trans '在读的书' %}
    @@ -117,7 +117,7 @@
    {% trans '想看的电影/剧集' %}
    - {% if wish_movies_more %} + {% if wish_music_more %} {% trans '更多' %} {% endif %} @@ -139,7 +139,7 @@
    -
    +
    {% trans '在看的电影/剧集' %}
    @@ -191,6 +191,114 @@
    + +
    +
    + {% trans '想听的音乐' %} +
    + {% if wish_music_more %} + {% trans '更多' %} + {% endif %} + + +
    + +
    +
    + {% trans '在听的音乐' %} +
    + {% if do_music_more %} + {% trans '更多' %} + {% endif %} + + +
    + +
    +
    + {% trans '听过的音乐' %} +
    + {% if collect_music_more %} + {% trans '更多' %} + {% endif %} + + +
    + +
    diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html index f362cbd4..e448d35b 100644 --- a/common/templates/common/search_result.html +++ b/common/templates/common/search_result.html @@ -187,7 +187,7 @@ {% if movie.genre %}{% trans '类型' %} {% for genre in movie.get_genre_display %} {{ genre }}{% if not forloop.last %} {% endif %} - {% endfor %} + {% endfor %}/ {% endif %} @@ -196,13 +196,13 @@ {% for actor in movie.actor %} 5 %}style="display: none;" {% endif %}>{{ actor }} {% if forloop.counter <= 5 %} - {% if not forloop.counter == 5 and not forloop.last %} / {% endif %} + {% if not forloop.counter == 5 and not forloop.last %} {% endif %} {% endif %} {% endfor %} {% endif %}

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

    {% for tag_dict in movie.tag_list %} @@ -226,30 +226,37 @@
  • - {% comment %} - - - - - {% endcomment %} + {% if item.category_name|lower == 'album' %} + + + + {% elif item.category_name|lower == 'song' %} + + + + {% endif %}
    - {% comment %} - - {% if item.category_name == 'album' %} + {% if item.category_name|lower == 'album' %} + {% if request.GET.q %} {{ music.title | highlight:request.GET.q }} + {% else %} + {{ music.title }} + {% endif %} - {% elif item.category_name = 'song' %} + {% elif item.category_name|lower == 'song' %} + {% if request.GET.q %} {{ music.title | highlight:request.GET.q }} + {% else %} + {{ music.title }} + {% endif %} {% endif %} - {% endcomment %} - {% if not request.GET.c or request.GET.c != 'music' and request.GET.c != 'book' and request.GET.c != 'music' %} @@ -268,26 +275,40 @@ {% endif %} - {% if music.genre %}{% trans '流派' %} - {{ music.genre }} / + {% if music.artist %}{% trans '艺术家' %} + {% for artist in music.artist %} + {{ artist }} + {% if not forloop.last %} {% endif %} + {% endfor %} + {% endif %} + + {% if music.genre %}/ {% trans '流派' %} + {{ music.genre }} {% endif %} - {% if music.release_date %} {% trans '发行日期' %} + {% 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 %} - + + + {% if music.brief %}

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

    + {% elif music.category_name|lower == 'album' %} +

    + {% trans '曲目:' %}{{ music.track_list }} +

    + {% else %} + +

    + {% trans '所属专辑:' %}{{ music.album }} +

    + {% endif %} +
    {% for tag_dict in music.tag_list %} {% for k, v in tag_dict.items %} diff --git a/common/templates/partial/_footer.html b/common/templates/partial/_footer.html index ff15fcb6..375fb522 100644 --- a/common/templates/partial/_footer.html +++ b/common/templates/partial/_footer.html @@ -6,7 +6,7 @@ Github 支持的网站 - Version 0.2.0 + Version 0.3.0
    \ No newline at end of file diff --git a/common/templates/widgets/key_value.html b/common/templates/widgets/key_value.html deleted file mode 100644 index 80082bf8..00000000 --- a/common/templates/widgets/key_value.html +++ /dev/null @@ -1,26 +0,0 @@ - -
    - {% if widget.value != None %} - - {% for k, v in widget.keyvalue_pairs.items %} - - {% endfor %} - - {% endif %} -
    - - \ No newline at end of file diff --git a/common/templatetags/highlight.py b/common/templatetags/highlight.py index 32af3aa2..a2276800 100644 --- a/common/templatetags/highlight.py +++ b/common/templatetags/highlight.py @@ -3,11 +3,15 @@ from django.utils.safestring import mark_safe from django.template.defaultfilters import stringfilter from django.utils.html import format_html +import re register = template.Library() @register.filter @stringfilter def highlight(text, search): - highlighted = text.replace(search, '{}'.format(search)) - return mark_safe(highlighted) \ No newline at end of file + to_be_replaced_words = set(re.findall(search, text, flags=re.IGNORECASE)) + + for word in to_be_replaced_words: + text = text.replace(word, f'{word}') + return mark_safe(text) diff --git a/common/views.py b/common/views.py index 1ab37c6a..7cbbc0c1 100644 --- a/common/views.py +++ b/common/views.py @@ -8,12 +8,11 @@ from django.utils.translation import gettext_lazy as _ from django.core.paginator import Paginator from django.core.validators import URLValidator from django.core.exceptions import ValidationError, ObjectDoesNotExist -from django.core.files.uploadedfile import SimpleUploadedFile 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 music.models import Album, Song, AlbumMark, SongMark from users.models import Report, User from mastodon.decorators import mastodon_request_included from common.models import MarkStatusEnum @@ -28,6 +27,8 @@ BOOKS_PER_SET = 5 # how many movies have in each set at the home page MOVIES_PER_SET = 5 +MUSIC_PER_SET = 5 + # how many items are showed in one search result page ITEMS_PER_PAGE = 20 @@ -43,6 +44,8 @@ logger = logging.getLogger(__name__) def home(request): if request.method == 'GET': + # really shitty code here + unread_announcements = Announcement.objects.filter( pk__gt=request.user.read_announcement_index).order_by('-pk') try: @@ -77,6 +80,28 @@ def home(request): status=MarkStatusEnum.COLLECT).order_by("-edited_time") collect_movies_more = True if collect_movie_marks.count() > MOVIES_PER_SET else False + do_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \ + + list(request.user.user_albummarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) + do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False + do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] + + wish_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \ + + list(request.user.user_albummarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) + wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False + wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] + + collect_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \ + + list(request.user.user_albummarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) + collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False + collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] + + for mark in do_music_marks + wish_music_marks + collect_music_marks: + # for template convenience + if mark.__class__ == AlbumMark: + mark.type = "album" + else: + mark.type = "song" + reports = Report.objects.order_by('-submitted_time').filter(is_read=False) # reports = Report.objects.latest('submitted_time').filter(is_read=False) @@ -96,6 +121,12 @@ def home(request): 'do_movies_more': do_movies_more, 'wish_movies_more': wish_movies_more, 'collect_movies_more': collect_movies_more, + 'do_music_marks': do_music_marks, + 'wish_music_marks': wish_music_marks, + 'collect_music_marks': collect_music_marks, + 'do_music_more': do_music_more, + 'wish_music_more': wish_music_more, + 'collect_music_more': collect_music_more, 'reports': reports, 'unread_announcements': unread_announcements, } @@ -125,20 +156,25 @@ def search(request): # category, book/movie/music etc category = request.GET.get("c", default='').strip().lower() + # keywords, seperated by blank space + keywords = request.GET.get("q", default='').strip().split() + # tag, when tag is provided there should be no keywords , for now + tag = request.GET.get("tag", default='') - def book_param_handler(): - q = Q() - query_args = [] + # white space string, empty query + if not (keywords or tag): + return [] + def book_param_handler(**kwargs): # keywords - keywords = request.GET.get("q", default='').strip() + keywords = kwargs.get('keywords') # tag - tag = request.GET.get("tag", default='') + tag = kwargs.get('tag') - if not (keywords or tag): - return [] + query_args = [] + q = Q() - for keyword in [keywords]: + for keyword in keywords: q = q | Q(title__icontains=keyword) q = q | Q(subtitle__icontains=keyword) q = q | Q(orig_title__icontains=keyword) @@ -171,19 +207,16 @@ def search(request): ordered_queryset = list(queryset) return ordered_queryset - def movie_param_handler(): - q = Q() - query_args = [] - + def movie_param_handler(**kwargs): # keywords - keywords = request.GET.get("q", default='').strip() + keywords = kwargs.get('keywords') # tag - tag = request.GET.get("tag", default='') + tag = kwargs.get('tag') - if not (keywords or tag): - return [] + query_args = [] + q = Q() - for keyword in [keywords]: + for keyword in keywords: q = q | Q(title__icontains=keyword) q = q | Q(other_title__icontains=keyword) q = q | Q(orig_title__icontains=keyword) @@ -215,33 +248,51 @@ def search(request): ordered_queryset = list(queryset) return ordered_queryset - def music_param_handler(): - q = Q() - query_args = [] - + def music_param_handler(**kwargs): # keywords - keywords = request.GET.get("q", default='').strip() + keywords = kwargs.get('keywords') # tag - tag = request.GET.get("tag", default='') + tag = kwargs.get('tag') - if not (keywords or tag): - return [] + query_args = [] + q = Q() # search albums - for keyword in [keywords]: + for keyword in keywords: q = q | Q(title__icontains=keyword) + q = q | Q(artist__icontains=keyword) if tag: q = q & Q(album_tags__content__iexact=tag) query_args.append(q) - queryset = Album.objects.filter(*query_args).distinct() + album_queryset = Album.objects.filter(*query_args).distinct() + + # extra query args for songs + q = Q() + for keyword in keywords: + q = q | Q(album__title__icontains=keyword) + q = q | Q(title__icontains=keyword) + q = q | Q(artist__icontains=keyword) + if tag: + q = q & Q(song_tags__content__iexact=tag) + query_args.clear() + query_args.append(q) + song_queryset = Song.objects.filter(*query_args).distinct() + queryset = list(album_queryset) + list(song_queryset) def calculate_similarity(music): if keywords: # search by name similarity, n = 0, 0 + artist_dump = ' '.join(music.artist) for keyword in keywords: - similarity += SequenceMatcher(None, keyword, music.title).quick_ratio() + if music.__class__ == Album: + similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \ + + 1/2 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() + elif music.__class__ == Song: + similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \ + + 1/6 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() \ + + 1/6 * SequenceMatcher(None, keyword, music.album.title).quick_ratio() n += 1 music.similarity = similarity / n elif tag: @@ -256,10 +307,10 @@ def search(request): 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() + def all_param_handler(**kwargs): + book_queryset = book_param_handler(**kwargs) + movie_queryset = movie_param_handler(**kwargs) + music_queryset = music_param_handler(**kwargs) ordered_queryset = sorted( book_queryset + movie_queryset + music_queryset, key=operator.attrgetter('similarity'), @@ -276,9 +327,15 @@ def search(request): } try: - queryset = param_handler[category]() + queryset = param_handler[category]( + keywords=keywords, + tag=tag + ) except KeyError as e: - queryset = param_handler['all']() + queryset = param_handler['all']( + keywords=keywords, + tag=tag + ) paginator = Paginator(queryset, ITEMS_PER_PAGE) page_number = request.GET.get('page', default=1) items = paginator.get_page(page_number) @@ -333,17 +390,11 @@ def jump_or_scrape(request, url): except ObjectDoesNotExist: # scrape if not exists try: - scraped_entity, raw_cover = scraper.scrape(url) - except: + scraper.scrape(url) + form = scraper.save(request_user=request.user) + except Exception as e: + logger.error(f"Scrape Failed URL: {url}") + logger.error("Expections during saving scraped data:", exc_info=e) return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")}) - scraped_cover = { - 'cover': SimpleUploadedFile('temp.jpg', raw_cover)} - form = scraper.form_class(scraped_entity, scraped_cover) - if form.is_valid(): - form.instance.last_editor = request.user - form.save() - return redirect(form.instance) - else: - msg = _("爬取数据失败😫") - logger.error(str(form.errors)) - return render(request, 'common/error.html', {'msg': msg}) + return redirect(form.instance) + diff --git a/movies/templates/movies/delete_review.html b/movies/templates/movies/delete_review.html index c81e0e0c..a16e6c49 100644 --- a/movies/templates/movies/delete_review.html +++ b/movies/templates/movies/delete_review.html @@ -37,8 +37,7 @@ {% if review.is_private %} + viewBox="0 0 20 20"> diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html index f4e7a400..31392376 100644 --- a/movies/templates/movies/detail.html +++ b/movies/templates/movies/detail.html @@ -225,20 +225,17 @@
  • + {% if movie.brief %}
    {% trans '简介' %}
    - {% if movie.brief %}

    {{ movie.brief | linebreaksbr }}

    展开全部
    - {% else %} -
    {% trans '暂无简介' %}
    - {% endif %} -
    + {% endif %}
    @@ -261,7 +258,7 @@ {% endif %} {% if others_mark.is_private %} - + {% endif %} {{ others_mark.edited_time }} {% if others_mark.text %} @@ -290,7 +287,7 @@
  • {{ others_review.owner.username }} {% if others_review.is_private %} - + {% endif %} {{ others_review.edited_time }} {{ others_review.title }} @@ -318,7 +315,7 @@ {% endif %} {% endif %} {% if mark.is_private %} - + {% endif %} {% trans '修改' %} @@ -366,7 +363,7 @@ {% trans '我的评论' %} {% if review.is_private %} - + {% endif %} diff --git a/movies/templates/movies/mark_list.html b/movies/templates/movies/mark_list.html index e661c479..8b0d68a6 100644 --- a/movies/templates/movies/mark_list.html +++ b/movies/templates/movies/mark_list.html @@ -47,8 +47,7 @@ data-rating-score="{{ mark.rating | floatformat:"0" }}"> {% endif %} {% if mark.is_private %} - + diff --git a/movies/templates/movies/review_detail.html b/movies/templates/movies/review_detail.html index b93401fd..03986460 100644 --- a/movies/templates/movies/review_detail.html +++ b/movies/templates/movies/review_detail.html @@ -39,8 +39,7 @@ {{ review.title }} {% if review.is_private %} - + diff --git a/movies/templates/movies/review_list.html b/movies/templates/movies/review_list.html index 25d34afb..88aa3046 100644 --- a/movies/templates/movies/review_list.html +++ b/movies/templates/movies/review_list.html @@ -42,7 +42,7 @@ {{ review.owner.username }} {% if review.is_private %} - + {% endif %} {{ review.edited_time }} diff --git a/movies/views.py b/movies/views.py index dc2ef6e2..f63cdeef 100644 --- a/movies/views.py +++ b/movies/views.py @@ -8,7 +8,6 @@ from django.db import IntegrityError, transaction from django.db.models import Count from django.utils import timezone from django.core.paginator import Paginator -from django.core.files.uploadedfile import SimpleUploadedFile from mastodon import mastodon_request_included from mastodon.api import check_visibility, post_toot, TootVisibilityEnum from mastodon.utils import rating_to_emoji diff --git a/music/forms.py b/music/forms.py index 24d423b7..3eec5646 100644 --- a/music/forms.py +++ b/music/forms.py @@ -19,6 +19,7 @@ class SongForm(forms.ModelForm): id = forms.IntegerField(required=False, widget=forms.HiddenInput()) other_info = JSONField(required=False, label=_("其他信息")) + duration = DurationField(required=False) class Meta: model = Song @@ -100,6 +101,7 @@ class AlbumForm(forms.ModelForm): id = forms.IntegerField(required=False, widget=forms.HiddenInput()) other_info = JSONField(required=False, label=_("其他信息")) + duration = DurationField(required=False) class Meta: model = Album diff --git a/music/models.py b/music/models.py index 8e489c7c..6becf007 100644 --- a/music/models.py +++ b/music/models.py @@ -51,7 +51,7 @@ class Album(Entity): default='', max_length=100) company = postgres.ArrayField( models.CharField(blank=True, - default='', max_length=100), + default='', max_length=500), null=True, blank=True, default=list, @@ -96,7 +96,7 @@ class Song(Entity): genre = models.CharField(_("流派"), blank=True, default='', max_length=100) album = models.ForeignKey( - Album, models.CASCADE, "album_songs", null=True, blank=True, verbose_name=_("所属专辑")) + Album, models.SET_NULL, "album_songs", null=True, blank=True, verbose_name=_("所属专辑")) def __str__(self): return self.title diff --git a/music/templates/music/album_detail.html b/music/templates/music/album_detail.html index 50b287cf..29e66c95 100644 --- a/music/templates/music/album_detail.html +++ b/music/templates/music/album_detail.html @@ -150,19 +150,16 @@
  • + {% if album.brief %}
    {% trans '简介' %}
    - {% if album.brief %}

    {{ album.brief | linebreaksbr }}

    展开全部
    - - {% else %} -
    {% trans '暂无简介' %}
    - {% endif %}
    + {% endif %} {% if album.track_list %}
    @@ -178,11 +175,32 @@
    {% trans '关联单曲' %}
    - {% for song in album.album_songs.all %} -
    - {{ song }} + - {% endfor %}
    {% endif %} @@ -204,7 +222,7 @@ {% endif %} {% if others_mark.is_private %} - + {% endif %} {{ others_mark.edited_time }} {% if others_mark.text %} @@ -229,7 +247,7 @@
  • {{ others_review.owner.username }} {% if others_review.is_private %} - + {% endif %} {{ others_review.edited_time }} {{ others_review.title }} @@ -257,7 +275,7 @@ {% endif %} {% endif %} {% if mark.is_private %} - + {% endif %} {% trans '修改' %} @@ -285,9 +303,9 @@
    {% trans '标记这部作品' %}
    - - - + + +
    {% endif %} @@ -300,7 +318,7 @@ {% trans '我的评论' %} {% if review.is_private %} - + {% endif %} diff --git a/music/templates/music/album_mark_list.html b/music/templates/music/album_mark_list.html index 8832f71b..a971e472 100644 --- a/music/templates/music/album_mark_list.html +++ b/music/templates/music/album_mark_list.html @@ -48,8 +48,7 @@ {% endif %} {% if mark.is_private %} + viewBox="0 0 20 20"> diff --git a/music/templates/music/album_review_detail.html b/music/templates/music/album_review_detail.html index 7ba9da40..c8fd1666 100644 --- a/music/templates/music/album_review_detail.html +++ b/music/templates/music/album_review_detail.html @@ -39,8 +39,7 @@ {{ review.title }} {% if review.is_private %} - + diff --git a/music/templates/music/album_review_list.html b/music/templates/music/album_review_list.html index 23edaa48..d815dd20 100644 --- a/music/templates/music/album_review_list.html +++ b/music/templates/music/album_review_list.html @@ -44,8 +44,7 @@ class="entity-reviews__owner-link">{{ review.owner.username }} {% if review.is_private %} + viewBox="0 0 20 20"> diff --git a/music/templates/music/create_update_album.html b/music/templates/music/create_update_album.html index f2a12288..841eab79 100644 --- a/music/templates/music/create_update_album.html +++ b/music/templates/music/create_update_album.html @@ -31,7 +31,10 @@ {% for field in form %} {% if field.name == 'release_date' %} {{ field.label_tag }} - + + + {% else %} {% if field.name != 'id' %} {{ field.label_tag }} diff --git a/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html index ed68d489..a694c891 100644 --- a/music/templates/music/create_update_song.html +++ b/music/templates/music/create_update_song.html @@ -36,7 +36,8 @@ {% for field in form %} {% if field.name == 'release_date' %} {{ field.label_tag }} - + {% else %} {% if field.name != 'id' %} {{ field.label_tag }} diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html index f31c71a3..7deaaa49 100644 --- a/music/templates/music/delete_album_review.html +++ b/music/templates/music/delete_album_review.html @@ -37,8 +37,7 @@ {% if review.is_private %} + viewBox="0 0 20 20"> diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html index 84019488..9e515b02 100644 --- a/music/templates/music/delete_song_review.html +++ b/music/templates/music/delete_song_review.html @@ -37,8 +37,7 @@ {% if review.is_private %} + viewBox="0 0 20 20"> diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html index 068c277c..841514d7 100644 --- a/music/templates/music/song_detail.html +++ b/music/templates/music/song_detail.html @@ -139,20 +139,18 @@
  • + {% if song.brief %}
    {% trans '简介' %}
    - {% if song.brief %}

    {{ song.brief | linebreaksbr }}

    展开全部
    - {% else %} -
    {% trans '暂无简介' %}
    - {% endif %} - +
    + {% endif %}
    @@ -171,7 +169,7 @@ {% endif %} {% if others_mark.is_private %} - + {% endif %} {{ others_mark.edited_time }} {% if others_mark.text %} @@ -196,7 +194,7 @@
  • {{ others_review.owner.username }} {% if others_review.is_private %} - + {% endif %} {{ others_review.edited_time }} {{ others_review.title }} @@ -224,7 +222,7 @@ {% endif %} {% endif %} {% if mark.is_private %} - + {% endif %} {% trans '修改' %} @@ -252,9 +250,9 @@
    {% trans '标记这部作品' %}
    - - - + + +
    {% endif %} @@ -267,7 +265,7 @@ {% trans '我的评论' %} {% if review.is_private %} - + {% endif %} diff --git a/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html index 0c1b8fae..07dbae3b 100644 --- a/music/templates/music/song_mark_list.html +++ b/music/templates/music/song_mark_list.html @@ -49,8 +49,7 @@ {% endif %} {% if mark.is_private %} + viewBox="0 0 20 20"> diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html index 806d56d5..14c43fc3 100644 --- a/music/templates/music/song_review_detail.html +++ b/music/templates/music/song_review_detail.html @@ -39,8 +39,7 @@ {{ review.title }} {% if review.is_private %} - + diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html index c231ec08..b204767e 100644 --- a/music/templates/music/song_review_list.html +++ b/music/templates/music/song_review_list.html @@ -42,7 +42,7 @@ {{ review.owner.username }} {% if review.is_private %} - + {% endif %} {{ review.edited_time }} diff --git a/music/views.py b/music/views.py index 5065f15f..993dae35 100644 --- a/music/views.py +++ b/music/views.py @@ -7,7 +7,6 @@ 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 @@ -158,13 +157,13 @@ def retrieve_song(request, id): seconds = x % 60 x //= 60 if x == 0: - return f"{seconds}" + return f"{seconds}秒" minutes = x % 60 x //= 60 if x == 0: - return f"{minutes}:{seconds}" + return f"{minutes}分{seconds}秒" hours = x % 24 - return f"{hours}:{minutes}:{seconds}" + return f"{hours}时{minutes}分{seconds}秒" song.get_duration_display = ms_to_readable(song.duration) @@ -730,13 +729,13 @@ def retrieve_album(request, id): seconds = x % 60 x //= 60 if x == 0: - return f"{seconds}" + return f"{seconds}秒" minutes = x % 60 x //= 60 if x == 0: - return f"{minutes}:{seconds}" + return f"{minutes}分{seconds}秒" hours = x % 24 - return f"{hours}:{minutes}:{seconds}" + return f"{hours}时{minutes}分{seconds}秒" album.get_duration_display = ms_to_readable(album.duration) diff --git a/users/templates/users/book_list.html b/users/templates/users/book_list.html index 3790556d..5694ed22 100644 --- a/users/templates/users/book_list.html +++ b/users/templates/users/book_list.html @@ -118,8 +118,7 @@ data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"> {% endif %} {% if mark.is_private %} - + diff --git a/users/templates/users/movie_list.html b/users/templates/users/movie_list.html index 09834a6b..8e0ff739 100644 --- a/users/templates/users/movie_list.html +++ b/users/templates/users/movie_list.html @@ -122,8 +122,7 @@ data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"> {% endif %} {% if mark.is_private %} - + diff --git a/users/templates/users/music_list.html b/users/templates/users/music_list.html new file mode 100644 index 00000000..8ed992ff --- /dev/null +++ b/users/templates/users/music_list.html @@ -0,0 +1,289 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} + + + + + + + {% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }} + + + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + +
    +
    + {{ user.username }}{{ list_title }} +
    +
    +
      + + {% for mark in marks %} + + {% with mark.music as music %} + +
    • +
      + {% if music.category_name|lower == 'album' %} + + + + {% elif music.category_name|lower == 'song' %} + + + + {% endif %} +
      +
      +
      + {% if music.category_name|lower == 'album' %} + + {{ music.title }} + + {% elif music.category_name|lower == 'song' %} + + {{ music.title }} + + {% endif %} + + {{ music.get_source_site_display }} + +
      + + {% if music.artist %}{% trans '艺术家' %} + {% for artist in music.artist %} + {{ artist }} + {% if not forloop.last %} {% endif %} + {% endfor %} + {% endif %} + + {% if music.genre %}/ {% trans '流派' %} + {{ music.genre }} + {% endif %} + + {% if music.release_date %}/ {% trans '发行日期' %} + {{ music.release_date }} + {% endif %} + + {% if music.brief %} +

      + {{ music.brief }} +

      + {% elif music.category_name|lower == 'album' %} +

      + {% trans '曲目:' %}{{ music.track_list }} +

      + {% else %} + +

      + {% trans '所属专辑:' %}{{ music.album }} +

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

        {{ mark.text }}

        + {% endif %} +
      • +
      +
      +
      + +
    • + + {% endwith %} + + {% empty %} +
      {% trans '无结果' %}
      + {% endfor %} + + + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + + + + + +
    +
    +
    + +
    +
    + {% trans '关注的人' %} +
    + {% trans '更多' %} + +
    + +
    +
    + {% trans '被他们关注' %} +
    + {% trans '更多' %} + +
    + +
    +
    +
    + +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + + + + {% if user == request.user %} + + {% else %} + + {% endif %} + + + + + + + diff --git a/users/templates/users/register.html b/users/templates/users/register.html index d0e6056b..9b750a66 100644 --- a/users/templates/users/register.html +++ b/users/templates/users/register.html @@ -19,7 +19,7 @@
    -

    欢迎来到NiceDB书影音(其实现在只有书汗电影)!

    +

    欢迎来到NiceDB书影音!

    NiceDB书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。 这里仍是一片处女地,丰富的内容需要大家共同创造。 diff --git a/users/urls.py b/users/urls.py index cf79b2b3..4b8adb7d 100644 --- a/users/urls.py +++ b/users/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('/following/', following, name='following'), path('/book//', book_list, name='book_list'), path('/movie//', movie_list, name='movie_list'), + path('/music//', music_list, name='music_list'), path('/', home, name='home'), path('/followers/', followers, name='followers'), path('/following/', following, name='following'), diff --git a/users/views.py b/users/views.py index ca7e0191..b9918405 100644 --- a/users/views.py +++ b/users/views.py @@ -17,8 +17,10 @@ from common.models import MarkStatusEnum from common.utils import PageLinksGenerator from books.models import * from movies.models import * +from music.models import * from books.forms import BookMarkStatusTranslator from movies.forms import MovieMarkStatusTranslator +from music.forms import MusicMarkStatusTranslator from mastodon.models import MastodonApplication @@ -380,7 +382,7 @@ def book_list(request, id, status): mark.book.tag_list = mark.book.get_tags_manager().values('content').annotate( tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) - list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书")) + list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的书")) return render( request, 'users/book_list.html', @@ -449,7 +451,7 @@ def movie_list(request, id, status): mark.movie.tag_list = mark.movie.get_tags_manager().values('content').annotate( tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) - list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集")) + list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的电影和剧集")) return render( request, 'users/movie_list.html', @@ -463,6 +465,86 @@ def movie_list(request, id, status): return HttpResponseBadRequest() +@mastodon_request_included +@login_required +def music_list(request, id, status): + if request.method == 'GET': + if not status.upper() in MarkStatusEnum.names: + return HttpResponseBadRequest() + + if isinstance(id, str): + try: + username = id.split('@')[0] + site = id.split('@')[1] + except IndexError as e: + return HttpResponseBadRequest("Invalid user id") + query_kwargs = {'username': username, 'mastodon_site': site} + elif isinstance(id, int): + query_kwargs = {'pk': id} + try: + user = User.objects.get(**query_kwargs) + except ObjectDoesNotExist: + msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!") + sec_msg = _("目前只开放本站用户注册") + return render( + request, + 'common/error.html', + { + 'msg': msg, + 'secondary_msg': sec_msg, + } + ) + if not user == request.user: + # mastodon request + relation = get_relationship(request.user, user, request.session['oauth_token'])[0] + if relation['blocked_by']: + msg = _("你没有访问TA主页的权限😥") + return render( + request, + 'common/error.html', + { + 'msg': msg, + } + ) + queryset = list(AlbumMark.get_available_by_user(user, relation['following']).filter( + status=MarkStatusEnum[status.upper()])) \ + + list(SongMark.get_available_by_user(user, relation['following']).filter( + status=MarkStatusEnum[status.upper()])) + + user.target_site_id = get_cross_site_id( + user, request.user.mastodon_site, request.session['oauth_token']) + else: + queryset = list(AlbumMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) \ + + list(SongMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) + queryset = sorted(queryset, key=lambda e: e.edited_time, reverse=True) + paginator = Paginator(queryset, ITEMS_PER_PAGE) + page_number = request.GET.get('page', default=1) + marks = paginator.get_page(page_number) + for mark in marks: + if mark.__class__ == AlbumMark: + mark.music = mark.album + mark.music.tag_list = mark.album.get_tags_manager().values('content').annotate( + tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] + elif mark.__class__ == SongMark: + mark.music = mark.song + mark.music.tag_list = mark.song.get_tags_manager().values('content').annotate( + tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] + + marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) + list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的音乐")) + return render( + request, + 'users/music_list.html', + { + 'marks': marks, + 'user': user, + 'list_title' : list_title, + } + ) + else: + return HttpResponseBadRequest() + + @login_required def report(request): if request.method == 'GET':