finish music part

This commit is contained in:
doubaniux 2021-02-15 21:27:50 +01:00
parent e5bc7f55c0
commit 575123f848
46 changed files with 1323 additions and 265 deletions

View file

@ -30,3 +30,8 @@ urlpatterns = [
path('', include('common.urls')),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

View file

@ -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):
@ -176,6 +171,55 @@ class HstoreField(forms.CharField):
return pairs
class DurationInput(forms.TextInput):
"""
HH:mm:ss input widget
"""
input_type = "time"
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
# context['widget']['type'] = self.input_type
context['widget']['attrs']['step'] = "1"
return context
def format_value(self, value):
"""
Given `value` is an integer in ms
"""
ms = value
if not ms:
return super().format_value(None)
x = ms // 1000
seconds = x % 60
x //= 60
if x == 0:
return super().format_value(f"00:00:{seconds:0>2}")
minutes = x % 60
x //= 60
if x == 0:
return super().format_value(f"00:{minutes:0>2}:{seconds:0>2}")
hours = x % 24
return super().format_value(f"{hours:0>2}:{minutes:0>2}:{seconds:0>2}")
class DurationField(forms.TimeField):
widget = DurationInput
def to_python(self, value):
# empty value
if value is None or value == '':
return
# if value is integer in ms
if isinstance(value, int):
return value
# if value is string in time format
h, m, s = value.split(':')
return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000
#############################
# Form
#############################

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

View file

