Merge pull request #71 from doubaniux/music-game

Add music
This commit is contained in:
doubaniux 2021-02-15 21:35:53 +01:00 committed by GitHub
commit 8a5897b4c3
67 changed files with 6170 additions and 315 deletions

View file

@ -25,7 +25,13 @@ urlpatterns = [
path('users/', include('users.urls')),
path('books/', include('books.urls')),
path('movies/', include('movies.urls')),
path('music/', include('music.urls')),
path('announcement/', include('management.urls')),
path('', include('common.urls')),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

View file

@ -77,42 +77,16 @@ class BookForm(forms.ModelForm):
return isbn
class BookMarkForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
STATUS_CHOICES = [(v, BookMarkStatusTranslator(v)) for v in MarkStatusEnum.values]
class BookMarkForm(MarkForm):
STATUS_CHOICES = [(v, BookMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
share_to_mastodon = forms.BooleanField(label=_("分享到长毛象"), initial=True, required=False)
rating = forms.IntegerField(validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
is_private = RadioBooleanField(
label=_("可见性"),
initial=True,
choices=IS_PRIVATE_CHOICES
)
tags = TagField(
required=False,
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
label = _("标签")
)
text = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"placeholder": _("最多只能写360字哦~"),
"maxlength": 360
}
),
label = _("短评"),
)
class Meta:
model = BookMark
@ -132,18 +106,8 @@ class BookMarkForm(forms.ModelForm):
}
class BookReviewForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
share_to_mastodon = forms.BooleanField(label=_("分享到长毛象"), initial=True, required=False)
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
is_private = RadioBooleanField(
label=_("可见性"),
initial=True,
choices=IS_PRIVATE_CHOICES
)
class BookReviewForm(ReviewForm):
class Meta:
model = BookReview
fields = [

View file

@ -47,7 +47,7 @@ class Book(Entity):
# since data origin is not formatted and might be CNY USD or other currency, use char instead
price = models.CharField(_("pricing"), blank=True, default='', max_length=50)
pages = models.PositiveIntegerField(_("pages"), null=True, blank=True)
isbn = models.CharField(_("ISBN"), blank=True, null=True, max_length=20, db_index=True)
isbn = models.CharField(_("ISBN"), blank=True, null=False, max_length=20, db_index=True, default='')
# to store previously scrapped data
cover = models.ImageField(_("cover picture"), upload_to=book_cover_path, default=DEFAULT_BOOK_IMAGE, blank=True)
contents = models.TextField(blank=True, default="")

View file

@ -52,9 +52,12 @@
{% endif %}
{% if book.last_editor %}
<a href="{% url 'users:home' book.last_editor.id %}">
<div>{% trans '最近编辑者:' %}{{ book.last_editor | default:"" }}</div>
</a>
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' book.last_editor.id %}">
<span>{{ book.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ book.edited_time }}</div>

View file

@ -99,7 +99,7 @@
<div>
<a href="{% url 'books:update' book.id %}">{% trans '编辑这本书' %}</a>
{% if user.is_staff %}
<a href="{% url 'books:delete' book.id %}"> / {% trans '删除' %}</a>
/<a href="{% url 'books:delete' book.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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):
@ -174,3 +169,104 @@ class HstoreField(forms.CharField):
if len(pairs) == 1:
pairs = (pairs,)
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
#############################
class MarkForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
share_to_mastodon = forms.BooleanField(
label=_("分享到长毛象"), initial=True, required=False)
rating = forms.IntegerField(
validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
is_private = RadioBooleanField(
label=_("可见性"),
initial=True,
choices=IS_PRIVATE_CHOICES
)
tags = TagField(
required=False,
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
label=_("标签")
)
text = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"placeholder": _("最多只能写360字哦~"),
"maxlength": 360
}
),
label=_("短评"),
)
class ReviewForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
share_to_mastodon = forms.BooleanField(
label=_("分享到长毛象"), initial=True, required=False)
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
is_private = RadioBooleanField(
label=_("可见性"),
initial=True,
choices=IS_PRIVATE_CHOICES
)

View file

