finish music part
This commit is contained in:
parent
e5bc7f55c0
commit
575123f848
46 changed files with 1323 additions and 265 deletions
|
@ -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)
|
||||
|
|
|
@ -120,20 +120,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if book.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
{% if book.brief %}
|
||||
|
||||
<p class="entity-desc__content">{{ book.brief | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div>{% trans '暂无简介' %}</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if book.contents %}
|
||||
<div class="entity-desc" id="contents">
|
||||
|
@ -160,7 +158,7 @@
|
|||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
|
@ -184,7 +182,7 @@
|
|||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
|
@ -212,7 +210,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="mark-panel__actions">
|
||||
<a href="" class="edit">{% trans '修改' %}</a>
|
||||
|
@ -255,7 +253,7 @@
|
|||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
|
||||
<span class="review-panel__actions">
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
112
common/forms.py
112
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
|
||||
#############################
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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) {
|
||||
|
|
2
common/static/css/boofilsic.min.css
vendored
2
common/static/css/boofilsic.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
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
|
|
@ -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)
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="bookWish">
|
||||
<div class="entity-sort" id="bookDo">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '在读的书' %}
|
||||
</h5>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<h5 class="entity-sort__label">
|
||||
{% trans '想看的电影/剧集' %}
|
||||
</h5>
|
||||
{% if wish_movies_more %}
|
||||
{% if wish_music_more %}
|
||||
<a href="{% url 'users:movie_list' user.id 'wish' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
@ -139,7 +139,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="movieWish">
|
||||
<div class="entity-sort" id="movieDo">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '在看的电影/剧集' %}
|
||||
</h5>
|
||||
|
@ -191,6 +191,114 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="entity-sort" id="musicWish">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '想听的音乐' %}
|
||||
</h5>
|
||||
{% if wish_music_more %}
|
||||
<a href="{% url 'users:music_list' user.id 'wish' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for wish_music_mark in wish_music_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
|
||||
|
||||
{% if wish_music_mark.type == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' wish_music_mark.album.id %}">
|
||||
<img src="{{ wish_music_mark.album.cover.url }}"
|
||||
alt="{{wish_music_mark.album.title}}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{wish_music_mark.album.title}}">
|
||||
{{ wish_music_mark.album.title }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'music:retrieve_song' wish_music_mark.song.id %}">
|
||||
<img src="{{ wish_music_mark.song.cover.url }}" alt="{{wish_music_mark.song.title}}"
|
||||
class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{wish_music_mark.song.title}}">
|
||||
{{ wish_music_mark.song.title }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="musicDo">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '在听的音乐' %}
|
||||
</h5>
|
||||
{% if do_music_more %}
|
||||
<a href="{% url 'users:music_list' user.id 'do' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for do_music_mark in do_music_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
{% if do_music_mark.type == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' do_music_mark.album.id %}">
|
||||
<img src="{{ do_music_mark.album.cover.url }}"
|
||||
alt="{{do_music_mark.album.title}}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{do_music_mark.album.title}}">
|
||||
{{ do_music_mark.album.title }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'music:retrieve_song' do_music_mark.song.id %}">
|
||||
<img src="{{ do_music_mark.song.cover.url }}"
|
||||
alt="{{do_music_mark.song.title}}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{do_music_mark.song.title}}">
|
||||
{{ do_music_mark.song.title }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="musicCollect">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '听过的音乐' %}
|
||||
</h5>
|
||||
{% if collect_music_more %}
|
||||
<a href="{% url 'users:music_list' user.id 'collect' %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for collect_music_mark in collect_music_marks %}
|
||||
<li class="entity-sort__entity">
|
||||
{% if collect_music_mark.type == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' collect_music_mark.album.id %}">
|
||||
<img src="{{ collect_music_mark.album.cover.url }}"
|
||||
alt="{{collect_music_mark.album.title}}" class="entity-sort__entity-img">
|
||||
<span class="entity-sort__entity-name"
|
||||
title="{{collect_music_mark.album.title}}">{{ collect_music_mark.album.title }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'music:retrieve_song' collect_music_mark.song.id %}">
|
||||
<img src="{{ collect_music_mark.song.cover.url }}"
|
||||
alt="{{collect_music_mark.song.title}}" class="entity-sort__entity-img">
|
||||
<span class="entity-sort__entity-name"
|
||||
title="{{collect_music_mark.song.title}}">{{ collect_music_mark.song.title }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -187,7 +187,7 @@
|
|||
{% if movie.genre %}{% trans '类型' %}
|
||||
{% for genre in movie.get_genre_display %}
|
||||
{{ genre }}{% if not forloop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}/
|
||||
{% endif %}
|
||||
|
||||
</span>
|
||||
|
@ -196,13 +196,13 @@
|
|||
{% for actor in movie.actor %}
|
||||
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
|
||||
{% 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 %}
|
||||
</span>
|
||||
<p class="entity-list__entity-brief">
|
||||
{{ movie.brief | truncate:170 }}
|
||||
{{ movie.brief }}
|
||||
</p>
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in movie.tag_list %}
|
||||
|
@ -226,30 +226,37 @@
|
|||
<li class="entity-list__entity">
|
||||
<div class="entity-list__entity-img-wrapper">
|
||||
|
||||
{% comment %}
|
||||
|
||||
<a href="{% url 'music:retrieve' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% endcomment %}
|
||||
{% if item.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% elif item.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="entity-list__entity-text">
|
||||
<div class="entity-list__entity-title">
|
||||
|
||||
{% comment %}
|
||||
|
||||
{% if item.category_name == 'album' %}
|
||||
{% if item.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
|
||||
{% if request.GET.q %}
|
||||
{{ music.title | highlight:request.GET.q }}
|
||||
{% else %}
|
||||
{{ music.title }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% elif item.category_name = 'song' %}
|
||||
{% elif item.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
|
||||
{% if request.GET.q %}
|
||||
{{ music.title | highlight:request.GET.q }}
|
||||
{% else %}
|
||||
{{ music.title }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% 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 %}
|
||||
|
||||
<span class="entity-list__entity-info ">
|
||||
{% if music.genre %}{% trans '流派' %}
|
||||
{{ music.genre }} /
|
||||
{% if music.artist %}{% trans '艺术家' %}
|
||||
{% for artist in music.artist %}
|
||||
<span>{{ artist }}</span>
|
||||
{% 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 %}
|
||||
</span>
|
||||
<span class="entity-list__entity-info entity-list__entity-info--full-length">
|
||||
{% if music.artist %}{% trans '艺术家' %}
|
||||
{% for artist in music.artist %}
|
||||
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ artist }}</span>
|
||||
{% if forloop.counter <= 5 %} {% if not forloop.counter == 5 and not forloop.last %} / {% endif %} {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
</span>
|
||||
|
||||
{% if music.brief %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{{ music.brief | truncate:170 }}
|
||||
{{ music.brief }}
|
||||
</p>
|
||||
{% elif music.category_name|lower == 'album' %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '曲目:' %}{{ music.track_list }}
|
||||
</p>
|
||||
{% else %}
|
||||
<!-- song -->
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '所属专辑:' %}{{ music.album }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in music.tag_list %}
|
||||
{% for k, v in tag_dict.items %}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic" id="githubLink">Github</a>
|
||||
<a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助项目</a>
|
||||
<a class="footer__link" target="_blank" href="/announcement/supported-sites/" id="supported-sites">支持的网站</a>
|
||||
<a class="footer__link" href="javascript:void();" id="version">Version 0.2.0</a>
|
||||
<a class="footer__link" href="javascript:void();" id="version">Version 0.3.0</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
|
@ -1,26 +0,0 @@
|
|||
<style>
|
||||
.widget-value-key-input input:nth-child(odd) {
|
||||
width: 49%;
|
||||
margin-right: 1%;
|
||||
}
|
||||
.widget-value-key-input input:nth-child(even) {
|
||||
width: 49%;
|
||||
margin-left: 1%;
|
||||
}
|
||||
</style>
|
||||
<div class="widget-value-key-input" name='{{ widget.name }}'{% include "django/forms/widgets/attrs.html" %}>
|
||||
{% if widget.value != None %}
|
||||
|
||||
{% for k, v in widget.keyvalue_pairs.items %}
|
||||
<input type="text" value="{{ k }}" ><input type="text" value="{{ v }}">
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="text" class="widget-value-key-input-data" hidden name="{{ widget.name }}">
|
||||
<script>
|
||||
keyValueInput(
|
||||
$(".widget-value-key-input[name='{{ widget.name }}']"),
|
||||
$(".widget-value-key-input-data[name='{{ widget.name }}']")
|
||||
);
|
||||
</script>
|
|
@ -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, '<span class="highlight">{}</span>'.format(search))
|
||||
return mark_safe(highlighted)
|
||||
to_be_replaced_words = set(re.findall(search, text, flags=re.IGNORECASE))
|
||||
|
||||
for word in to_be_replaced_words:
|
||||
text = text.replace(word, f'<span class="highlight">{word}</span>')
|
||||
return mark_safe(text)
|
||||
|
|
149
common/views.py
149
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)
|
||||
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -225,20 +225,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if movie.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
{% if movie.brief %}
|
||||
|
||||
<p class="entity-desc__content">{{ movie.brief | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div>{% trans '暂无简介' %}</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="entity-marks">
|
||||
|
@ -261,7 +258,7 @@
|
|||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
|
@ -290,7 +287,7 @@
|
|||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
|
@ -318,7 +315,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="mark-panel__actions">
|
||||
<a href="" class="edit">{% trans '修改' %}</a>
|
||||
|
@ -366,7 +363,7 @@
|
|||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
|
||||
<span class="review-panel__actions">
|
||||
|
|
|
@ -47,8 +47,7 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -39,8 +39,7 @@
|
|||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -150,19 +150,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if album.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
{% if album.brief %}
|
||||
|
||||
<p class="entity-desc__content">{{ album.brief | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div>{% trans '暂无简介' %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if album.track_list %}
|
||||
<div class="entity-desc" id="description">
|
||||
|
@ -178,11 +175,32 @@
|
|||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '关联单曲' %}</h5>
|
||||
<!-- TODO: Limit the maximum -->
|
||||
{% for song in album.album_songs.all %}
|
||||
<div>
|
||||
<a href="{% url 'music:retrieve_song' song.id %}">{{ song }}</a>
|
||||
<div class="track-carousel">
|
||||
<div class="track-carousel__content">
|
||||
{% for song in album.album_songs.all %}
|
||||
<div class="track-carousel__track">
|
||||
<a href="{% url 'music:retrieve_song' song.id %}">
|
||||
<img src="{{ song.cover.url }}" alt="{{ song }}" class="track-carousel__track-image">
|
||||
<span class="track-carousel__track-title">
|
||||
{{ song }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- <button class="track-carousel__button track-carousel__button--prev">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M15.61 7.41L14.2 6l-6 6 6 6 1.41-1.41L11.03 12l4.58-4.59z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="track-carousel__button track-carousel__button--next">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z" />
|
||||
</svg>
|
||||
</button> -->
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -204,7 +222,7 @@
|
|||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
|
@ -229,7 +247,7 @@
|
|||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'music:retrieve_album_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
|
@ -257,7 +275,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="mark-panel__actions">
|
||||
<a href="" class="edit">{% trans '修改' %}</a>
|
||||
|
@ -285,9 +303,9 @@
|
|||
<div class="action-panel" id="addMarkPanel">
|
||||
<div class="action-panel__label">{% trans '标记这部作品' %}</div>
|
||||
<div class="action-panel__button-group">
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想看' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在看' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '看过' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '听过' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -300,7 +318,7 @@
|
|||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
|
||||
<span class="review-panel__actions">
|
||||
|
|
|
@ -48,8 +48,7 @@
|
|||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -39,8 +39,7 @@
|
|||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -44,8 +44,7 @@
|
|||
class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -31,7 +31,10 @@
|
|||
{% for field in form %}
|
||||
{% if field.name == 'release_date' %}
|
||||
{{ field.label_tag }}
|
||||
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}">
|
||||
|
||||
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
|
||||
value="{{ form.instance.release_date | date:"Y-m-d" }}">
|
||||
|
||||
{% else %}
|
||||
{% if field.name != 'id' %}
|
||||
{{ field.label_tag }}
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
{% for field in form %}
|
||||
{% if field.name == 'release_date' %}
|
||||
{{ field.label_tag }}
|
||||
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}">
|
||||
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
|
||||
value="{{ form.instance.release_date | date:"Y-m-d" }}">
|
||||
{% else %}
|
||||
{% if field.name != 'id' %}
|
||||
{{ field.label_tag }}
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -139,20 +139,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if song.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
{% if song.brief %}
|
||||
|
||||
<p class="entity-desc__content">{{ song.brief | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div>{% trans '暂无简介' %}</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="entity-marks">
|
||||
|
@ -171,7 +169,7 @@
|
|||
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
{% if others_mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
|
||||
{% if others_mark.text %}
|
||||
|
@ -196,7 +194,7 @@
|
|||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'music:retrieve_song_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
|
@ -224,7 +222,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="mark-panel__actions">
|
||||
<a href="" class="edit">{% trans '修改' %}</a>
|
||||
|
@ -252,9 +250,9 @@
|
|||
<div class="action-panel" id="addMarkPanel">
|
||||
<div class="action-panel__label">{% trans '标记这部作品' %}</div>
|
||||
<div class="action-panel__button-group">
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想看' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在看' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '看过' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在听' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '听过' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -267,7 +265,7 @@
|
|||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
|
||||
<span class="review-panel__actions">
|
||||
|
|
|
@ -49,8 +49,7 @@
|
|||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -39,8 +39,7 @@
|
|||
{{ review.title }}
|
||||
</h5>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
<a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
|
||||
{% if review.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-time">{{ review.edited_time }}</span>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -118,8 +118,7 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
|
@ -122,8 +122,7 @@
|
|||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
|
|
289
users/templates/users/music_list.html
Normal file
289
users/templates/users/music_list.html
Normal file
|
@ -0,0 +1,289 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load highlight %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/rating-star-readonly.js' %}"></script>
|
||||
<script src="{% static 'js/mastodon.js' %}"></script>
|
||||
<script src="{% static 'js/home.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content" class="container">
|
||||
<div class="grid grid--reverse-order">
|
||||
<div class="grid__main grid__main--reverse-order">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="entity-list">
|
||||
|
||||
<div class="set">
|
||||
<h5 class="entity-list__title">
|
||||
{{ user.username }}{{ list_title }}
|
||||
</h5>
|
||||
</div>
|
||||
<ul class="entity-list__entities">
|
||||
|
||||
{% for mark in marks %}
|
||||
|
||||
{% with mark.music as music %}
|
||||
|
||||
<li class="entity-list__entity">
|
||||
<div class="entity-list__entity-img-wrapper">
|
||||
{% if music.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% elif music.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}">
|
||||
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="entity-list__entity-text">
|
||||
<div class="entity-list__entity-title">
|
||||
{% if music.category_name|lower == 'album' %}
|
||||
<a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
|
||||
{{ music.title }}
|
||||
</a>
|
||||
{% elif music.category_name|lower == 'song' %}
|
||||
<a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
|
||||
{{ music.title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ music.source_url }}">
|
||||
<span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<span class="entity-list__entity-info ">
|
||||
{% if music.artist %}{% trans '艺术家' %}
|
||||
{% for artist in music.artist %}
|
||||
<span>{{ artist }}</span>
|
||||
{% if not forloop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if music.genre %}/ {% trans '流派' %}
|
||||
{{ music.genre }}
|
||||
{% endif %}
|
||||
|
||||
{% if music.release_date %}/ {% trans '发行日期' %}
|
||||
{{ music.release_date }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if music.brief %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{{ music.brief }}
|
||||
</p>
|
||||
{% elif music.category_name|lower == 'album' %}
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '曲目:' %}{{ music.track_list }}
|
||||
</p>
|
||||
{% else %}
|
||||
<!-- song -->
|
||||
<p class="entity-list__entity-brief">
|
||||
{% trans '所属专辑:' %}{{ music.album }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="tag-collection">
|
||||
{% for tag_dict in music.tag_list %}
|
||||
{% for k, v in tag_dict.items %}
|
||||
{% if k == 'content' %}
|
||||
<span class="tag-collection__tag">
|
||||
<a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="dividing-line dividing-line--dashed"></div>
|
||||
<div class="entity-marks" style="margin-bottom: 0;">
|
||||
<ul class="entity-marks__mark-list">
|
||||
<li class="entity-marks__mark">
|
||||
|
||||
{% if mark.rating %}
|
||||
<span class="entity-marks__rating-star rating-star"
|
||||
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
|
||||
{% endif %}
|
||||
{% if mark.is_private %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
|
||||
{% if mark.text %}
|
||||
<p class="entity-marks__mark-content">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
|
||||
{% endwith %}
|
||||
|
||||
{% empty %}
|
||||
<div>{% trans '无结果' %}</div>
|
||||
{% endfor %}
|
||||
<!-- user mark -->
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
|
||||
{% if marks.pagination.has_prev %}
|
||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</a>
|
||||
<a href="?page={{ marks.previous_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page in marks.pagination.page_range %}
|
||||
|
||||
{% if page == marks.pagination.current_page %}
|
||||
<a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
||||
{% else %}
|
||||
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% if marks.pagination.has_next %}
|
||||
<a href="?page={{ marks.next_page_number }}"
|
||||
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
||||
<a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">»</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
|
||||
<div class="aside-section-wrapper aside-section-wrapper--no-margin">
|
||||
<div class="user-profile" id="userInfoCard">
|
||||
<div class="user-profile__header">
|
||||
<!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
|
||||
<img src="" class="user-profile__avatar mast-avatar">
|
||||
<a href="{% url 'users:home' user.id %}">
|
||||
<h5 class="user-profile__username mast-displayname"></h5>
|
||||
</a>
|
||||
</div>
|
||||
<p class="user-profile__bio mast-brief"></p>
|
||||
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||
|
||||
{% if request.user != user %}
|
||||
<a href="{% url 'users:report' %}?user_id={{ user.id }}"
|
||||
class="user-profile__report-link">{% trans '举报用户' %}</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relation-dropdown">
|
||||
<div class="relation-dropdown__button">
|
||||
<span class="icon-arrow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
|
||||
<path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="relation-dropdown__body">
|
||||
<div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
|
||||
|
||||
<div class="user-relation" id="followings">
|
||||
<h5 class="user-relation__label">
|
||||
{% trans '关注的人' %}
|
||||
</h5>
|
||||
<a href="{% url 'users:following' user.id %}"
|
||||
class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
|
||||
<ul class="user-relation__related-user-list mast-following">
|
||||
<li class="user-relation__related-user">
|
||||
<a>
|
||||
<img src="" alt="" class="user-relation__related-user-avatar">
|
||||
<div class="user-relation__related-user-name mast-displayname">
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="user-relation" id="followers">
|
||||
<h5 class="user-relation__label">
|
||||
{% trans '被他们关注' %}
|
||||
</h5>
|
||||
<a href="{% url 'users:followers' user.id %}"
|
||||
class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
|
||||
<ul class="user-relation__related-user-list mast-followers">
|
||||
<li class="user-relation__related-user">
|
||||
<a>
|
||||
<img src="" alt="" class="user-relation__related-user-avatar">
|
||||
<div class="user-relation__related-user-name mast-displayname">
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
|
||||
<!--current user mastodon id-->
|
||||
{% if user == request.user %}
|
||||
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||
{% else %}
|
||||
<div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
|
||||
{% endif %}
|
||||
<div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
|
||||
<div id="spinner" hidden>
|
||||
<div class="spinner">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
|
@ -19,7 +19,7 @@
|
|||
<img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
|
||||
|
||||
<div id="loginButton">
|
||||
<p>欢迎来到NiceDB书影音(其实现在只有书汗电影)!</p>
|
||||
<p>欢迎来到NiceDB书影音!</p>
|
||||
<p>
|
||||
NiceDB书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
|
||||
这里仍是一片处女地,丰富的内容需要大家共同创造。
|
||||
|
|
|
@ -13,6 +13,7 @@ urlpatterns = [
|
|||
path('<int:id>/following/', following, name='following'),
|
||||
path('<int:id>/book/<str:status>/', book_list, name='book_list'),
|
||||
path('<int:id>/movie/<str:status>/', movie_list, name='movie_list'),
|
||||
path('<int:id>/music/<str:status>/', music_list, name='music_list'),
|
||||
path('<str:id>/', home, name='home'),
|
||||
path('<str:id>/followers/', followers, name='followers'),
|
||||
path('<str:id>/following/', following, name='following'),
|
||||
|
|
|
@ -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':
|
||||
|
|
Loading…
Add table
Reference in a new issue