@ -2,11 +2,19 @@ import requests
import functools
import random
import logging
from lxml import html
import re
import dateparser
import datetime
import time
from lxml import html
from mimetypes import guess_extension
from threading import Thread
from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG
from boofilsic.settings import SPOTIFY_CREDENTIAL
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from common.models import SourceSiteEnum
from movies.models import Movie, MovieGenreEnum
from movies.forms import MovieForm
@ -56,15 +64,19 @@ def log_url(func):
try:
return func(*args, **kwargs)
except Exception as e:
# log the url
# log the url and trace stack
logger.error(f"Scrape Failed URL: {args[1]}")
logger.error(str(e))
logger.error("Expections during scraping:", exc_info=e)
raise e
return wrapper
class AbstractScraper:
"""
Scrape entities. The entities means those defined in the models.py file,
like Book, Movie......
"""
# subclasses must specify those two variables
# site means general sites, like amazon/douban etc
@ -77,6 +89,10 @@ class AbstractScraper:
form_class = None
# used to extract effective url
regex = None
# scraped raw image
raw_img = None
# scraped raw data
raw_data = {}
def __init_subclass__(cls, **kwargs):
# this statement initialize the subclasses
@ -107,11 +123,15 @@ class AbstractScraper:
Scrape/request model schema specified data from given url and return it.
Implementations of subclasses to this method would be decorated as class method.
return (data_dict, image)
Should set the `raw_data` and the `raw_img`
"""
raise NotImplementedError("Subclass should implement this method")
@classmethod
def get_effective_url(cls, raw_url):
"""
The return value should be identical with that saved in DB as `source_url`
"""
url = cls.regex.findall(raw_url)
if not url:
raise ValueError("not valid url")
@ -166,7 +186,25 @@ class AbstractScraper:
)
if img_response.status_code == 200:
raw_img = img_response.content
return raw_img
content_type = img_response.headers.get('Content-Type')
ext = guess_extension(content_type.partition(';')[0].strip())
return raw_img, ext
@classmethod
def save(cls, request_user):
entity_cover = {
'cover': SimpleUploadedFile('temp' + cls.img_ext, cls.raw_img)
}
form = cls.form_class(cls.raw_data, entity_cover)
if form.is_valid():
form.instance.last_editor = request_user
form.save()
cls.instance = form.instance
else:
logger.error(str(form.errors))
raise ValidationError("Form invalid.")
return form
class DoubanBookScraper(AbstractScraper):
@ -175,7 +213,7 @@ class DoubanBookScraper(AbstractScraper):
data_class = Book
form_class = BookForm
regex = re.compile(r"https://book.douban.com/subject/\d+/{0,1}")
regex = re.compile(r"https://book\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
@ -265,7 +303,7 @@ class DoubanBookScraper(AbstractScraper):
img_url_elem = content.xpath("//*[@id='mainpic']/a/img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img = self.download_image(img_url)
raw_img, ext = self.download_image(img_url)
# there are two html formats for authors and translators
authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
@ -326,6 +364,7 @@ class DoubanBookScraper(AbstractScraper):
'source_site': self.site_name,
'source_url': self.get_effective_url(url),
}
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
@ -335,7 +374,7 @@ class DoubanMovieScraper(AbstractScraper):
data_class = Movie
form_class = MovieForm
regex = re.compile(r"https://movie.douban.com/subject/\d+/{0,1}")
regex = re.compile(r"https://movie\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
@ -473,7 +512,7 @@ class DoubanMovieScraper(AbstractScraper):
img_url_elem = content.xpath("//img[@rel='v:image']/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img = self.download_image(img_url)
raw_img, ext = self.download_image(img_url)
data = {
'title': title,
@ -498,6 +537,7 @@ class DoubanMovieScraper(AbstractScraper):
'source_site': self.site_name,
'source_url': self.get_effective_url(url),
}
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
@ -507,7 +547,7 @@ class DoubanAlbumScraper(AbstractScraper):
data_class = Album
form_class = AlbumForm
regex = re.compile(r"https://music.douban.com/subject/\d+/{0,1}")
regex = re.compile(r"https://music\.douban\.com/subject/\d+/{0,1}")
def scrape(self, url):
headers = DEFAULT_REQUEST_HEADERS.copy()
@ -533,7 +573,7 @@ class DoubanAlbumScraper(AbstractScraper):
date_elem = content.xpath(
"//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
release_date = dateparser.parse(date_elem[0].strip(), settings={
'PREFER_DAY_OF_MONTH': 'first'}) if date_elem else None
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)}) if date_elem else None
company_elem = content.xpath(
"//div[@id='info']//span[text()='出版者:']/following::text()[1]")
@ -581,7 +621,7 @@ class DoubanAlbumScraper(AbstractScraper):
img_url_elem = content.xpath("//div[@id='mainpic']//img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None
raw_img = self.download_image(img_url)
raw_img, ext = self.download_image(img_url)
data = {
'title': title,
@ -596,4 +636,285 @@ class DoubanAlbumScraper(AbstractScraper):
'source_site': self.site_name,
'source_url': self.get_effective_url(url),
}
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
spotify_token = None
spotify_token_expire_time = time.time()
class SpotifyTrackScraper(AbstractScraper):
site_name = SourceSiteEnum.SPOTIFY.value
# API URL
host = 'https://open.spotify.com/track/'
data_class = Song
form_class = SongForm
regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+")
def scrape(self, url):
"""
Request from API, not really scraping
"""
global spotify_token, spotify_token_expire_time
if spotify_token is None or is_spotify_token_expired():
invoke_spotify_token()
effective_url = self.get_effective_url(url)
if effective_url is None:
raise ValueError("not valid url")
api_url = self.get_api_url(effective_url)
headers = {
'Authorization': f"Bearer {spotify_token}"
}
r = requests.get(api_url, headers=headers)
res_data = r.json()
artist = []
for artist_dict in res_data['artists']:
artist.append(artist_dict['name'])
if not artist:
artist = None
title = res_data['name']
release_date = dateparser.parse(
res_data['album']['release_date'],
settings={
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)
}
)
duration = res_data['duration_ms']
if res_data['external_ids'].get('isrc'):
isrc = res_data['external_ids']['isrc']
else:
isrc = None
raw_img, ext = self.download_image(res_data['album']['images'][0]['url'])
data = {
'title': title,
'artist': artist,
'genre': None,
'release_date': release_date,
'duration': duration,
'isrc': isrc,
'album': None,
'brief': None,
'other_info': None,
'source_site': self.site_name,
'source_url': effective_url,
}
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
@classmethod
def get_effective_url(cls, raw_url):
code = cls.regex.findall(raw_url)
if code:
return f"https://open.spotify.com/track/{code[0]}"
else:
return None
@classmethod
def get_api_url(cls, url):
return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0]
class SpotifyAlbumScraper(AbstractScraper):
site_name = SourceSiteEnum.SPOTIFY.value
# API URL
host = 'https://open.spotify.com/album/'
data_class = Album
form_class = AlbumForm
regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+")
def scrape(self, url):
"""
Request from API, not really scraping
"""
global spotify_token, spotify_token_expire_time
if spotify_token is None or is_spotify_token_expired():
invoke_spotify_token()
effective_url = self.get_effective_url(url)
if effective_url is None:
raise ValueError("not valid url")
api_url = self.get_api_url(effective_url)
headers = {
'Authorization': f"Bearer {spotify_token}"
}
r = requests.get(api_url, headers=headers)
res_data = r.json()
artist = []
for artist_dict in res_data['artists']:
artist.append(artist_dict['name'])
title = res_data['name']
genre = ', '.join(res_data['genres'])
company = []
for com in res_data['copyrights']:
company.append(com['text'])
duration = 0
track_list = []
track_urls = []
for track in res_data['tracks']['items']:
track_urls.append(track['external_urls']['spotify'])
duration += track['duration_ms']
if res_data['tracks']['items'][-1]['disc_number'] > 1:
# more than one disc
track_list.append(str(
track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name'])
else:
track_list.append(str(track['track_number']) + '. ' + track['name'])
track_list = '\n'.join(track_list)
release_date = dateparser.parse(
res_data['release_date'],
settings={
"RELATIVE_BASE": datetime.datetime(1900, 1, 1)
}
)
other_info = {}
if res_data['external_ids'].get('upc'):
# bar code
other_info['UPC'] = res_data['external_ids']['upc']
raw_img, ext = self.download_image(res_data['images'][0]['url'])
data = {
'title': title,
'artist': artist,
'genre': genre,
'track_list': track_list,
'release_date': release_date,
'duration': duration,
'company': company,
'brief': None,
'other_info': other_info,
'source_site': self.site_name,
'source_url': effective_url,
}
# set tracks_data, used for adding tracks
self.track_urls = track_urls
self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
return data, raw_img
@classmethod
def get_effective_url(cls, raw_url):
code = cls.regex.findall(raw_url)
if code:
return f"https://open.spotify.com/album/{code[0]}"
else:
return None
@classmethod
def save(cls, request_user):
form = super().save(request_user)
task = Thread(
target=cls.add_tracks,
args=(form.instance, request_user),
daemon=True
)
task.start()
return form
@classmethod
def get_api_url(cls, url):
return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0]
@classmethod
def add_tracks(cls, album: Album, request_user):
to_be_updated_tracks = []
for track_url in cls.track_urls:
track = cls.get_track_or_none(track_url)
# seems lik if fire too many requests at the same time
# spotify would limit access
if track is None:
task = Thread(
target=cls.scrape_and_save_track,
args=(track_url, album, request_user),
daemon=True
)
task.start()
task.join()
else:
to_be_updated_tracks.append(track)
cls.bulk_update_track_album(to_be_updated_tracks, album, request_user)
@classmethod
def get_track_or_none(cls, track_url: str):
try:
instance = Song.objects.get(source_url=track_url)
return instance
except ObjectDoesNotExist:
return None
@classmethod
def scrape_and_save_track(cls, url: str, album: Album, request_user):
data, img = SpotifyTrackScraper.scrape(url)
SpotifyTrackScraper.raw_data['album'] = album
SpotifyTrackScraper.save(request_user)
@classmethod
def bulk_update_track_album(cls, tracks, album, request_user):
for track in tracks:
track.last_editor = request_user
track.edited_time = timezone.now()
track.album = album
Song.objects.bulk_update(tracks, [
'last_editor',
'edited_time',
'album'
])
def is_spotify_token_expired():
global spotify_token_expire_time
return True if spotify_token_expire_time <= time.time() else False
def invoke_spotify_token():
global spotify_token, spotify_token_expire_time
r = requests.post(
"https://accounts.spotify.com/api/token",
data={
"grant_type": "client_credentials"
},
headers={
"Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
}
)
data = r.json()
if r.status_code == 401:
# token expired, try one more time
# this maybe caused by external operations,
# for example debugging using a http client
r = requests.post(
"https://accounts.spotify.com/api/token",
data={
"grant_type": "client_credentials"
},
headers={
"Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
}
)
data = r.json()
elif r.status_code != 200:
raise Exception(f"Request to spotify API fails. Reason: {r.reason}")
# minus 2 for execution time error
spotify_token_expire_time = int(data['expires_in']) + time.time() - 2
spotify_token = data['access_token']

View file

@ -1201,6 +1201,8 @@ select::placeholder {
padding-top: 2px;
font-weight: lighter;
letter-spacing: 0.1rem;
word-break: keep-all;
opacity: 0.8;
position: relative;
top: -1px;
}
@ -1211,8 +1213,16 @@ select::placeholder {
}
.source-label.source-label__douban {
border-color: #319840;
color: #319840;
border: none;
color: white;
background-color: #319840;
}
.source-label.source-label__spotify {
background-color: #1ed760;
color: black;
border: none;
font-weight: bold;
}
.main-section-wrapper {
@ -1285,9 +1295,7 @@ select::placeholder {
}
.entity-list .entity-list__entity-info--full-length {
display: block;
max-width: 100%;
margin-bottom: 12px;
}
.entity-list .entity-list__entity-brief {
@ -1327,6 +1335,8 @@ select::placeholder {
object-fit: contain;
float: left;
max-width: 150px;
-o-object-position: top;
object-position: top;
}
.entity-detail .entity-detail__info {
@ -1396,7 +1406,7 @@ select::placeholder {
}
.entity-desc .entity-desc__content--folded {
max-height: 200px;
max-height: 202px;
}
.entity-desc .entity-desc__unfold-button {
@ -1666,6 +1676,77 @@ select::placeholder {
color: #00a1cc;
}
.track-carousel {
position: relative;
overflow: auto;
scroll-behavior: smooth;
scrollbar-width: none;
margin-top: 5px;
}
.track-carousel::-webkit-scrollbar {
height: 0px;
}
.track-carousel__content {
display: -ms-grid;
display: grid;
grid-gap: 16px;
margin: auto;
-webkit-box-sizing: border-box;
box-sizing: border-box;
grid-auto-flow: column;
}
.track-carousel__track {
width: 10vw;
height: 13vw;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
}
.track-carousel__track img {
-o-object-fit: contain;
object-fit: contain;
}
.track-carousel__track-title {
white-space: nowrap;
}
.track-carousel__button {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-line-pack: center;
align-content: center;
background: white;
border: none;
padding: 8px;
border-radius: 50%;
outline: 0;
cursor: pointer;
position: absolute;
}
.track-carousel__button--prev {
top: 50%;
left: 0;
-webkit-transform: translate(50%, -50%);
transform: translate(50%, -50%);
}
.track-carousel__button--next {
top: 50%;
right: 0;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
@media (max-width: 575.98px) {
.entity-list .entity-list__entity {
-webkit-box-orient: vertical;
@ -1739,6 +1820,10 @@ select::placeholder {
.review-head .review-head__actions {
float: unset;
}
.track-carousel__track {
width: 32vw;
height: 40vw;
}
}
@media (max-width: 991.98px) {

File diff suppressed because one or more lines are too long

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
@ -13,9 +16,12 @@ $in-site-color: $color-primary
font-size: 1.1rem
margin: 3px
padding: 1px 3px
padding-top: 2px;
font-weight: lighter;
letter-spacing: 0.1rem;
padding-top: 2px
font-weight: lighter
letter-spacing: 0.1rem
word-break: keep-all
opacity: 0.8
position: relative;
top: -1px;
@ -24,6 +30,12 @@ $in-site-color: $color-primary
border-color: $in-site-color
color: $in-site-color
&.source-label__douban
border-color: $douban-color
color: $douban-color
&.source-label__amazon
border: none
color: $douban-color-secondary
background-color: $douban-color-primary
&.source-label__amazon
&.source-label__spotify
background-color: $spotify-color-primary
color: $spotify-color-secondary
border: none
font-weight: bold

View file

@ -66,9 +66,9 @@ $sub-section-title-margin: 8px
position: relative
top: 0.52em
&--full-length
display: block
// display: block
max-width: 100%
margin-bottom: 12px
// margin-bottom: 12px
& &__entity-brief
margin-top: 8px
@ -102,10 +102,11 @@ $sub-section-title-margin: 8px
.entity-detail
& &__img
height: 210px;
object-fit: contain;
float: left;
max-width: 150px;
height: 210px
object-fit: contain
float: left
max-width: 150px
object-position: top
& &__info
float: left
@ -160,7 +161,7 @@ $mark-review-padding-wider: 6px 0
& &__content
overflow: hidden
&--folded
max-height: 200px
max-height: 202px
& &__unfold-button
display: flex
@ -375,6 +376,59 @@ $mark-review-padding-wider: 6px 0
&:hover
color: $color-primary
.track-carousel
position: relative
overflow: auto
scroll-behavior: smooth
scrollbar-width: none
margin-top: 5px
// padding: 0
&::-webkit-scrollbar
height: 0px
&__content
display: grid
grid-gap: 16px
margin: auto
box-sizing: border-box
grid-auto-flow: column
// grid-template-columns: max-content
&__track
width: 10vw
height: 13vw
// grid-column: 1
text-align: center
overflow: hidden
text-overflow: ellipsis
& img
object-fit: contain
&__track-title
// word-break: keep-all
// overflow-wrap: anywhere
white-space: nowrap
&__button
display: flex
justify-content: center
align-content: center
background: white
border: none
padding: 8px
border-radius: 50%
outline: 0
cursor: pointer
position: absolute
&--prev
top: 50%
left: 0
transform: translate(50%, -50%)
&--next
top: 50%
right: 0
transform: translate(-50%, -50%)
// Small devices (landscape phones, 576px and up)
@media (max-width: $small-devices)
.entity-list
@ -439,6 +493,11 @@ $mark-review-padding-wider: 6px 0
float: unset
& &__actions
float: unset
.track-carousel
&__track
width: 32vw
height: 40vw
// Medium devices (tablets, 768px and up)

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

@ -187,7 +187,7 @@
{% if movie.genre %}{% trans '类型' %}
{% for genre in movie.get_genre_display %}
{{ genre }}{% if not forloop.last %} {% endif %}
{% endfor %}
{% endfor %}/
{% endif %}
</span>
@ -196,13 +196,13 @@
{% for actor in movie.actor %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
{% if forloop.counter <= 5 %}
{% if not forloop.counter == 5 and not forloop.last %} / {% endif %}
{% if not forloop.counter == 5 and not forloop.last %} {% endif %}
{% endif %}
{% endfor %}
{% endif %}
</span>
<p class="entity-list__entity-brief">
{{ movie.brief | truncate:170 }}
{{ movie.brief }}
</p>
<div class="tag-collection">
{% for tag_dict in movie.tag_list %}
@ -226,30 +226,37 @@
<li class="entity-list__entity">
<div class="entity-list__entity-img-wrapper">
{% comment %}
<a href="{% url 'music:retrieve' music.id %}">
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
</a>
{% endcomment %}
{% if item.category_name|lower == 'album' %}
<a href="{% url 'music:retrieve_album' music.id %}">
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
</a>
{% elif item.category_name|lower == 'song' %}
<a href="{% url 'music:retrieve_song' music.id %}">
<img src="{{ music.cover.url }}" alt="" class="entity-list__entity-img">
</a>
{% endif %}
</div>
<div class="entity-list__entity-text">
<div class="entity-list__entity-title">
{% comment %}
{% if item.category_name == 'album' %}
{% if item.category_name|lower == 'album' %}
<a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
{% if request.GET.q %}
{{ music.title | highlight:request.GET.q }}
{% else %}
{{ music.title }}
{% endif %}
</a>
{% elif item.category_name = 'song' %}
{% elif item.category_name|lower == 'song' %}
<a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
{% if request.GET.q %}
{{ music.title | highlight:request.GET.q }}
{% else %}
{{ music.title }}
{% endif %}
</a>
{% endif %}
{% endcomment %}
{% if not request.GET.c or request.GET.c != 'music' and request.GET.c != 'book' and request.GET.c != 'music' %}
@ -268,26 +275,40 @@
{% endif %}
<span class="entity-list__entity-info ">
{% if music.genre %}{% trans '流派' %}
{{ music.genre }} /
{% if music.artist %}{% trans '艺术家' %}
{% for artist in music.artist %}
<span>{{ artist }}</span>
{% if not forloop.last %} {% endif %}
{% endfor %}
{% endif %}
{% if music.genre %}/ {% trans '流派' %}
{{ music.genre }}
{% endif %}
{% if music.release_date %} {% trans '发行日期' %}
{% if music.release_date %}/ {% trans '发行日期' %}
{{ music.release_date }}
{% endif %}
</span>
<span class="entity-list__entity-info entity-list__entity-info--full-length">
{% if music.artist %}{% trans '艺术家' %}
{% for artist in music.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ artist }}</span>
{% if forloop.counter <= 5 %} {% if not forloop.counter == 5 and not forloop.last %} / {% endif %} {% endif %}
{% endfor %}
{% endif %}
</span>
</span>
{% if music.brief %}
<p class="entity-list__entity-brief">
{{ music.brief | truncate:170 }}
{{ music.brief }}
</p>
{% elif music.category_name|lower == 'album' %}
<p class="entity-list__entity-brief">
{% trans '曲目:' %}{{ music.track_list }}
</p>
{% else %}
<!-- song -->
<p class="entity-list__entity-brief">
{% trans '所属专辑:' %}{{ music.album }}
</p>
{% endif %}
<div class="tag-collection">
{% for tag_dict in music.tag_list %}
{% for k, v in tag_dict.items %}

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

@ -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,12 +8,11 @@ from django.utils.translation import gettext_lazy as _
from django.core.paginator import Paginator
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q, Count
from django.http import HttpResponseBadRequest
from books.models import Book
from movies.models import Movie
from music.models import Album, Song
from music.models import Album, Song, AlbumMark, SongMark
from users.models import Report, User
from mastodon.decorators import mastodon_request_included
from common.models import MarkStatusEnum
@ -28,6 +27,8 @@ BOOKS_PER_SET = 5
# how many movies have in each set at the home page
MOVIES_PER_SET = 5
MUSIC_PER_SET = 5
# how many items are showed in one search result page
ITEMS_PER_PAGE = 20
@ -43,6 +44,8 @@ logger = logging.getLogger(__name__)
def home(request):
if request.method == 'GET':
# really shitty code here
unread_announcements = Announcement.objects.filter(
pk__gt=request.user.read_announcement_index).order_by('-pk')
try:
@ -77,6 +80,28 @@ def home(request):
status=MarkStatusEnum.COLLECT).order_by("-edited_time")
collect_movies_more = True if collect_movie_marks.count() > MOVIES_PER_SET else False
do_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \
+ list(request.user.user_albummarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET])
do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False
do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
wish_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \
+ list(request.user.user_albummarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET])
wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False
wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
collect_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \
+ list(request.user.user_albummarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET])
collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False
collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET]
for mark in do_music_marks + wish_music_marks + collect_music_marks:
# for template convenience
if mark.__class__ == AlbumMark:
mark.type = "album"
else:
mark.type = "song"
reports = Report.objects.order_by('-submitted_time').filter(is_read=False)
# reports = Report.objects.latest('submitted_time').filter(is_read=False)
@ -96,6 +121,12 @@ def home(request):
'do_movies_more': do_movies_more,
'wish_movies_more': wish_movies_more,
'collect_movies_more': collect_movies_more,
'do_music_marks': do_music_marks,
'wish_music_marks': wish_music_marks,
'collect_music_marks': collect_music_marks,
'do_music_more': do_music_more,
'wish_music_more': wish_music_more,
'collect_music_more': collect_music_more,
'reports': reports,
'unread_announcements': unread_announcements,
}
@ -125,20 +156,25 @@ def search(request):
# category, book/movie/music etc
category = request.GET.get("c", default='').strip().lower()
# keywords, seperated by blank space
keywords = request.GET.get("q", default='').strip().split()
# tag, when tag is provided there should be no keywords , for now
tag = request.GET.get("tag", default='')
def book_param_handler():
q = Q()
query_args = []
# white space string, empty query
if not (keywords or tag):
return []
def book_param_handler(**kwargs):
# keywords
keywords = request.GET.get("q", default='').strip()
keywords = kwargs.get('keywords')
# tag
tag = request.GET.get("tag", default='')
tag = kwargs.get('tag')
if not (keywords or tag):
return []
query_args = []
q = Q()
for keyword in [keywords]:
for keyword in keywords:
q = q | Q(title__icontains=keyword)
q = q | Q(subtitle__icontains=keyword)
q = q | Q(orig_title__icontains=keyword)
@ -171,19 +207,16 @@ def search(request):
ordered_queryset = list(queryset)
return ordered_queryset
def movie_param_handler():
q = Q()
query_args = []
def movie_param_handler(**kwargs):
# keywords
keywords = request.GET.get("q", default='').strip()
keywords = kwargs.get('keywords')
# tag
tag = request.GET.get("tag", default='')
tag = kwargs.get('tag')
if not (keywords or tag):
return []
query_args = []
q = Q()
for keyword in [keywords]:
for keyword in keywords:
q = q | Q(title__icontains=keyword)
q = q | Q(other_title__icontains=keyword)
q = q | Q(orig_title__icontains=keyword)
@ -215,33 +248,51 @@ def search(request):
ordered_queryset = list(queryset)
return ordered_queryset
def music_param_handler():
q = Q()
query_args = []
def music_param_handler(**kwargs):
# keywords
keywords = request.GET.get("q", default='').strip()
keywords = kwargs.get('keywords')
# tag
tag = request.GET.get("tag", default='')
tag = kwargs.get('tag')
if not (keywords or tag):
return []
query_args = []
q = Q()
# search albums
for keyword in [keywords]:
for keyword in keywords:
q = q | Q(title__icontains=keyword)
q = q | Q(artist__icontains=keyword)
if tag:
q = q & Q(album_tags__content__iexact=tag)
query_args.append(q)
queryset = Album.objects.filter(*query_args).distinct()
album_queryset = Album.objects.filter(*query_args).distinct()
# extra query args for songs
q = Q()
for keyword in keywords:
q = q | Q(album__title__icontains=keyword)
q = q | Q(title__icontains=keyword)
q = q | Q(artist__icontains=keyword)
if tag:
q = q & Q(song_tags__content__iexact=tag)
query_args.clear()
query_args.append(q)
song_queryset = Song.objects.filter(*query_args).distinct()
queryset = list(album_queryset) + list(song_queryset)
def calculate_similarity(music):
if keywords:
# search by name
similarity, n = 0, 0
artist_dump = ' '.join(music.artist)
for keyword in keywords:
similarity += SequenceMatcher(None, keyword, music.title).quick_ratio()
if music.__class__ == Album:
similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
+ 1/2 * SequenceMatcher(None, keyword, artist_dump).quick_ratio()
elif music.__class__ == Song:
similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
+ 1/6 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() \
+ 1/6 * SequenceMatcher(None, keyword, music.album.title).quick_ratio()
n += 1
music.similarity = similarity / n
elif tag:
@ -256,10 +307,10 @@ def search(request):
ordered_queryset = list(queryset)
return ordered_queryset
def all_param_handler():
book_queryset = book_param_handler()
movie_queryset = movie_param_handler()
music_queryset = music_param_handler()
def all_param_handler(**kwargs):
book_queryset = book_param_handler(**kwargs)
movie_queryset = movie_param_handler(**kwargs)
music_queryset = music_param_handler(**kwargs)
ordered_queryset = sorted(
book_queryset + movie_queryset + music_queryset,
key=operator.attrgetter('similarity'),
@ -276,9 +327,15 @@ def search(request):
}
try:
queryset = param_handler[category]()
queryset = param_handler[category](
keywords=keywords,
tag=tag
)
except KeyError as e:
queryset = param_handler['all']()
queryset = param_handler['all'](
keywords=keywords,
tag=tag
)
paginator = Paginator(queryset, ITEMS_PER_PAGE)
page_number = request.GET.get('page', default=1)
items = paginator.get_page(page_number)
@ -333,17 +390,11 @@ def jump_or_scrape(request, url):
except ObjectDoesNotExist:
# scrape if not exists
try:
scraped_entity, raw_cover = scraper.scrape(url)
except:
scraper.scrape(url)
form = scraper.save(request_user=request.user)
except Exception as e:
logger.error(f"Scrape Failed URL: {url}")
logger.error("Expections during saving scraped data:", exc_info=e)
return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
scraped_cover = {
'cover': SimpleUploadedFile('temp.jpg', raw_cover)}
form = scraper.form_class(scraped_entity, scraped_cover)
if form.is_valid():
form.instance.last_editor = request.user
form.save()
return redirect(form.instance)
else:
msg = _("爬取数据失败😫")
logger.error(str(form.errors))
return render(request, 'common/error.html', {'msg': msg})
return redirect(form.instance)

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

@ -225,20 +225,17 @@
</div>
</div>
<div class="dividing-line"></div>
{% if movie.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
{% if movie.brief %}
<p class="entity-desc__content">{{ movie.brief | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
{% else %}
<div>{% trans '暂无简介' %}</div>
{% endif %}
</div>
{% endif %}
<div class="entity-marks">
@ -261,7 +258,7 @@
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% if others_mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
{% if others_mark.text %}
@ -290,7 +287,7 @@
<li class="entity-reviews__review">
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
@ -318,7 +315,7 @@
{% endif %}
{% endif %}
{% if mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
@ -366,7 +363,7 @@
<span class="review-panel__label">{% trans '我的评论' %}</span>
{% if review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">

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

View file

@ -19,6 +19,7 @@ class SongForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
duration = DurationField(required=False)
class Meta:
model = Song
@ -100,6 +101,7 @@ class AlbumForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
other_info = JSONField(required=False, label=_("其他信息"))
duration = DurationField(required=False)
class Meta:
model = Album

View file

@ -51,7 +51,7 @@ class Album(Entity):
default='', max_length=100)
company = postgres.ArrayField(
models.CharField(blank=True,
default='', max_length=100),
default='', max_length=500),
null=True,
blank=True,
default=list,
@ -96,7 +96,7 @@ class Song(Entity):
genre = models.CharField(_("流派"), blank=True, default='', max_length=100)
album = models.ForeignKey(
Album, models.CASCADE, "album_songs", null=True, blank=True, verbose_name=_("所属专辑"))
Album, models.SET_NULL, "album_songs", null=True, blank=True, verbose_name=_("所属专辑"))
def __str__(self):
return self.title

View file

@ -150,19 +150,16 @@
</div>
</div>
<div class="dividing-line"></div>
{% if album.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
{% if album.brief %}
<p class="entity-desc__content">{{ album.brief | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
{% else %}
<div>{% trans '暂无简介' %}</div>
{% endif %}
</div>
{% endif %}
{% if album.track_list %}
<div class="entity-desc" id="description">
@ -178,11 +175,32 @@
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '关联单曲' %}</h5>
<!-- TODO: Limit the maximum -->
{% for song in album.album_songs.all %}
<div>
<a href="{% url 'music:retrieve_song' song.id %}">{{ song }}</a>
<div class="track-carousel">
<div class="track-carousel__content">
{% for song in album.album_songs.all %}
<div class="track-carousel__track">
<a href="{% url 'music:retrieve_song' song.id %}">
<img src="{{ song.cover.url }}" alt="{{ song }}" class="track-carousel__track-image">
<span class="track-carousel__track-title">
{{ song }}
</span>
</a>
</div>
{% endfor %}
</div>
<!-- <button class="track-carousel__button track-carousel__button--prev">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M15.61 7.41L14.2 6l-6 6 6 6 1.41-1.41L11.03 12l4.58-4.59z" />
</svg>
</button>
<button class="track-carousel__button track-carousel__button--next">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M10.02 6L8.61 7.41 13.19 12l-4.58 4.59L10.02 18l6-6-6-6z" />
</svg>
</button> -->
</div>
{% endfor %}
</div>
{% endif %}
@ -204,7 +222,7 @@
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% if others_mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
{% if others_mark.text %}
@ -229,7 +247,7 @@
<li class="entity-reviews__review">
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'music:retrieve_album_review' others_review.id %}">{{ others_review.title }}</a></span>
@ -257,7 +275,7 @@
{% endif %}
{% endif %}
{% if mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
@ -285,9 +303,9 @@
<div class="action-panel" id="addMarkPanel">
<div class="action-panel__label">{% trans '标记这部作品' %}</div>
<div class="action-panel__button-group">
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '过' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '过' %}</button>
</div>
</div>
{% endif %}
@ -300,7 +318,7 @@
<span class="review-panel__label">{% trans '我的评论' %}</span>
{% if review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">

View file

@ -48,8 +48,7 @@
{% endif %}
{% if mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>

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

@ -44,8 +44,7 @@
class="entity-reviews__owner-link">{{ review.owner.username }}</a>
{% if review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>

View file

@ -31,7 +31,10 @@
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}">
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
value="{{ form.instance.release_date | date:"Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}

View file

@ -36,7 +36,8 @@
{% for field in form %}
{% if field.name == 'release_date' %}
{{ field.label_tag }}
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}">
<input type="date" name="{{ field.name }}" id="{{ field.id_for_label }}"
value="{{ form.instance.release_date | date:"Y-m-d" }}">
{% else %}
{% if field.name != 'id' %}
{{ field.label_tag }}

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

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

@ -139,20 +139,18 @@
</div>
</div>
<div class="dividing-line"></div>
{% if song.brief %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
{% if song.brief %}
<p class="entity-desc__content">{{ song.brief | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
{% else %}
<div>{% trans '暂无简介' %}</div>
{% endif %}
</div>
{% endif %}
<div class="entity-marks">
@ -171,7 +169,7 @@
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
{% endif %}
{% if others_mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
{% if others_mark.text %}
@ -196,7 +194,7 @@
<li class="entity-reviews__review">
<a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
{% if others_review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
<span class="entity-reviews__review-title"> <a href="{% url 'music:retrieve_song_review' others_review.id %}">{{ others_review.title }}</a></span>
@ -224,7 +222,7 @@
{% endif %}
{% endif %}
{% if mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="mark-panel__actions">
<a href="" class="edit">{% trans '修改' %}</a>
@ -252,9 +250,9 @@
<div class="action-panel" id="addMarkPanel">
<div class="action-panel__label">{% trans '标记这部作品' %}</div>
<div class="action-panel__button-group">
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '过' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在' %}</button>
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '过' %}</button>
</div>
</div>
{% endif %}
@ -267,7 +265,7 @@
<span class="review-panel__label">{% trans '我的评论' %}</span>
{% if review.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %}
<span class="review-panel__actions">

View file

@ -49,8 +49,7 @@
{% endif %}
{% if mark.is_private %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
viewBox="0 0 20 20">
<path
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
</svg></span>

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

@ -7,7 +7,6 @@ from common.utils import PageLinksGenerator
from mastodon.utils import rating_to_emoji
from mastodon.api import check_visibility, post_toot, TootVisibilityEnum
from mastodon import mastodon_request_included
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.paginator import Paginator
from django.utils import timezone
from django.db.models import Count
@ -158,13 +157,13 @@ def retrieve_song(request, id):
seconds = x % 60
x //= 60
if x == 0:
return f"{seconds}"
return f"{seconds}"
minutes = x % 60
x //= 60
if x == 0:
return f"{minutes}:{seconds}"
return f"{minutes}{seconds}"
hours = x % 24
return f"{hours}:{minutes}:{seconds}"
return f"{hours}{minutes}{seconds}"
song.get_duration_display = ms_to_readable(song.duration)
@ -730,13 +729,13 @@ def retrieve_album(request, id):
seconds = x % 60
x //= 60
if x == 0:
return f"{seconds}"
return f"{seconds}"
minutes = x % 60
x //= 60
if x == 0:
return f"{minutes}:{seconds}"
return f"{minutes}{seconds}"
hours = x % 24
return f"{hours}:{minutes}:{seconds}"
return f"{hours}{minutes}{seconds}"
album.get_duration_display = ms_to_readable(album.duration)

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
@ -380,7 +382,7 @@ def book_list(request, id, status):
mark.book.tag_list = mark.book.get_tags_manager().values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书"))
list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的书"))
return render(
request,
'users/book_list.html',
@ -449,7 +451,7 @@ def movie_list(request, id, status):
mark.movie.tag_list = mark.movie.get_tags_manager().values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集"))
list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的电影和剧集"))
return render(
request,
'users/movie_list.html',
@ -463,6 +465,86 @@ def movie_list(request, id, status):
return HttpResponseBadRequest()
@mastodon_request_included
@login_required
def music_list(request, id, status):
if request.method == 'GET':
if not status.upper() in MarkStatusEnum.names:
return HttpResponseBadRequest()
if isinstance(id, str):
try:
username = id.split('@')[0]
site = id.split('@')[1]
except IndexError as e:
return HttpResponseBadRequest("Invalid user id")
query_kwargs = {'username': username, 'mastodon_site': site}
elif isinstance(id, int):
query_kwargs = {'pk': id}
try:
user = User.objects.get(**query_kwargs)
except ObjectDoesNotExist:
msg = _("😖哎呀这位老师还没有注册书影音呢快去长毛象喊TA来吧")
sec_msg = _("目前只开放本站用户注册")
return render(
request,
'common/error.html',
{
'msg': msg,
'secondary_msg': sec_msg,
}
)
if not user == request.user:
# mastodon request
relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
if relation['blocked_by']:
msg = _("你没有访问TA主页的权限😥")
return render(
request,
'common/error.html',
{
'msg': msg,
}
)
queryset = list(AlbumMark.get_available_by_user(user, relation['following']).filter(
status=MarkStatusEnum[status.upper()])) \
+ list(SongMark.get_available_by_user(user, relation['following']).filter(
status=MarkStatusEnum[status.upper()]))
user.target_site_id = get_cross_site_id(
user, request.user.mastodon_site, request.session['oauth_token'])
else:
queryset = list(AlbumMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) \
+ list(SongMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]))
queryset = sorted(queryset, key=lambda e: e.edited_time, reverse=True)
paginator = Paginator(queryset, ITEMS_PER_PAGE)
page_number = request.GET.get('page', default=1)
marks = paginator.get_page(page_number)
for mark in marks:
if mark.__class__ == AlbumMark:
mark.music = mark.album
mark.music.tag_list = mark.album.get_tags_manager().values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
elif mark.__class__ == SongMark:
mark.music = mark.song
mark.music.tag_list = mark.song.get_tags_manager().values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的音乐"))
return render(
request,
'users/music_list.html',
{
'marks': marks,
'user': user,
'list_title' : list_title,
}
)
else:
return HttpResponseBadRequest()
@login_required
def report(request):
if request.method == 'GET':