@ -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):
@ -32,8 +33,8 @@ class Entity(models.Model):
edited_time = models.DateTimeField(auto_now_add=True)
last_editor = models.ForeignKey(
User, on_delete=models.SET_NULL, related_name='%(class)s_last_editor', null=True, blank=False)
brief = models.TextField(blank=True, default="")
other_info = postgres.JSONField(
brief = models.TextField(_("简介"), blank=True, default="")
other_info = postgres.JSONField(_("其他信息"),
blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
# source_url should include shceme, which is normally https://
source_url = models.URLField(_("URL"), max_length=500, unique=True)
@ -114,7 +115,7 @@ class Entity(models.Model):
"""
raise NotImplementedError("Subclass should implement this method.")
def get_revies_manager(self):
def get_reviews_manager(self):
"""
Normally this won't be used.
There is no ocassion where visitor can simply view all the reviews.

View file

@ -2,15 +2,26 @@ 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
from books.models import Book
from books.forms import BookForm
from music.models import Album, Song
from music.forms import AlbumForm, SongForm
RE_NUMBERS = re.compile(r"\d+\d*")
@ -18,7 +29,7 @@ RE_WHITESPACES = re.compile(r"\s+")
DEFAULT_REQUEST_HEADERS = {
'Host': 'book.douban.com',
'Host': '',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:70.0) Gecko/20100101 Firefox/70.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
@ -53,19 +64,23 @@ 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
site = None
site_name = None
# host means technically hostname
host = None
# corresponding data class
@ -74,34 +89,49 @@ 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
super().__init_subclass__(**kwargs)
assert cls.site is not None, "class variable `site` must be specified"
assert cls.host is not None, "class variable `host` must be specified"
assert cls.site_name is not None, "class variable `site_name` must be specified"
assert bool(cls.host), "class variable `host` must be specified"
assert cls.data_class is not None, "class variable `data_class` must be specified"
assert cls.form_class is not None, "class variable `form_class` must be specified"
assert cls.regex is not None, "class variable `regex` must be specified"
assert isinstance(cls.host, str), "`host` must be type str"
assert cls.site in SourceSiteEnum, "`site` must be one of `SourceSiteEnum` value"
assert hasattr(cls, 'scrape') and callable(cls.scrape), "scaper must have method `.scrape()`"
assert isinstance(cls.host, str) or (isinstance(cls.host, list) and isinstance(
cls.host[0], str)), "`host` must be type str or list"
assert cls.site_name in SourceSiteEnum, "`site_name` must be one of `SourceSiteEnum` value"
assert hasattr(cls, 'scrape') and callable(
cls.scrape), "scaper must have method `.scrape()`"
# decorate the scrape method
cls.scrape = classmethod(log_url(cls.scrape))
scraper_registry[cls.host] = cls
# register scraper
if isinstance(cls.host, list):
for host in cls.host:
scraper_registry[host] = cls
else:
scraper_registry[cls.host] = cls
def scrape(self, url):
"""
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")
@ -113,14 +143,15 @@ class AbstractScraper:
session_id = random.random()
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
proxies = {
'http': proxy_url,
'https': proxy_url,
}
# if DEBUG:
# proxies = None
r = requests.get(url, proxies=proxies, headers=headers, timeout=TIMEOUT)
r = requests.get(url, proxies=proxies,
headers=headers, timeout=TIMEOUT)
# r = requests.get(url, headers=DEFAULT_REQUEST_HEADERS, timeout=TIMEOUT)
return html.fromstring(r.content.decode('utf-8'))
@ -132,7 +163,7 @@ class AbstractScraper:
raw_img = None
session_id = random.random()
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
(LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
proxies = {
'http': proxy_url,
'https': proxy_url,
@ -155,16 +186,34 @@ 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):
site = SourceSiteEnum.DOUBAN.value
site_name = SourceSiteEnum.DOUBAN.value
host = "book.douban.com"
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()
@ -234,7 +283,8 @@ class DoubanBookScraper(AbstractScraper):
brief_elem = content.xpath(
"//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()")
brief = '\n'.join(p.strip() for p in brief_elem) if brief_elem else None
brief = '\n'.join(p.strip()
for p in brief_elem) if brief_elem else None
contents = None
try:
@ -253,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]/
@ -311,23 +361,24 @@ class DoubanBookScraper(AbstractScraper):
'brief': brief,
'contents': contents,
'other_info': other,
'source_site': self.site,
'source_site': self.site_name,
'source_url': self.get_effective_url(url),
}
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
class DoubanMovieScraper(AbstractScraper):
site = SourceSiteEnum.DOUBAN.value
site_name = SourceSiteEnum.DOUBAN.value
host = 'movie.douban.com'
data_class = Movie
form_class = MovieForm
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()
headers['Host'] = 'movie.douban.com'
headers['Host'] = self.host
content = self.download_page(url, headers)
# parsing starts here
@ -461,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,
@ -483,8 +534,387 @@ class DoubanMovieScraper(AbstractScraper):
'single_episode_length': single_episode_length,
'brief': brief,
'is_series': is_series,
'source_site': self.site,
'source_site': self.site_name,
'source_url': self.get_effective_url(url),
}
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
class DoubanAlbumScraper(AbstractScraper):
site_name = SourceSiteEnum.DOUBAN.value
host = 'music.douban.com'
data_class = Album
form_class = AlbumForm
regex = re.compile(r"https://music\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
headers['Host'] = self.host
content = self.download_page(url, headers)
# parsing starts here
try:
title = content.xpath("//h1/span/text()")[0].strip()
except IndexError:
raise ValueError("given url contains no album info")
if not title:
raise ValueError("given url contains no album info")
artists_elem = content.xpath("""//div[@id='info']/span/span[@class='pl']/a/text()""")
artist = None if not artists_elem else artists_elem
genre_elem = content.xpath(
"//div[@id='info']//span[text()='流派:']/following::text()[1]")
genre = genre_elem[0].strip() if genre_elem else None
date_elem = content.xpath(
"//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
release_date = dateparser.parse(date_elem[0].strip(), settings={
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)}) if date_elem else None
company_elem = content.xpath(
"//div[@id='info']//span[text()='出版者:']/following::text()[1]")
company = company_elem[0].strip() if company_elem else None
track_list_elem = content.xpath(
"//div[@class='track-list']/div[@class='indent']/div/text()"
)
if track_list_elem:
track_list = '\n'.join([track.strip() for track in track_list_elem])
else:
track_list = None
brief_elem = content.xpath("//span[@class='all hidden']")
if not brief_elem:
brief_elem = content.xpath("//span[@property='v:summary']")
brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
'./text()')]) if brief_elem else None
other_info = {}
other_elem = content.xpath(
"//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
if other_elem:
other_info['又名'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
if other_elem:
other_info['专辑类型'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
if other_elem:
other_info['介质'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
if other_elem:
other_info['ISRC'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
if other_elem:
other_info['条形码'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
if other_elem:
other_info['碟片数'] = other_elem[0].strip()
img_url_elem = content.xpath("//div[@id='mainpic']//img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img, ext = self.download_image(img_url)
data = {
'title': title,
'artist': artist,
'genre': genre,
'release_date': release_date,
'duration': None,
'company': company,
'track_list': track_list,
'brief': brief,
'other_info': other_info,
'source_site': self.site_name,
'source_url': self.get_effective_url(url),
}
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']

View file

@ -374,6 +374,9 @@ input[type='search'],
input[type='tel'],
input[type='text'],
input[type='url'],
input[type='date'],
input[type='time'],
input[type='color'],
textarea,
select {
-webkit-appearance: none;
@ -396,6 +399,9 @@ input[type='search']:focus,
input[type='tel']:focus,
input[type='text']:focus,
input[type='url']:focus,
input[type='date']:focus,
input[type='time']:focus,
input[type='color']:focus,
textarea:focus,
select:focus {
border-color: #00a1cc;
@ -409,6 +415,9 @@ input[type='search']::-webkit-input-placeholder,
input[type='tel']::-webkit-input-placeholder,
input[type='text']::-webkit-input-placeholder,
input[type='url']::-webkit-input-placeholder,
input[type='date']::-webkit-input-placeholder,
input[type='time']::-webkit-input-placeholder,
input[type='color']::-webkit-input-placeholder,
textarea::-webkit-input-placeholder,
select::-webkit-input-placeholder {
color: #ccc;
@ -421,6 +430,9 @@ input[type='search']:-ms-input-placeholder,
input[type='tel']:-ms-input-placeholder,
input[type='text']:-ms-input-placeholder,
input[type='url']:-ms-input-placeholder,
input[type='date']:-ms-input-placeholder,
input[type='time']:-ms-input-placeholder,
input[type='color']:-ms-input-placeholder,
textarea:-ms-input-placeholder,
select:-ms-input-placeholder {
color: #ccc;
@ -433,6 +445,9 @@ input[type='search']::-ms-input-placeholder,
input[type='tel']::-ms-input-placeholder,
input[type='text']::-ms-input-placeholder,
input[type='url']::-ms-input-placeholder,
input[type='date']::-ms-input-placeholder,
input[type='time']::-ms-input-placeholder,
input[type='color']::-ms-input-placeholder,
textarea::-ms-input-placeholder,
select::-ms-input-placeholder {
color: #ccc;
@ -445,6 +460,9 @@ input[type='search']::placeholder,
input[type='tel']::placeholder,
input[type='text']::placeholder,
input[type='url']::placeholder,
input[type='date']::placeholder,
input[type='time']::placeholder,
input[type='color']::placeholder,
textarea::placeholder,
select::placeholder {
color: #ccc;
@ -1184,6 +1202,8 @@ select::placeholder {
font-weight: lighter;
letter-spacing: 0.1rem;
word-break: keep-all;
opacity: 0.8;
position: relative;
top: -1px;
}
@ -1194,8 +1214,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 {
@ -1308,6 +1336,8 @@ select::placeholder {
object-fit: contain;
float: left;
max-width: 150px;
-o-object-position: top;
object-position: top;
}
.entity-detail .entity-detail__info {
@ -1377,7 +1407,7 @@ select::placeholder {
}
.entity-desc .entity-desc__content--folded {
max-height: 200px;
max-height: 202px;
}
.entity-desc .entity-desc__unfold-button {
@ -1647,6 +1677,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;
@ -1720,6 +1821,10 @@ select::placeholder {
.review-head .review-head__actions {
float: unset;
}
.track-carousel__track {
width: 32vw;
height: 40vw;
}
}
@media (max-width: 991.98px) {
@ -1772,7 +1877,7 @@ select::placeholder {
line-height: unset;
height: unset;
padding: 4px 15px;
margin: 0 5px;
margin: 5px;
}
.action-panel {

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,8 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
if (placeholderValue == null) {
placeholderValue = '';
}
// assign existing pairs to hidden input
setHiddenInput(valueKeyWidget);
let newInputPair = $('<input type="text"' + 'placeholder=' + placeholderKey + '><input type="text"' + 'placeholder=' + placeholderValue + '>');
valueKeyWidget.append(newInputPair.clone());
@ -27,7 +29,7 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
$(this).next().remove();
$(this).remove();
}
});
});
valueKeyWidget.on('input', ':nth-last-child(3)', function () {
if (!$(this).val() && !$(this).prev().val() && valueKeyWidget.children("input").length > 2) {
@ -37,12 +39,16 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
});
valueKeyWidget.on('input', function () {
let keys = $(this).children(":nth-child(odd)").map(function () {
setHiddenInput(this);
});
function setHiddenInput(elem) {
let keys = $(elem).children(":nth-child(odd)").map(function () {
if ($(this).val()) {
return $(this).val();
}
}).get();
let values = $(this).children(":nth-child(even)").map(function () {
let values = $(elem).children(":nth-child(even)").map(function () {
if ($(this).val()) {
return $(this).val();
}
@ -55,7 +61,7 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
finalValue.push(JSON.stringify(json))
});
hiddenInput.val(finalValue.toString());
} else if(keys.length - values.length == 1) {
} else if (keys.length - values.length == 1) {
let finalValue = [];
keys.forEach(function (key, i) {
let json = new Object;
@ -66,8 +72,10 @@ function keyValueInput(valueKeyWidget, hiddenInput) {
}
finalValue.push(JSON.stringify(json))
});
hiddenInput.val(finalValue.toString());
hiddenInput.val(finalValue.toString());
}
});
}
}

View file

@ -30,7 +30,7 @@ $aside-section-padding-mobile: 24px 25px 10px 25px
line-height: unset;
height: unset;
padding: 4px 15px;
margin: 0 5px;
margin: 5px;
.action-panel
margin-bottom: 20px

View file

@ -68,6 +68,9 @@ input[type='search'],
input[type='tel'],
input[type='text'],
input[type='url'],
input[type='date'],
input[type='time'],
input[type='color'],
textarea,
select
appearance: none // Removes awkward default styles on some inputs for iOS

View file

@ -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
@ -17,6 +20,8 @@ $in-site-color: $color-primary
font-weight: lighter
letter-spacing: 0.1rem
word-break: keep-all
opacity: 0.8
position: relative
top: -1px
@ -25,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

View file

@ -66,7 +66,9 @@ $sub-section-title-margin: 8px
position: relative
top: 0.52em
&--full-length
// display: block
max-width: 100%
// margin-bottom: 12px
& &__entity-brief
margin-top: 8px
@ -100,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
@ -158,7 +161,7 @@ $mark-review-padding-wider: 6px 0
& &__content
overflow: hidden
&--folded
max-height: 200px
max-height: 202px
& &__unfold-button
display: flex
@ -373,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
@ -437,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)

View file

@ -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>

View file

@ -175,7 +175,7 @@
<div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
{% endif %}
<span class="entity-list__entity-info entity-list__entity-info--full-length">
<span class="entity-list__entity-info ">
{% if movie.director %}{% trans '导演' %}
@ -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 %} / {% 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 %}
@ -219,6 +219,111 @@
</li>
{% endwith %}
{% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %}
{% with music=item %}
<li class="entity-list__entity">
<div class="entity-list__entity-img-wrapper">
{% 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">
{% 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|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 %}
{% if not request.GET.c or request.GET.c != 'music' and request.GET.c != 'book' and request.GET.c != 'music' %}
<span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
{% endif %}
<a href="{{ music.source_url }}">
<span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
</a>
</div>
{% if music.rating %}
<div class="rating-star entity-list__rating-star" data-rating-score="{{ music.rating | floatformat:"0" }}"></div>
<span class="entity-list__rating-score rating-score">{{ music.rating }}</span>
{% else %}
<div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
{% endif %}
<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>
<span class="entity-list__entity-info entity-list__entity-info--full-length">
</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>
</li>
{% endwith %}
{% endif %}
@ -278,6 +383,12 @@
<a href="{% url 'movies:create' %}">
<button class="add-entity-entries__button">{% trans '添加电影/剧集' %}</button>
</a>
{% elif request.GET.c|lower == 'music' %}
<a href="{% url 'movies:create' %}">
<button class="add-entity-entries__button">{% trans '添加音乐' %}</button>
</a>
{% else %}
<a href="{% url 'books:create' %}">
@ -286,6 +397,12 @@
<a href="{% url 'movies:create' %}">
<button class="add-entity-entries__button">{% trans '添加电影/剧集' %}</button>
</a>
<a href="{% url 'music:create_album' %}">
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
</a>
<a href="{% url 'music:create_song' %}">
<button class="add-entity-entries__button">{% trans '添加单曲' %}</button>
</a>
{% endif %}
@ -296,6 +413,12 @@
<a href="{% url 'movies:create' %}">
<button class="add-entity-entries__button">{% trans '添加电影/剧集' %}</button>
</a>
<a href="{% url 'music:create_album' %}">
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
</a>
<a href="{% url 'music:create_song' %}">
<button class="add-entity-entries__button">{% trans '添加单曲' %}</button>
</a>
{% endif %}
</div>
<div class="add-entity-entries__entry">
@ -319,6 +442,15 @@
<button class="add-entity-entries__button">{% trans '从表瓣剽取数据' %}</button>
</a>
{% elif request.GET.c|lower == 'music' %}
<div class="add-entity-entries__label">
{% trans '或者(≖ ◡ ≖)✧' %}
</div>
<a href="{% url 'music:scrape_album' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
<button class="add-entity-entries__button">{% trans '从表瓣剽取数据' %}</button>
</a>
{% else %}
<div class="add-entity-entries__label">
@ -330,6 +462,9 @@
<a href="{% url 'movies:scrape' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
<button class="add-entity-entries__button">{% trans '电影/剧集' %}</button>
</a>
<a href="{% url 'music:scrape_album' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
<button class="add-entity-entries__button">{% trans '专辑' %}</button>
</a>
{% endif %}
@ -344,7 +479,9 @@
<a href="{% url 'movies:scrape' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
<button class="add-entity-entries__button">{% trans '电影/剧集' %}</button>
</a>
<a href="{% url 'music:scrape_album' %}{% if request.GET.q %}?q={{ request.GET.q }}{% endif %}">
<button class="add-entity-entries__button">{% trans '专辑' %}</button>
</a>
{% endif %}
</div>

View file

@ -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>

View file

@ -17,6 +17,7 @@
<option value="all" {% if request.GET.c and request.GET.c != 'movie' and request.GET.c != 'book' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
<option value="book" {% if request.GET.c and request.GET.c == 'book' %}selected{% endif %}>{% trans '书籍' %}</option>
<option value="movie" {% if request.GET.c and request.GET.c == 'movie' %}selected{% endif %}>{% trans '电影' %}</option>
<option value="music" {% if request.GET.c and request.GET.c == 'music' %}selected{% endif %}>{% trans '音乐' %}</option>
</select>
</div>
<button class="navbar__dropdown-btn">• • •</button>

View file

@ -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>

View file

@ -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)

View file

@ -8,11 +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, AlbumMark, SongMark
from users.models import Report, User
from mastodon.decorators import mastodon_request_included
from common.models import MarkStatusEnum
@ -27,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
@ -42,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:
@ -76,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)
@ -95,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,
}
@ -122,22 +154,30 @@ def search(request):
except ValidationError as e:
pass
# category, book/movie/record etc
# category, book/movie/music etc
category = request.GET.get("c", default='').strip().lower()
# 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 = kwargs.get('tag')
for keyword in [keywords]:
query_args = []
q = Q()
for keyword in keywords:
q = q | Q(title__icontains=keyword)
q = q | Q(subtitle__icontains=keyword)
q = q | Q(orig_title__icontains=keyword)
# tag
tag = request.GET.get("tag", default='')
if tag:
q = q & Q(book_tags__content__iexact=tag)
@ -158,6 +198,8 @@ def search(request):
elif tag:
# search by single tag
book.similarity = 0 if book.rating_number is None else book.rating_number
else:
book.similarity = 0
return book.similarity
if len(queryset) > 0:
ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
@ -165,19 +207,19 @@ 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 = kwargs.get('tag')
for keyword in [keywords]:
query_args = []
q = Q()
for keyword in keywords:
q = q | Q(title__icontains=keyword)
q = q | Q(other_title__icontains=keyword)
q = q | Q(orig_title__icontains=keyword)
# tag
tag = request.GET.get("tag", default='')
if tag:
q = q & Q(movie_tags__content__iexact=tag)
@ -197,6 +239,8 @@ def search(request):
elif tag:
# search by single tag
movie.similarity = 0 if movie.rating_number is None else movie.rating_number
else:
movie.similarity = 0
return movie.similarity
if len(queryset) > 0:
ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
@ -204,11 +248,71 @@ def search(request):
ordered_queryset = list(queryset)
return ordered_queryset
def all_param_handler():
book_queryset = book_param_handler()
movie_queryset = movie_param_handler()
def music_param_handler(**kwargs):
# keywords
keywords = kwargs.get('keywords')
# tag
tag = kwargs.get('tag')
query_args = []
q = Q()
# search albums
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)
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:
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:
# search by single tag
music.similarity = 0 if music.rating_number is None else music.rating_number
else:
music.similarity = 0
return music.similarity
if len(queryset) > 0:
ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
else:
ordered_queryset = list(queryset)
return ordered_queryset
def all_param_handler(**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,
book_queryset + movie_queryset + music_queryset,
key=operator.attrgetter('similarity'),
reverse=True
)
@ -217,14 +321,21 @@ def search(request):
param_handler = {
'book': book_param_handler,
'movie': movie_param_handler,
'music': music_param_handler,
'all': all_param_handler,
'': all_param_handler
}
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)
@ -279,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)

View file

@ -16,11 +16,6 @@ def MovieMarkStatusTranslator(status):
class MovieForm(forms.ModelForm):
# pub_year = forms.IntegerField(
# required=False, max_value=9999, min_value=0, label=_("出版年份"))
# pub_month = forms.IntegerField(
# required=False, max_value=12, min_value=1, label=_("出版月份"))
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
genre = forms.MultipleChoiceField(
required=False,
@ -103,52 +98,18 @@ class MovieForm(forms.ModelForm):
'is_series': forms.CheckboxInput(attrs={'style': 'width: auto; position: relative; top: 2px'})
}
# def clean_isbn(self):
# isbn = self.cleaned_data.get('isbn')
# if isbn:
# isbn = isbn.strip()
# return isbn
class MovieMarkForm(MarkForm):
class MovieMarkForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
STATUS_CHOICES = [(v, MovieMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
share_to_mastodon = forms.BooleanField(
label=_("分享到长毛象"), initial=True, required=False)
rating = forms.IntegerField(
validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
is_private = RadioBooleanField(
label=_("可见性"),
initial=True,
choices=IS_PRIVATE_CHOICES
)
tags = TagField(
required=False,
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
label=_("标签")
)
text = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"placeholder": _("最多只能写360字哦~"),
"maxlength": 360
}
),
label=_("短评"),
)
class Meta:
model = MovieMark
@ -168,19 +129,7 @@ class MovieMarkForm(forms.ModelForm):
}
class MovieReviewForm(forms.ModelForm):
IS_PRIVATE_CHOICES = [
(True, _("仅关注者")),
(False, _("公开")),
]
share_to_mastodon = forms.BooleanField(
label=_("分享到长毛象"), initial=True, required=False)
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
is_private = RadioBooleanField(
label=_("可见性"),
initial=True,
choices=IS_PRIVATE_CHOICES
)
class MovieReviewForm(ReviewForm):
class Meta:
model = MovieReview

View file

@ -54,6 +54,7 @@ class MovieGenreEnum(models.TextChoices):
REALITY_TV = 'Reality-TV', _('真人秀')
FAMILY = 'Family', _('家庭')
TALK_SHOW = 'Talk-Show', _('脱口秀')
OTHER = 'Other', _('其他')
MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum)
@ -76,7 +77,7 @@ class Movie(Entity):
default=list,
)
imdb_code = models.CharField(
blank=True, max_length=10, null=True, db_index=True)
blank=True, max_length=10, null=False, db_index=True, default='')
director = postgres.ArrayField(
models.CharField(_("director"), blank=True,
default='', max_length=100),

View file

@ -59,9 +59,12 @@
{% endif %}
{% if movie.last_editor %}
<a href="{% url 'users:home' movie.last_editor.id %}">
<div>{% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}</div>
</a>
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' movie.last_editor.id %}">
<span>{{ movie.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ movie.edited_time }}</div>

View file

@ -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>

View file

@ -204,7 +204,7 @@
<a href="{% url 'movies:update' movie.id %}">{% trans '编辑这部电影' %}</a>
{% endif %}
{% if user.is_staff %}
<a href="{% url 'movies:delete' movie.id %}"> / {% trans '删除' %}</a>
/<a href="{% url 'movies:delete' movie.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
@ -225,30 +225,18 @@
</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>
{% if movie.contents %}
<div class="entity-desc" id="contents">
<h5 class="entity-desc__title">{% trans '目录' %}</h5>
<p class="entity-desc__content">{{ movie.contents | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
<div class="entity-marks">
@ -270,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 %}
@ -299,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>
@ -327,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>
@ -375,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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
@ -17,7 +16,6 @@ from common.views import PAGE_LINK_NUMBER, jump_or_scrape
from common.models import SourceSiteEnum
from .models import *
from .forms import *
from .forms import MovieMarkStatusTranslator
from boofilsic.settings import MASTODON_TAGS
@ -225,7 +223,7 @@ def retrieve(request, id):
}
)
else:
logger.warning('non-GET method at /movie/<id>')
logger.warning('non-GET method at /movies/<id>')
return HttpResponseBadRequest()

0
music/__init__.py Normal file
View file

11
music/admin.py Normal file
View file

@ -0,0 +1,11 @@
from django.contrib import admin
from .models import *
admin.site.register(Song)
admin.site.register(SongMark)
admin.site.register(SongReview)
admin.site.register(SongTag)
admin.site.register(Album)
admin.site.register(AlbumMark)
admin.site.register(AlbumReview)
admin.site.register(AlbumTag)

5
music/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class MusicConfig(AppConfig):
name = 'music'

180
music/forms.py Normal file
View file

@ -0,0 +1,180 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _
from .models import *
from common.models import MarkStatusEnum
from common.forms import *
def MusicMarkStatusTranslator(status):
trans_dict = {
MarkStatusEnum.DO.value: _("在听"),
MarkStatusEnum.WISH.value: _("想听"),
MarkStatusEnum.COLLECT.value: _("听过")
}
return trans_dict[status]
class SongForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
duration = DurationField(required=False)
class Meta:
model = Song
# fields = '__all__'
fields = [
'id',
'title',
'source_site',
'source_url',
'artist',
'release_date',
'duration',
'isrc',
'genre',
'cover',
'album',
'brief',
'other_info',
]
widgets = {
'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}),
'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}),
'cover': PreviewImageInput(),
}
class SongMarkForm(MarkForm):
STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = SongMark
fields = [
'id',
'song',
'status',
'rating',
'text',
'is_private',
]
labels = {
'rating': _("评分"),
}
widgets = {
'song': forms.TextInput(attrs={"hidden": ""}),
}
class SongReviewForm(ReviewForm):
class Meta:
model = SongReview
fields = [
'id',
'song',
'title',
'content',
'is_private'
]
labels = {
'song': "",
'title': _("标题"),
'content': _("正文"),
'share_to_mastodon': _("分享到长毛象")
}
widgets = {
'song': forms.TextInput(attrs={"hidden": ""}),
}
class AlbumForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
duration = DurationField(required=False)
class Meta:
model = Album
# fields = '__all__'
fields = [
'id',
'title',
'source_site',
'source_url',
'artist',
'company',
'release_date',
'duration',
'genre',
'cover',
'brief',
'track_list',
'other_info',
]
widgets = {
'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}),
'company': forms.TextInput(attrs={'placeholder': _("多个发行方使用英文逗号分隔")}),
'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}),
'cover': PreviewImageInput(),
}
class AlbumMarkForm(MarkForm):
STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v))
for v in MarkStatusEnum.values]
status = forms.ChoiceField(
label=_(""),
widget=forms.RadioSelect(),
choices=STATUS_CHOICES
)
class Meta:
model = AlbumMark
fields = [
'id',
'album',
'status',
'rating',
'text',
'is_private',
]
labels = {
'rating': _("评分"),
}
widgets = {
'album': forms.TextInput(attrs={"hidden": ""}),
}
class AlbumReviewForm(ReviewForm):
class Meta:
model = AlbumReview
fields = [
'id',
'album',
'title',
'content',
'is_private'
]
labels = {
'album': "",
'title': _("标题"),
'content': _("正文"),
'share_to_mastodon': _("分享到长毛象")
}
widgets = {
'album': forms.TextInput(attrs={"hidden": ""}),
}

181
music/models.py Normal file
View file

@ -0,0 +1,181 @@
import uuid
import django.contrib.postgres.fields as postgres
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import reverse
from common.models import Entity, Mark, Review, Tag
from common.utils import ChoicesDictGenerator
from boofilsic.settings import SONG_MEDIA_PATH_ROOT, DEFAULT_SONG_IMAGE, ALBUM_MEDIA_PATH_ROOT, DEFAULT_ALBUM_IMAGE
from django.utils import timezone
def song_cover_path(instance, filename):
ext = filename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
root = ''
if SONG_MEDIA_PATH_ROOT.endswith('/'):
root = SONG_MEDIA_PATH_ROOT
else:
root = SONG_MEDIA_PATH_ROOT + '/'
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
def album_cover_path(instance, filename):
ext = filename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
root = ''
if ALBUM_MEDIA_PATH_ROOT.endswith('/'):
root = ALBUM_MEDIA_PATH_ROOT
else:
root = ALBUM_MEDIA_PATH_ROOT + '/'
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
class Album(Entity):
title = models.CharField(_("标题"), max_length=500)
release_date = models.DateField(
_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
cover = models.ImageField(
_("封面"), upload_to=album_cover_path, default=DEFAULT_ALBUM_IMAGE, blank=True)
duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
artist = postgres.ArrayField(
models.CharField(_("artist"), blank=True,
default='', max_length=100),
null=True,
blank=True,
default=list,
verbose_name=_("艺术家")
)
genre = models.CharField(_("流派"), blank=True,
default='', max_length=100)
company = postgres.ArrayField(
models.CharField(blank=True,
default='', max_length=500),
null=True,
blank=True,
default=list,
verbose_name=_("发行方")
)
track_list = models.TextField(_("曲目"), blank=True, default="")
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("music:retrieve_album", args=[self.id])
def get_tags_manager(self):
return self.album_tags
@property
def verbose_category_name(self):
return _("专辑")
class Song(Entity):
'''
Song(track) entity, can point to entity Album
'''
title = models.CharField(_("标题"), max_length=500)
release_date = models.DateField(_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
isrc = models.CharField(_("ISRC"),
blank=True, max_length=15, db_index=True, default='')
# duration in ms
duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
cover = models.ImageField(
_("封面"), upload_to=song_cover_path, default=DEFAULT_SONG_IMAGE, blank=True)
artist = postgres.ArrayField(
models.CharField(blank=True,
default='', max_length=100),
null=True,
blank=True,
default=list,
verbose_name=_("艺术家")
)
genre = models.CharField(_("流派"), blank=True, default='', max_length=100)
album = models.ForeignKey(
Album, models.SET_NULL, "album_songs", null=True, blank=True, verbose_name=_("所属专辑"))
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("music:retrieve_song", args=[self.id])
def get_tags_manager(self):
return self.song_tags
@property
def verbose_category_name(self):
return _("单曲")
class SongMark(Mark):
song = models.ForeignKey(
Song, on_delete=models.CASCADE, related_name='song_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'song'], name='unique_song_mark')
]
class SongReview(Review):
song = models.ForeignKey(
Song, on_delete=models.CASCADE, related_name='song_reviews', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'song'], name='unique_song_review')
]
class SongTag(Tag):
song = models.ForeignKey(
Song, on_delete=models.CASCADE, related_name='song_tags', null=True)
mark = models.ForeignKey(
SongMark, on_delete=models.CASCADE, related_name='songmark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_songmark_tag")
]
class AlbumMark(Mark):
album = models.ForeignKey(
Album, on_delete=models.CASCADE, related_name='album_marks', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'album'], name='unique_album_mark')
]
class AlbumReview(Review):
album = models.ForeignKey(
Album, on_delete=models.CASCADE, related_name='album_reviews', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['owner', 'album'], name='unique_album_review')
]
class AlbumTag(Tag):
album = models.ForeignKey(
Album, on_delete=models.CASCADE, related_name='album_tags', null=True)
mark = models.ForeignKey(
AlbumMark, on_delete=models.CASCADE, related_name='albummark_tags', null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['content', 'mark'], name="unique_albummark_tag")
]

View file

@ -0,0 +1,453 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="NiceDB音乐 - {{ album.title }}">
<meta property="og:type" content="music.album">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ album.cover.url }}">
<meta property="og:site_name" content="NiceDB">
<meta property="og:description"content="{{ album.brief }}">
<title>{% trans 'NiceDB - 音乐详情' %} | {{ album.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/detail.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-detail">
<img src="{{ album.cover.url }}" class="entity-detail__img" alt="{{ album.title }}">
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ album.title }}
<a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if album.rating %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ album.rating }} </span>
{% else %}
<span> {% trans '评分:暂无评分' %}</span>
{% endif %}
</div>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if album.company %}{% trans '发行方:' %}
{% for company in album.company %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="company">{{ company }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.company|length > 5 %}
<a href="javascript:void(0);" id="companyMore">{% trans '更多' %}</a>
<script>
$("#companyMore").click(function (e) {
$("span.company:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if album.release_date %}
{% trans '发行日期:' %}{{ album.release_date }}
{% endif %}
</div>
<div>{% if album.duration %}
{% trans '时长:' %}{{ album.get_duration_display }}
{% endif %}
</div>
<div>{% if album.genre %}
{% trans '流派:' %}{{ album.genre }}
{% endif %}
</div>
</div>
<div class="entity-detail__fields">
{% if album.other_info %}
{% for k, v in album.other_info.items %}
<div>
{{k}}{{v}}
</div>
{% endfor %}
{% endif %}
{% if album.last_editor %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.id %}">{{ album.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'music:update_album' album.id %}">{% trans '编辑这个作品' %}</a>
{% if user.is_staff %}
/<a href="{% url 'music:delete_album' album.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in album_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>
</div>
<div class="dividing-line"></div>
{% if album.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<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>
</div>
{% endif %}
{% if album.track_list %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '曲目' %}</h5>
<p class="entity-desc__content">{{ album.track_list | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
{% if album.album_songs.count %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '关联单曲' %}</h5>
<!-- TODO: Limit the maximum -->
<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>
</div>
{% endif %}
<div class="entity-marks">
<h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
{% if mark_list_more %}
<a href="{% url 'music:retrieve_album_mark_list' album.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
{% endif %}
{% if mark_list %}
<ul class="entity-marks__mark-list">
{% for others_mark in mark_list %}
<li class="entity-marks__mark">
<a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
<span>{{ others_mark.get_status_display }}</span>
{% if others_mark.rating %}
<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"><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 %}
<p class="entity-marks__mark-content">{{ others_mark.text }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div>{% trans '暂无标记' %}</div>
{% endif %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'music:retrieve_album_review_list' album.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
{% endif %}
{% if review_list %}
<ul class="entity-reviews__review-list">
{% for others_review in review_list %}
<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"><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>
<span>{{ others_review.get_plain_content | truncate:100 }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<div>{% trans '暂无评论' %}</div>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
{% if mark %}
<div class="mark-panel">
<span class="mark-panel__status">{% trans '我' %}{{ mark.get_status_display }}</span>
{% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%}
{% if mark.rating %}
<span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% 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="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'music:delete_album_mark' mark.id %}" method="post">
{% csrf_token %}
<a href="" class="delete">{% trans '删除' %}</a>
</form>
</span>
<div class="mark-panel__clear"></div>
<div class="mark-panel__time">{{ mark.edited_time }}</div>
{% if mark.text %}
<p class="mark-panel__text">{{ mark.text }}</p>
{% endif %}
<div class="tag-collection">
{% for tag in mark_tags %}
<span class="tag-collection__tag">{{ tag }}</span>
{% endfor %}
</div>
</div>
{% else %}
<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>
</div>
</div>
{% endif %}
</div>
<div class="aside-section-wrapper">
{% if review %}
<div class="review-panel">
<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"><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">
<a href="{% url 'music:update_album_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'music:delete_album_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'music:retrieve_album_review' review.id %}" class="review-panel__review-title">
{{ review.title }}
</a>
</div>
{% else %}
<div class="action-panel">
<div class="action-panel__label">{% trans '我的评论' %}</div>
<div class="action-panel__button-group action-panel__button-group--center">
<a href="{% url 'music:create_album_review' album.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<div id="modals">
<div class="mark-modal modal">
<div class="mark-modal__head">
{% if not mark %}
<style>
.mark-modal__title::after {
content: "{% trans '这部作品' %}";
}
</style>
<span class="mark-modal__title"></span>
{% else %}
<span class="mark-modal__title">{% trans '我的标记' %}</span>
{% endif %}
<span class="mark-modal__close-button modal-close">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="mark-modal__body">
<form action="{% url 'music:create_update_album_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.album }}
{% if mark.rating %}
{% endif %}
<div class="mark-modal__rating-star rating-star-edit"></div>
{{ mark_form.rating }}
<div id="statusSelection" class="mark-modal__status-radio" {% if not mark %}hidden{% endif %}>
{{ mark_form.status }}
</div>
<div class="mark-modal__clear"></div>
{{ mark_form.text }}
<div class="mark-modal__tag">
<label>{{ mark_form.tags.label }}</label>
{{ mark_form.tags }}
</div>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>{{ mark_form.is_private.label }}:</span>
{{ mark_form.is_private }}
</div>
<div class="mark-modal__share-checkbox">
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
</div>
</div>
<div class="mark-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '提交' %}">
</div>
</form>
</div>
</div>
<div class="confirm-modal modal">
<div class="confirm-modal__head">
<span class="confirm-modal__title">{% trans '确定要删除你的标记吗?' %}</span>
<span class="confirm-modal__close-button modal-close">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="confirm-modal__body">
<div class="confirm-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '确认' %}">
</div>
</div>
</div>
</div>
<div class="bg-mask"></div>
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,169 @@
{% 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 - ' %}{{ album.title }}{% trans '的标记' %}</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>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'music:retrieve_album' album.id %}">{{ album.title }}</a>{% trans '的标记' %}
</h5>
<ul class="entity-marks__mark-list">
{% for mark in marks %}
<li class="entity-marks__mark entity-marks__mark--wider">
<a href="{% url 'users:home' mark.owner.id %}"
class="entity-marks__owner-link">{{ mark.owner.username }}</a>
<span>{{ mark.get_status_display }}</span>
{% if mark.rating %}
<span class="entity-marks__rating-star rating-star"
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">
<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">{{ mark.edited_time }}</span>
{% if mark.text %}
<p class="entity-marks__mark-content">{{ mark.text }}</p>
{% endif %}
</li>
{% empty %}
<div>
{% trans '无结果' %}
</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if marks.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ marks.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</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">&rsaquo;</a>
<a href="?page={{ marks.pagination.last_page }}"
class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_album' album.id %}"><img src="{{ album.cover.url }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}"><span
class="source-label source-label__{{ album.source_site }}">
{{ album.get_source_site_display }}</span></a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ album.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ album.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,156 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
<title>{% trans 'NiceDB - 评论详情' %}</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>
<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">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.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 %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.id %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link"
href="{% url 'music:update_album_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link"
href="{% url 'music:delete_album_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_album' album.id %}"><img src="{{ album.cover.url }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}">
<span class="source-label source-label__{{ album.source_site }}">
{{ album.get_source_site_display }}
</span>
</a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ album.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ album.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -0,0 +1,166 @@
{% 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 - ' %}{{ album.title }}{% trans '的评论' %}</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>
<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">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'music:retrieve_album' album.id %}">{{ album.title }}</a>{% trans '的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<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">
<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>
<span href="{% url 'music:retrieve_album_review' review.id %}"
class="entity-reviews__review-title"><a
href="{% url 'music:retrieve_album_review' review.id %}">{{ review.title
}}</a></span>
</li>
{% empty %}
<div>{% trans '无结果' %}</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if reviews.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ reviews.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in reviews.pagination.page_range %}
{% if page == reviews.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 reviews.pagination.has_next %}
<a href="?page={{ reviews.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ reviews.pagination.last_page }}"
class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_album' album.id %}"><img src="{{ album.cover.url }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}"><span
class="source-label source-label__{{ album.source_site }}">
{{ album.get_source_site_display }}</span></a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ album.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ album.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,94 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<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">
<div class="single-section-wrapper" id="main">
<a href="{% url 'music:scrape_album' %}"
class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<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 }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -0,0 +1,131 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="{% static 'js/create_update_review.js' %}"></script>
<script src="{% static 'lib/js/rating-star.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">
<div class="grid">
<div class="single-section-wrapper">
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_album' album.id %}">
<img src="{{ album.cover.url }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_album' album.id %}">
{{ album.title }}
</a>
<a href="{{ album.source_url }}"><a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a></a>
</h5>
<div>{% if album.artist %}{% trans '艺术家:' %}
{% for artist in album.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if album.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}</div>
<div>{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}</div>
{% if album.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ album.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.album }}
<div>
{{ form.title.label }}
</div>
{{ form.title }}
<div class="clearfix">
<span class="float-left">
{{ form.content.label }}
</span>
<span class="float-right">
<span class="review-form__preview-button">{% trans '预览' %}</span>
</span>
</div>
<div id="rawContent">
{{ form.content }}
</div>
<div class="review-form__fyi">{% trans '不知道什么是Markdown可以参考' %}<a target="_blank" href="https://www.markdownguide.org/">{% trans '这里' %}</a></div>
<div class="review-form__option">
<div class="review-form__visibility-radio">
{{ form.is_private.label }}{{ form.is_private }}
</div>
<div class="review-form__share-checkbox">
{{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
</div>
</div>
<div class="clearfix">
<input class="button float-right" type="submit" value="{% trans '提交' %}">
</div>
{{ form.media }}
</form>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,97 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<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">
<div class="single-section-wrapper" id="main">
{% comment %}
<a href="{% url 'music:scrape_song' %}"
class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}
</a>
{% endcomment %}
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<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 }}
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
<input class="button" type="submit" value="{% trans '提交' %}">
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
// when source site is this site, hide url input box and populate it with fake url
// the backend would update this field
if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
}
$("select[name='source_site']").change(function () {
let value = $(this).val();
if (value == "{{ this_site_enum_value }}") {
$("input[name='source_url']").hide();
$("label[for='id_source_url']").hide();
$("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
} else {
$("input[name='source_url']").show();
$("label[for='id_source_url']").show();
$("input[name='source_url']").val("");
}
});
</script>
</body>
</html>

View file

@ -0,0 +1,135 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - ' %}{{ title }}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="{% static 'js/create_update_review.js' %}"></script>
<script src="{% static 'lib/js/rating-star.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">
<div class="grid">
<div class="single-section-wrapper">
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_song' song.id %}">
<img src="{{ song.cover.url }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}</div>
{% if song.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}"> </span>
<span class="entity-card__rating-score rating-score"> {{ song.rating }} </span>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<form action="{{ submit_url }}" method="post" class="review-form">
{% csrf_token %}
{{ form.song }}
<div>
{{ form.title.label }}
</div>
{{ form.title }}
<div class="clearfix">
<span class="float-left">
{{ form.content.label }}
</span>
<span class="float-right">
<span class="review-form__preview-button">{% trans '预览' %}</span>
</span>
</div>
<div id="rawContent">
{{ form.content }}
</div>
<div class="review-form__fyi">{% trans '不知道什么是Markdown可以参考' %}<a target="_blank" href="https://www.markdownguide.org/">{% trans '这里' %}</a></div>
<div class="review-form__option">
<div class="review-form__visibility-radio">
{{ form.is_private.label }}{{ form.is_private }}
</div>
<div class="review-form__share-checkbox">
{{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
</div>
</div>
<div class="clearfix">
<input class="button float-right" type="submit" value="{% trans '提交' %}">
</div>
{{ form.media }}
</form>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,104 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - 删除音乐' %}</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>
<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">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_album' album.id %}">
<img src="{{ album.cover.url }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'music:retrieve_album' album.id %}">
<h5 class="entity-card__title">
{{ album.title }}
<a href="{{ album.source_url }}"><span class="source-label source-label__{{ album.source_site }}">{{ album.get_source_site_display }}</span></a>
</h5>
</a>
{% if album.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ album.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if album.last_editor %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' album.last_editor.id %}">
<span>{{ album.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ album.edited_time }}</div>
{% if album.album_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ album.album_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if album.album_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ album.album_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_album' album.id %}" method="post" class="float-right">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '确认' %}">
</form>
<button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,107 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - 删除评论' %}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.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">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这篇评论吗?' %}</h5>
<div class="dividing-line"></div>
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.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 %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.id %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_album_review' review.id %}" method="post" class="float-right">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '确认' %}">
</form>
<button onclick="history.back()"
class="button button-clear float-right">{% trans '返回' %}</button>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -0,0 +1,104 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - 删除音乐' %}</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>
<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">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}</h5>
<div class="entity-card entity-card--horizontal">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_song' song.id %}">
<img src="{{ song.cover.url }}" alt="" class="item-image float-left">
</a>
</div>
<div class="entity-card__info-wrapper entity-card__info-wrapper--horizontal">
<a href="{% url 'music:retrieve_song' song.id %}">
<h5 class="entity-card__title">
{{ song.title }}
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
</a>
{% if song.rating %}
{% trans '评分:' %}<span class="entity-card__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}">
</span>
<span class="entity-card__rating-score">{{ song.rating }}</span>
{% else %}
<span>{% trans '评分:暂无评分' %}</span>
{% endif %}
{% if song.last_editor %}
<div>
{% trans '最近编辑者:' %}
<a href="{% url 'users:home' song.last_editor.id %}">
<span>{{ song.last_editor | default:"" }}</span>
</a>
</div>
{% endif %}
<div>{% trans '上次编辑时间:' %}{{ song.edited_time }}</div>
{% if song.song_marks.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ song.song_marks.count }}</a> 个标记</strong></div>
{% endif %}
{% if song.song_reviews.all %}
<div><strong>{% trans '这个条目有' %} <a href="javascript:void();">{{ song.song_reviews.count }}</a> 个评论</strong></div>
{% endif %}
</div>
</div>
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_song' song.id %}" method="post" class="float-right">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '确认' %}">
</form>
<button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,107 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - 删除评论' %}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.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">
<div class="grid">
<div class="single-section-wrapper" id="main">
<h5>{% trans '确认删除这篇评论吗?' %}</h5>
<div class="dividing-line"></div>
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.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 %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.id %}"
class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star"
data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
</div>
</div>
<div id="rawContent" class="delete-preview">
{{ form.content }}
</div>
{{ form.media }}
<div class="dividing-line"></div>
<div class="clearfix">
<form action="{% url 'music:delete_song_review' review.id %}" method="post" class="float-right">
{% csrf_token %}
<input class="button" type="submit" value="{% trans '确认' %}">
</form>
<button onclick="history.back()"
class="button button-clear float-right">{% trans '返回' %}</button>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
$(".markdownx textarea").hide();
$(".markdownx .markdownx-preview").show();
</script>
</body>
</html>

View file

@ -0,0 +1,109 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<style>
#scrape {
overflow: auto;
}
#scrape iframe {
width: 100%;
}
#scrape textarea {
height: 200px;
resize: vertical;
}
#scrape iframe {
height: 500px;
}
</style>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid grid--reverse-order">
<div class="grid__main grid__main--reverse-order" id="main">
<div class="main-section-wrapper">
<div id="scrape">
<h5>
{% trans '根据豆瓣内容填写下方表单' %}
</h5>
<iframe id='test' sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="https://search.douban.com/music/subject_search{% if q %}?search_text={{ q }}{% endif %}" frameborder="0"></iframe>
<div class="dividing-line"></div>
<div id="scrapeForm">
<form action="{% url 'music:create_album' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.id_for_label == 'id_is_series' %}
<label for="{{ field.id_for_label }}" style="display: inline-block; position: relative; left: -4px;">{{ field.label }}</label>
{{ field }}
{% else %}
{% if field.id_for_label != 'id_id' %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
</form>
<a href="#" class="button add-button submit">{% trans '剽取!' %}</a>
</div>
</div>
</div>
</div>
<div class="grid__aside grid__aside--reverse-order" id="aside">
<div class="aside-section-wrapper aside-section-wrapper--singular">
<h5>
{% trans '复制详情页链接' %}
</h5>
<form action="{% url 'music:click_to_scrape_album' %}" method="post">
{% csrf_token %}
<input type="text" name="url" required placeholder="https://music.douban.com/subject/1000000/">
<input type="submit" class="button add-button" value="{% trans '一键剽取!' %}">
</form>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -0,0 +1,109 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="{% static 'js/scrape.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
</head>
<body>
<style>
#scrape {
overflow: auto;
}
#scrape iframe {
width: 100%;
}
#scrape textarea {
height: 200px;
resize: vertical;
}
#scrape iframe {
height: 500px;
}
</style>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid grid--reverse-order">
<div class="grid__main grid__main--reverse-order" id="main">
<div class="main-section-wrapper">
<div id="scrape">
<h5>
{% trans '根据豆瓣内容填写下方表单' %}
</h5>
<iframe id='test' sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src="https://search.douban.com/movie/subject_search{% if q %}?search_text={{ q }}{% endif %}" frameborder="0"></iframe>
<div class="dividing-line"></div>
<div id="scrapeForm">
<form action="{% url 'movies:create' %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{% for field in form %}
{% if field.id_for_label == 'id_is_series' %}
<label for="{{ field.id_for_label }}" style="display: inline-block; position: relative; left: -4px;">{{ field.label }}</label>
{{ field }}
{% else %}
{% if field.id_for_label != 'id_id' %}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% endif %}
{{ field }}
{% endif %}
{% endfor %}
</form>
<a href="#" class="button add-button submit">{% trans '剽取!' %}</a>
</div>
</div>
</div>
</div>
<div class="grid__aside grid__aside--reverse-order" id="aside">
<div class="aside-section-wrapper aside-section-wrapper--singular">
<h5>
{% trans '复制详情页链接' %}
</h5>
<form action="{% url 'movies:click_to_scrape' %}" method="post">
{% csrf_token %}
<input type="text" name="url" required placeholder="https://movie.douban.com/subject/1000000/">
<input type="submit" class="button add-button" value="{% trans '一键剽取!' %}">
</form>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<script>
// mark required
$("#content *[required]").each(function () {
$(this).prev().prepend("*");
});
$('form').submit(function () {
$(this).find("input[type='submit']").prop('disabled', true);
$(this).find("button[type='submit']").prop('disabled', true);
});
</script>
</body>
</html>

View file

@ -0,0 +1,400 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load strip_scheme %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="NiceDB音乐 - {{ song.title }}">
<meta property="og:type" content="music.song">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ song.cover.url }}">
<meta property="og:site_name" content="NiceDB">
<meta property="og:description"content="{{ song.brief }}">
<title>{% trans 'NiceDB - 音乐详情' %} | {{ song.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/detail.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-detail">
<img src="{{ song.cover.url }}" class="entity-detail__img" alt="{{ song.title }}">
<div class="entity-detail__info">
<h5 class="entity-detail__title">
{{ song.title }}
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if song.rating %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ song.rating }} </span>
{% else %}
<span> {% trans '评分:暂无评分' %}</span>
{% endif %}
</div>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if song.release_date %}
{% trans '发行日期:' %}{{ song.release_date }}
{% endif %}
</div>
<div>{% if song.duration %}
{% trans '时长:' %}{{ song.get_duration_display }}
{% endif %}
</div>
<div>{% if song.genre %}
{% trans '流派:' %}{{ song.genre }}
{% endif %}
</div>
</div>
<div class="entity-detail__fields">
<div>{% if song.isrc %}
{% trans 'ISRC' %}{{ song.isrc }}
{% endif %}
</div>
<div>{% if song.album %}
{% trans '所属专辑:' %}<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
{% if song.other_info %}
{% for k, v in song.other_info.items %}
<div>
{{k}}{{v}}
</div>
{% endfor %}
{% endif %}
{% if song.last_editor %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.id %}">{{ song.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'music:update_song' song.id %}">{% trans '编辑这个作品' %}</a>
{% if user.is_staff %}
/<a href="{% url 'music:delete_song' song.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
<div class="tag-collection">
{% for tag_dict in song_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>
</div>
<div class="dividing-line"></div>
{% if song.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
<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>
</div>
{% endif %}
<div class="entity-marks">
<h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
{% if mark_list_more %}
<a href="{% url 'music:retrieve_song_mark_list' song.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
{% endif %}
{% if mark_list %}
<ul class="entity-marks__mark-list">
{% for others_mark in mark_list %}
<li class="entity-marks__mark">
<a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
<span>{{ others_mark.get_status_display }}</span>
{% if others_mark.rating %}
<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"><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 %}
<p class="entity-marks__mark-content">{{ others_mark.text }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div>{% trans '暂无标记' %}</div>
{% endif %}
</div>
<div class="entity-reviews">
<h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
{% if review_list_more %}
<a href="{% url 'music:retrieve_song_review_list' song.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
{% endif %}
{% if review_list %}
<ul class="entity-reviews__review-list">
{% for others_review in review_list %}
<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"><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>
<span>{{ others_review.get_plain_content | truncate:100 }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<div>{% trans '暂无评论' %}</div>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
{% if mark %}
<div class="mark-panel">
<span class="mark-panel__status">{% trans '我' %}{{ mark.get_status_display }}</span>
{% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%}
{% if mark.rating %}
<span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% 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="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
<form action="{% url 'music:delete_song_mark' mark.id %}" method="post">
{% csrf_token %}
<a href="" class="delete">{% trans '删除' %}</a>
</form>
</span>
<div class="mark-panel__clear"></div>
<div class="mark-panel__time">{{ mark.edited_time }}</div>
{% if mark.text %}
<p class="mark-panel__text">{{ mark.text }}</p>
{% endif %}
<div class="tag-collection">
{% for tag in mark_tags %}
<span class="tag-collection__tag">{{ tag }}</span>
{% endfor %}
</div>
</div>
{% else %}
<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>
</div>
</div>
{% endif %}
</div>
<div class="aside-section-wrapper">
{% if review %}
<div class="review-panel">
<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"><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">
<a href="{% url 'music:update_song_review' review.id %}">{% trans '编辑' %}</a>
<a href="{% url 'music:delete_song_review' review.id %}">{% trans '删除' %}</a>
</span>
<div class="review-panel__time">{{ review.edited_time }}</div>
<a href="{% url 'music:retrieve_song_review' review.id %}" class="review-panel__review-title">
{{ review.title }}
</a>
</div>
{% else %}
<div class="action-panel">
<div class="action-panel__label">{% trans '我的评论' %}</div>
<div class="action-panel__button-group action-panel__button-group--center">
<a href="{% url 'music:create_song_review' song.id %}">
<button class="action-panel__button">{% trans '去写评论' %}</button>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
<div id="modals">
<div class="mark-modal modal">
<div class="mark-modal__head">
{% if not mark %}
<style>
.mark-modal__title::after {
content: "{% trans '这部作品' %}";
}
</style>
<span class="mark-modal__title"></span>
{% else %}
<span class="mark-modal__title">{% trans '我的标记' %}</span>
{% endif %}
<span class="mark-modal__close-button modal-close">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="mark-modal__body">
<form action="{% url 'music:create_update_song_mark' %}" method="post">
{{ mark_form.media }}
{% csrf_token %}
{{ mark_form.id }}
{{ mark_form.song }}
{% if mark.rating %}
{% endif %}
<div class="mark-modal__rating-star rating-star-edit"></div>
{{ mark_form.rating }}
<div id="statusSelection" class="mark-modal__status-radio" {% if not mark %}hidden{% endif %}>
{{ mark_form.status }}
</div>
<div class="mark-modal__clear"></div>
{{ mark_form.text }}
<div class="mark-modal__tag">
<label>{{ mark_form.tags.label }}</label>
{{ mark_form.tags }}
</div>
<div class="mark-modal__option">
<div class="mark-modal__visibility-radio">
<span>{{ mark_form.is_private.label }}:</span>
{{ mark_form.is_private }}
</div>
<div class="mark-modal__share-checkbox">
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
</div>
</div>
<div class="mark-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '提交' %}">
</div>
</form>
</div>
</div>
<div class="confirm-modal modal">
<div class="confirm-modal__head">
<span class="confirm-modal__title">{% trans '确定要删除你的标记吗?' %}</span>
<span class="confirm-modal__close-button modal-close">
<span class="icon-cross">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<polygon
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
</polygon>
</svg>
</span>
</span>
</div>
<div class="confirm-modal__body">
<div class="confirm-modal__confirm-button">
<input type="submit" class="button float-right" value="{% trans '确认' %}">
</div>
</div>
</div>
</div>
<div class="bg-mask"></div>
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,175 @@
{% 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 - ' %}{{ song.title }}{% trans '的标记' %}</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>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-marks">
<h5 class="entity-marks__title entity-marks__title--stand-alone">
<a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans '
的标记' %}
</h5>
<ul class="entity-marks__mark-list">
{% for mark in marks %}
<li class="entity-marks__mark entity-marks__mark--wider">
<a href="{% url 'users:home' mark.owner.id %}"
class="entity-marks__owner-link">{{ mark.owner.username }}</a>
<span>{{ mark.get_status_display }}</span>
{% if mark.rating %}
<span class="entity-marks__rating-star rating-star"
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">
<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">{{ mark.edited_time }}</span>
{% if mark.text %}
<p class="entity-marks__mark-content">{{ mark.text }}</p>
{% endif %}
</li>
{% empty %}
<div>
{% trans '无结果' %}
</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if marks.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ marks.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</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">&rsaquo;</a>
<a href="?page={{ marks.pagination.last_page }}"
class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_song' song.id %}"><img src="{{ song.cover.url }}"
alt="" class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span
class="source-label source-label__{{ song.source_site }}">{{
song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
</div>
{% if song.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ song.rating | floatformat:" 0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ song.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

View file

@ -0,0 +1,152 @@
{% load static %}
{% load i18n %}
{% load l10n %}
{% load humanize %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
<meta property="og:type" content="article">
<meta property="og:article:author" content="{{ review.owner.username }}">
<meta property="og:url" content="{{ request.build_absolute_uri }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
<title>{% trans 'NiceDB - 评论详情' %}</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>
<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">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="review-head">
<h5 class="review-head__title">
{{ review.title }}
</h5>
{% if review.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 %}
<div class="review-head__body">
<div class="review-head__info">
<a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
{% if mark %}
{% if mark.rating %}
<span class="review-head__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% endif %}
<span class="review-head__time">{{ review.edited_time }}</span>
</div>
<div class="review-head__actions">
{% if request.user == review.owner %}
<a class="review-head__action-link" href="{% url 'music:update_song_review' review.id %}">{% trans '编辑' %}</a>
<a class="review-head__action-link" href="{% url 'music:delete_song_review' review.id %}">{% trans '删除' %}</a>
{% endif %}
</div>
</div>
<!-- <div class="dividing-line"></div> -->
<div id="rawContent">
{{ form.content }}
</div>
{{ form.media }}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_song' song.id %}"><img src="{{ song.cover.url }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}</div>
{% if song.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ song.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
$(".markdownx textarea").hide();
</script>
</body>
</html>

View file

@ -0,0 +1,158 @@
{% 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 - ' %}{{ song.title }}{% trans '的评论' %}</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>
<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">
<div class="grid">
<div class="grid__main" id="main">
<div class="main-section-wrapper">
<div class="entity-reviews">
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
<a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans ' 的评论' %}
</h5>
<ul class="entity-reviews__review-list">
{% for review in reviews %}
<li class="entity-reviews__review entity-reviews__review--wider">
<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"><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>
<span href="{% url 'music:retrieve_song_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'music:retrieve_song_review' review.id %}">{{ review.title }}</a></span>
</li>
{% empty %}
<div>{% trans '无结果' %}</div>
{% endfor %}
</ul>
</div>
<div class="pagination">
{% if reviews.pagination.has_prev %}
<a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ reviews.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in reviews.pagination.page_range %}
{% if page == reviews.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 reviews.pagination.has_next %}
<a href="?page={{ reviews.next_page_number }}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ reviews.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
{% endif %}
</div>
</div>
</div>
<div class="grid__aside" id="aside">
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'music:retrieve_song' song.id %}"><img src="{{ song.cover.url }}" alt=""
class="entity-card__img"></a>
</div>
<div class="entity-card__info-wrapper">
<h5 class="entity-card__title"><a href="{% url 'music:retrieve_song' song.id %}">
{{ song.title }}
</a>
<a href="{{ song.source_url }}"><span class="source-label source-label__{{ song.source_site }}">{{ song.get_source_site_display }}</span></a>
</h5>
<div>{% if song.artist %}{% trans '艺术家:' %}
{% for artist in song.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if song.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").click(function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}</div>
<div>{% if song.album %}{% trans '所属专辑:' %}
<a href="{% url 'music:retrieve_album' song.album.id %}">{{ song.album }}</a>
{% endif %}
</div>
<div>{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}</div>
{% if song.rating %}
{% trans '评分: ' %}<span class="entity-card__rating-star rating-star"
data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
<span class="entity-card__rating-score rating-score">{{ song.rating }}</span>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% comment %}
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
<div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
<!--current user mastodon id-->
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
{% endcomment %}
<script>
</script>
</body>
</html>

3
music/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

40
music/urls.py Normal file
View file

@ -0,0 +1,40 @@
from django.urls import path
from .views import *
app_name = 'music'
urlpatterns = [
path('song/create/', create_song, name='create_song'),
path('song/<int:id>/', retrieve_song, name='retrieve_song'),
path('song/update/<int:id>/', update_song, name='update_song'),
path('song/delete/<int:id>/', delete_song, name='delete_song'),
path('song/mark/', create_update_song_mark, name='create_update_song_mark'),
path('song/<int:song_id>/mark/list/',
retrieve_song_mark_list, name='retrieve_song_mark_list'),
path('song/mark/delete/<int:id>/', delete_song_mark, name='delete_song_mark'),
path('song/<int:song_id>/review/create/', create_song_review, name='create_song_review'),
path('song/review/update/<int:id>/', update_song_review, name='update_song_review'),
path('song/review/delete/<int:id>/', delete_song_review, name='delete_song_review'),
path('song/review/<int:id>/', retrieve_song_review, name='retrieve_song_review'),
path('song/<int:song_id>/review/list/',
retrieve_song_review_list, name='retrieve_song_review_list'),
# path('song/scrape/', scrape_song, name='scrape_song'),
path('song/click_to_scrape/', click_to_scrape_song, name='click_to_scrape_song'),
path('album/create/', create_album, name='create_album'),
path('album/<int:id>/', retrieve_album, name='retrieve_album'),
path('album/update/<int:id>/', update_album, name='update_album'),
path('album/delete/<int:id>/', delete_album, name='delete_album'),
path('album/mark/', create_update_album_mark, name='create_update_album_mark'),
path('album/<int:album_id>/mark/list/',
retrieve_album_mark_list, name='retrieve_album_mark_list'),
path('album/mark/delete/<int:id>/', delete_album_mark, name='delete_album_mark'),
path('album/<int:album_id>/review/create/', create_album_review, name='create_album_review'),
path('album/review/update/<int:id>/', update_album_review, name='update_album_review'),
path('album/review/delete/<int:id>/', delete_album_review, name='delete_album_review'),
path('album/review/<int:id>/', retrieve_album_review, name='retrieve_album_review'),
path('album/<int:album_id>/review/list/',
retrieve_album_review_list, name='retrieve_album_review_list'),
path('album/scrape/', scrape_album, name='scrape_album'),
path('album/click_to_scrape/', click_to_scrape_album, name='click_to_scrape_album'),
]

1182
music/views.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -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>

View 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">&laquo;</a>
<a href="?page={{ marks.previous_page_number }}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</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">&rsaquo;</a>
<a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</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>

View file

@ -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的痕迹。
这里仍是一片处女地,丰富的内容需要大家共同创造。

View file

@ -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'),

View file

@ -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
@ -108,6 +110,14 @@ def register(request):
elif request.method == 'POST':
token = request.session['new_user_token']
user_data = get_user_data(request.COOKIES['mastodon_domain'], token)
if user_data is None:
return render(
request,
'common/error.html',
{
'msg': _("长毛象访问失败😫")
}
)
new_user = User(
username=user_data['username'],
mastodon_id=user_data['id'],
@ -372,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',
@ -441,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',
@ -455,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':