
* fix scraping failure with wepb image (merge upstream/fix-webp-scrape) * add filetype to requirements * add proxycrawl.com as fallback for douban scraper * load 3p js/css from cdn * add fix-cover task * fix book/album cover tasks * scrapestack * bandcamp scrape and preview ; manage.py scrape <url> ; make ^C work when DEBUG * use scrapestack when fix cover * add user agent to improve compatibility * search BandCamp for music albums * add missing MovieGenre * fix search 500 when song has no parent album * adjust timeout * individual scrapers * fix tmdb parser * export marks via rq; pref to send public toot; move import to data page * fix spotify import * fix edge cases * export: fix dupe tags * use rq to manage doufen import * add django command to manage rq jobs * fix export edge case * tune rq admin * fix detail page 502 step 1: async pull mastodon follow/block/mute list * fix detail page 502 step 2: calculate relationship by local cached data * manual sync mastodon follow info * domain_blocks parsing fix * marks by who i follows * adjust label * use username in urls * add page to list a user\'s review * review widget on user home page * fix preview 500 * fix typo * minor fix * fix google books parsing * allow mark/review visible to oneself * fix auto sync masto for new user * fix search 500 * add command to restart a sync task * reset visibility * delete user data * fix tag search result pagination * not upgrade to django 4 yet * basic doc * wip: collection * wip * wip * collection use htmx * show in-collection section for entities * fix typo * add su for easier debug * fix some 500s * fix login using alternative domain * hide data from disabled user * add item to list from detail page * my tags * collection: inline comment edit * show number of ratings * fix collection delete * more detail in collection view * use item template in search result * fix 500 * write index to meilisearch * fix search * reindex in batch * fix 500 * show search result from meilisearch * more search commands * index less fields * index new items only * search highlights * fix 500 * auto set search category * classic search if no meili server * fix index stats error * support typesense backend * workaround typesense bug * make external search async * fix 500, typo * fix cover scripts * fix minor issue in douban parser * supports m.douban.com and customized bandcamp domain * move account * reword with gender-friendly and instance-neutral language * Friendica does not have vapid_key in api response * enable anonymous search * tweak book result template * API v0 API v0 * fix meilisearch reindex * fix search by url error * login via twitter.com * login via pixelfed * minor fix * no refresh on inactive users * support refresh access token * get rid of /users/number-id/ * refresh twitter handler automatically * paste image when review * support PixelFed (very long token) * fix django-markdownx version * ignore single quote for meilisearch for now * update logo * show book review/mark from same isbn * show movie review/mark from same imdb * fix login with older mastodon servers * import Goodreads book list and profile * add timestamp to Goodreads import * support new google books api * import goodreads list * minor goodreads fix * click corner action icon to add to wishlist * clean up duplicated code * fix anonymous search * fix 500 * minor fix search 500 * show rating only if votes > 5 * Entity.refresh_rating() * preference to append text when sharing; clean up duplicated code * fix missing data for user tagged view * fix page link for tag view * fix 500 when language field longer than 10 * fix 500 when sharing mark for song * fix error when reimport goodread profile * fix minor typo * fix a rare 500 * error log dump less * fix tags in marks export * fix missing param in pagination * import douban review * clarify text * fix missing sheet in review import * review: show in progress * scrape douban: ignore unknown genre * minor fix * improve review import by guess entity urls * clear guide text for review import * improve review import form text * workaround some 500 * fix mark import error * fix img in review import * load external results earlier * ignore search server errors * simplify user register flow to avoid inconsistent state * Add a learn more link on login page * Update login.html * show mark created timestamp as mark time * no 500 for api error * redirect for expired tokens * ensure preference object created. * mark collections * tag list * fix tag display * fix sorting etc * fix 500 * fix potential export 500; save shared links * fix share to twittwe * fix review url * fix 500 * fix 500 * add timeline, etc * missing status change in timeline * missing id in timeline * timeline view by default * workaround bug in markdownx... * fix typo * option to create new collection when add from detail page * add missing announcement and tags in timeline home * add missing announcement * add missing announcement * opensearch * show fediverse shared link * public review no longer requires login * fix markdownx bug * fix 500 * use cloudflare cdn * validate jquery load and domain input * fix 500 * tips for goodreads import * collaborative collection * show timeline and profile link on nav bar * minor tweak * share collection * fix Goodreads search * show wish mark in timeline * resync failed urls with local proxy * resync failed urls with local proxy: check proxy first * scraper minor fix * resync failed urls * fix fields limit * fix douban parsing error * resync * scraper minor fix * scraper minor fix * scraper minor fix * local proxy * local proxy * sync default config from neodb * configurable site name * fix 500 * fix 500 for anonymous user * add sentry * add git version in log * add git version in log * no longer rely on cdnjs.cloudflare.com * move jq/cash to _common_libs template partial * fix rare js error * fix 500 * avoid double submission error * import tag in lower case * catch some js network errors * catch some js network errors * support more goodread urls * fix unaired tv in tmdb * support more google book urls * fix related series * more goodreads urls * robust googlebooks search * robust search * Update settings.py * Update scraper.py * Update requirements.txt * make nicedb work * doc update * simplify permission check * update doc * update doc for bug report link * skip spotify tracks * fix 500 * improve search api * blind fix import compatibility * show years for movie in timeline * show years for movie in timeline; thinner font * export reviews * revert user home to use jquery https://github.com/fabiospampinato/cash/issues/246 * IGDB * use IGDB for Steam * use TMDB for IMDb * steam: igdb then fallback to steam * keep change history * keep change history: add django settings * Steam: keep localized title/brief while merging IGDB * basic Docker support * rescrape * Create codeql-analysis.yml * Create SECURITY.md * Create pysa.yml Co-authored-by: doubaniux <goodsir@vivaldi.net> Co-authored-by: Your Name <you@example.com> Co-authored-by: Their Name <they@example.com> Co-authored-by: Mt. Front <mfcndw@gmail.com>
1152 lines
39 KiB
Python
1152 lines
39 KiB
Python
from .forms import *
|
|
from .models import *
|
|
from common.models import SourceSiteEnum
|
|
from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
|
|
from common.utils import PageLinksGenerator
|
|
from mastodon.models import MastodonApplication
|
|
from mastodon.api import share_mark, share_review
|
|
from mastodon import mastodon_request_included
|
|
from django.core.paginator import Paginator
|
|
from django.utils import timezone
|
|
from django.db.models import Count
|
|
from django.db import IntegrityError, transaction
|
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
|
from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.contrib.auth.decorators import login_required, permission_required
|
|
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
|
import logging
|
|
from django.shortcuts import render
|
|
from collection.models import CollectionItem
|
|
from common.scraper import get_scraper_by_url, get_normalized_url
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
mastodon_logger = logging.getLogger("django.mastodon")
|
|
|
|
|
|
# how many marks showed on the detail page
|
|
MARK_NUMBER = 5
|
|
# how many marks at the mark page
|
|
MARK_PER_PAGE = 20
|
|
# how many reviews showed on the detail page
|
|
REVIEW_NUMBER = 5
|
|
# how many reviews at the mark page
|
|
REVIEW_PER_PAGE = 20
|
|
# max tags on detail page
|
|
TAG_NUMBER = 10
|
|
|
|
|
|
# public data
|
|
###########################
|
|
@login_required
|
|
def create_song(request):
|
|
if request.method == 'GET':
|
|
form = SongForm()
|
|
return render(
|
|
request,
|
|
'music/create_update_song.html',
|
|
{
|
|
'form': form,
|
|
'title': _('添加音乐'),
|
|
'submit_url': reverse("music:create_song"),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
if request.user.is_authenticated:
|
|
# only local user can alter public data
|
|
form = SongForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
form.instance.last_editor = request.user
|
|
try:
|
|
with transaction.atomic():
|
|
form.save()
|
|
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
|
|
real_url = form.instance.get_absolute_url()
|
|
form.instance.source_url = real_url
|
|
form.instance.save()
|
|
except IntegrityError as e:
|
|
logger.error(e.__str__())
|
|
return HttpResponseServerError("integrity error")
|
|
return redirect(reverse("music:retrieve_song", args=[form.instance.id]))
|
|
else:
|
|
return render(
|
|
request,
|
|
'music/create_update_song.html',
|
|
{
|
|
'form': form,
|
|
'title': _('添加音乐'),
|
|
'submit_url': reverse("music:create_song"),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
else:
|
|
return redirect(reverse("users:login"))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def update_song(request, id):
|
|
if request.method == 'GET':
|
|
song = get_object_or_404(Song, pk=id)
|
|
form = SongForm(instance=song)
|
|
page_title = _('修改音乐')
|
|
return render(
|
|
request,
|
|
'music/create_update_song.html',
|
|
{
|
|
'form': form,
|
|
'is_update': True,
|
|
'title': page_title,
|
|
'submit_url': reverse("music:update_song", args=[song.id]),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
song = get_object_or_404(Song, pk=id)
|
|
form = SongForm(request.POST, request.FILES, instance=song)
|
|
page_title = _('修改音乐')
|
|
if form.is_valid():
|
|
form.instance.last_editor = request.user
|
|
form.instance.edited_time = timezone.now()
|
|
try:
|
|
with transaction.atomic():
|
|
form.save()
|
|
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
|
|
real_url = form.instance.get_absolute_url()
|
|
form.instance.source_url = real_url
|
|
form.instance.save()
|
|
except IntegrityError as e:
|
|
logger.error(e.__str__())
|
|
return HttpResponseServerError("integrity error")
|
|
else:
|
|
return render(
|
|
request,
|
|
'music/create_update_song.html',
|
|
{
|
|
'form': form,
|
|
'is_update': True,
|
|
'title': page_title,
|
|
'submit_url': reverse("music:update_song", args=[song.id]),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
return redirect(reverse("music:retrieve_song", args=[form.instance.id]))
|
|
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
# @login_required
|
|
def retrieve_song(request, id):
|
|
if request.method == 'GET':
|
|
song = get_object_or_404(Song, pk=id)
|
|
mark = None
|
|
mark_tags = None
|
|
review = None
|
|
|
|
def ms_to_readable(ms):
|
|
if not ms:
|
|
return
|
|
x = ms // 1000
|
|
seconds = x % 60
|
|
x //= 60
|
|
if x == 0:
|
|
return f"{seconds}秒"
|
|
minutes = x % 60
|
|
x //= 60
|
|
if x == 0:
|
|
return f"{minutes}分{seconds}秒"
|
|
hours = x % 24
|
|
return f"{hours}时{minutes}分{seconds}秒"
|
|
|
|
song.get_duration_display = ms_to_readable(song.duration)
|
|
|
|
|
|
# retrieve tags
|
|
song_tag_list = song.song_tags.values('content').annotate(
|
|
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER]
|
|
|
|
# retrieve user mark and initialize mark form
|
|
try:
|
|
if request.user.is_authenticated:
|
|
mark = SongMark.objects.get(owner=request.user, song=song)
|
|
except ObjectDoesNotExist:
|
|
mark = None
|
|
if mark:
|
|
mark_tags = mark.songmark_tags.all()
|
|
mark.get_status_display = MusicMarkStatusTranslator(mark.status)
|
|
mark_form = SongMarkForm(instance=mark, initial={
|
|
'tags': mark_tags
|
|
})
|
|
else:
|
|
mark_form = SongMarkForm(initial={
|
|
'song': song,
|
|
'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
|
|
'tags': mark_tags
|
|
})
|
|
|
|
# retrieve user review
|
|
try:
|
|
if request.user.is_authenticated:
|
|
review = SongReview.objects.get(
|
|
owner=request.user, song=song)
|
|
except ObjectDoesNotExist:
|
|
review = None
|
|
|
|
# retrieve other related reviews and marks
|
|
if request.user.is_anonymous:
|
|
# hide all marks and reviews for anonymous user
|
|
mark_list = None
|
|
review_list = None
|
|
mark_list_more = None
|
|
review_list_more = None
|
|
else:
|
|
mark_list = SongMark.get_available(song, request.user)
|
|
review_list = SongReview.get_available(song, request.user)
|
|
mark_list_more = True if len(mark_list) > MARK_NUMBER else False
|
|
mark_list = mark_list[:MARK_NUMBER]
|
|
for m in mark_list:
|
|
m.get_status_display = MusicMarkStatusTranslator(m.status)
|
|
review_list_more = True if len(
|
|
review_list) > REVIEW_NUMBER else False
|
|
review_list = review_list[:REVIEW_NUMBER]
|
|
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(song=song)))
|
|
|
|
# def strip_html_tags(text):
|
|
# import re
|
|
# regex = re.compile('<.*?>')
|
|
# return re.sub(regex, '', text)
|
|
|
|
# for r in review_list:
|
|
# r.content = strip_html_tags(r.content)
|
|
|
|
return render(
|
|
request,
|
|
'music/song_detail.html',
|
|
{
|
|
'song': song,
|
|
'mark': mark,
|
|
'review': review,
|
|
'status_enum': MarkStatusEnum,
|
|
'mark_form': mark_form,
|
|
'mark_list': mark_list,
|
|
'mark_list_more': mark_list_more,
|
|
'review_list': review_list,
|
|
'review_list_more': review_list_more,
|
|
'song_tag_list': song_tag_list,
|
|
'mark_tags': mark_tags,
|
|
'collection_list': collection_list,
|
|
}
|
|
)
|
|
else:
|
|
logger.warning('non-GET method at /song/<id>')
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@permission_required("music.delete_song")
|
|
@login_required
|
|
def delete_song(request, id):
|
|
if request.method == 'GET':
|
|
song = get_object_or_404(Song, pk=id)
|
|
return render(
|
|
request,
|
|
'music/delete_song.html',
|
|
{
|
|
'song': song,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
if request.user.is_staff:
|
|
# only staff has right to delete
|
|
song = get_object_or_404(Song, pk=id)
|
|
song.delete()
|
|
return redirect(reverse("common:home"))
|
|
else:
|
|
raise PermissionDenied()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
# user owned entites
|
|
###########################
|
|
@mastodon_request_included
|
|
@login_required
|
|
def create_update_song_mark(request):
|
|
# check list:
|
|
# clean rating if is wish
|
|
# transaction on updating song rating
|
|
# owner check(guarantee)
|
|
if request.method == 'POST':
|
|
pk = request.POST.get('id')
|
|
old_rating = None
|
|
old_tags = None
|
|
if not pk:
|
|
song_id = request.POST.get('song')
|
|
mark = SongMark.objects.filter(song_id=song_id, owner=request.user).first()
|
|
if mark:
|
|
pk = mark.id
|
|
if pk:
|
|
mark = get_object_or_404(SongMark, pk=pk)
|
|
if request.user != mark.owner:
|
|
return HttpResponseBadRequest()
|
|
old_rating = mark.rating
|
|
old_tags = mark.songmark_tags.all()
|
|
if mark.status != request.POST.get('status'):
|
|
mark.created_time = timezone.now()
|
|
# update
|
|
form = SongMarkForm(request.POST, instance=mark)
|
|
else:
|
|
# create
|
|
form = SongMarkForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
if form.instance.status == MarkStatusEnum.WISH.value or form.instance.rating == 0:
|
|
form.instance.rating = None
|
|
form.cleaned_data['rating'] = None
|
|
form.instance.owner = request.user
|
|
form.instance.edited_time = timezone.now()
|
|
song = form.instance.song
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
# update song rating
|
|
song.update_rating(old_rating, form.instance.rating)
|
|
form.save()
|
|
# update tags
|
|
if old_tags:
|
|
for tag in old_tags:
|
|
tag.delete()
|
|
if form.cleaned_data['tags']:
|
|
for tag in form.cleaned_data['tags']:
|
|
SongTag.objects.create(
|
|
content=tag,
|
|
song=song,
|
|
mark=form.instance
|
|
)
|
|
except IntegrityError as e:
|
|
logger.error(e.__str__())
|
|
return HttpResponseServerError("integrity error")
|
|
|
|
if form.cleaned_data['share_to_mastodon']:
|
|
if not share_mark(form.instance):
|
|
return go_relogin(request)
|
|
else:
|
|
return HttpResponseBadRequest(f"invalid form data {form.errors}")
|
|
|
|
return redirect(reverse("music:retrieve_song", args=[form.instance.song.id]))
|
|
else:
|
|
return HttpResponseBadRequest("invalid method")
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def wish_song(request, id):
|
|
if request.method == 'POST':
|
|
song = get_object_or_404(Song, pk=id)
|
|
params = {
|
|
'owner': request.user,
|
|
'status': MarkStatusEnum.WISH,
|
|
'visibility': 0,
|
|
'song': song,
|
|
}
|
|
try:
|
|
SongMark.objects.create(**params)
|
|
except Exception:
|
|
pass
|
|
return HttpResponse("✔️")
|
|
else:
|
|
return HttpResponseBadRequest("invalid method")
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def retrieve_song_mark_list(request, song_id, following_only=False):
|
|
if request.method == 'GET':
|
|
song = get_object_or_404(Song, pk=song_id)
|
|
queryset = SongMark.get_available(song, request.user, following_only=following_only)
|
|
paginator = Paginator(queryset, MARK_PER_PAGE)
|
|
page_number = request.GET.get('page', default=1)
|
|
marks = paginator.get_page(page_number)
|
|
marks.pagination = PageLinksGenerator(
|
|
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
|
for m in marks:
|
|
m.get_status_display = MusicMarkStatusTranslator(m.status)
|
|
return render(
|
|
request,
|
|
'music/song_mark_list.html',
|
|
{
|
|
'marks': marks,
|
|
'song': song,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def delete_song_mark(request, id):
|
|
if request.method == 'POST':
|
|
mark = get_object_or_404(SongMark, pk=id)
|
|
if request.user != mark.owner:
|
|
return HttpResponseBadRequest()
|
|
song_id = mark.song.id
|
|
try:
|
|
with transaction.atomic():
|
|
# update song rating
|
|
mark.song.update_rating(mark.rating, None)
|
|
mark.delete()
|
|
except IntegrityError as e:
|
|
return HttpResponseServerError()
|
|
return redirect(reverse("music:retrieve_song", args=[song_id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def create_song_review(request, song_id):
|
|
if request.method == 'GET':
|
|
form = SongReviewForm(initial={'song': song_id})
|
|
song = get_object_or_404(Song, pk=song_id)
|
|
return render(
|
|
request,
|
|
'music/create_update_song_review.html',
|
|
{
|
|
'form': form,
|
|
'title': _("添加评论"),
|
|
'song': song,
|
|
'submit_url': reverse("music:create_song_review", args=[song_id]),
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
form = SongReviewForm(request.POST)
|
|
if form.is_valid():
|
|
form.instance.owner = request.user
|
|
form.save()
|
|
if form.cleaned_data['share_to_mastodon']:
|
|
if not share_review(form.instance):
|
|
return go_relogin(request)
|
|
return redirect(reverse("music:retrieve_song_review", args=[form.instance.id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def update_song_review(request, id):
|
|
if request.method == 'GET':
|
|
review = get_object_or_404(SongReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
form = SongReviewForm(instance=review)
|
|
song = review.song
|
|
return render(
|
|
request,
|
|
'music/create_update_song_review.html',
|
|
{
|
|
'form': form,
|
|
'title': _("编辑评论"),
|
|
'song': song,
|
|
'submit_url': reverse("music:update_song_review", args=[review.id]),
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
review = get_object_or_404(SongReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
form = SongReviewForm(request.POST, instance=review)
|
|
if form.is_valid():
|
|
form.instance.edited_time = timezone.now()
|
|
form.save()
|
|
if form.cleaned_data['share_to_mastodon']:
|
|
if not share_review(form.instance):
|
|
return go_relogin(request)
|
|
return redirect(reverse("music:retrieve_song_review", args=[form.instance.id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def delete_song_review(request, id):
|
|
if request.method == 'GET':
|
|
review = get_object_or_404(SongReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
review_form = SongReviewForm(instance=review)
|
|
return render(
|
|
request,
|
|
'music/delete_song_review.html',
|
|
{
|
|
'form': review_form,
|
|
'review': review,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
review = get_object_or_404(SongReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
song_id = review.song.id
|
|
review.delete()
|
|
return redirect(reverse("music:retrieve_song", args=[song_id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
def retrieve_song_review(request, id):
|
|
if request.method == 'GET':
|
|
review = get_object_or_404(SongReview, pk=id)
|
|
if not review.is_visible_to(request.user):
|
|
msg = _("你没有访问这个页面的权限😥")
|
|
return render(
|
|
request,
|
|
'common/error.html',
|
|
{
|
|
'msg': msg,
|
|
}
|
|
)
|
|
review_form = SongReviewForm(instance=review)
|
|
song = review.song
|
|
try:
|
|
mark = SongMark.objects.get(owner=review.owner, song=song)
|
|
mark.get_status_display = MusicMarkStatusTranslator(mark.status)
|
|
except ObjectDoesNotExist:
|
|
mark = None
|
|
return render(
|
|
request,
|
|
'music/song_review_detail.html',
|
|
{
|
|
'form': review_form,
|
|
'review': review,
|
|
'song': song,
|
|
'mark': mark,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def retrieve_song_review_list(request, song_id):
|
|
if request.method == 'GET':
|
|
song = get_object_or_404(Song, pk=song_id)
|
|
queryset = SongReview.get_available(song, request.user)
|
|
paginator = Paginator(queryset, REVIEW_PER_PAGE)
|
|
page_number = request.GET.get('page', default=1)
|
|
reviews = paginator.get_page(page_number)
|
|
reviews.pagination = PageLinksGenerator(
|
|
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
|
return render(
|
|
request,
|
|
'music/song_review_list.html',
|
|
{
|
|
'reviews': reviews,
|
|
'song': song,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def scrape_song(request):
|
|
if request.method == 'GET':
|
|
keywords = request.GET.get('q')
|
|
form = SongForm()
|
|
return render(
|
|
request,
|
|
'music/scrape_song.html',
|
|
{
|
|
'q': keywords,
|
|
'form': form,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def click_to_scrape_song(request):
|
|
if request.method == "POST":
|
|
url = request.POST.get("url")
|
|
if url:
|
|
return jump_or_scrape(request, url)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def create_album(request):
|
|
if request.method == 'GET':
|
|
form = AlbumForm()
|
|
return render(
|
|
request,
|
|
'music/create_update_album.html',
|
|
{
|
|
'form': form,
|
|
'title': _('添加音乐'),
|
|
'submit_url': reverse("music:create_album"),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
if request.user.is_authenticated:
|
|
# only local user can alter public data
|
|
form = AlbumForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
form.instance.last_editor = request.user
|
|
try:
|
|
with transaction.atomic():
|
|
form.save()
|
|
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
|
|
real_url = form.instance.get_absolute_url()
|
|
form.instance.source_url = real_url
|
|
form.instance.save()
|
|
except IntegrityError as e:
|
|
logger.error(e.__str__())
|
|
return HttpResponseServerError("integrity error")
|
|
return redirect(reverse("music:retrieve_album", args=[form.instance.id]))
|
|
else:
|
|
return render(
|
|
request,
|
|
'music/create_update_album.html',
|
|
{
|
|
'form': form,
|
|
'title': _('添加音乐'),
|
|
'submit_url': reverse("music:create_album"),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
else:
|
|
return redirect(reverse("users:login"))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def rescrape(request, id):
|
|
if request.method != 'POST':
|
|
return HttpResponseBadRequest()
|
|
item = get_object_or_404(Album, pk=id)
|
|
url = get_normalized_url(item.source_url)
|
|
scraper = get_scraper_by_url(url)
|
|
scraper.scrape(url)
|
|
form = scraper.save(request_user=request.user, instance=item)
|
|
return redirect(reverse("music:retrieve_album", args=[form.instance.id]))
|
|
|
|
|
|
@login_required
|
|
def update_album(request, id):
|
|
if request.method == 'GET':
|
|
album = get_object_or_404(Album, pk=id)
|
|
form = AlbumForm(instance=album)
|
|
page_title = _('修改音乐')
|
|
return render(
|
|
request,
|
|
'music/create_update_album.html',
|
|
{
|
|
'form': form,
|
|
'is_update': True,
|
|
'title': page_title,
|
|
'submit_url': reverse("music:update_album", args=[album.id]),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
album = get_object_or_404(Album, pk=id)
|
|
form = AlbumForm(request.POST, request.FILES, instance=album)
|
|
page_title = _('修改音乐')
|
|
if form.is_valid():
|
|
form.instance.last_editor = request.user
|
|
form.instance.edited_time = timezone.now()
|
|
try:
|
|
with transaction.atomic():
|
|
form.save()
|
|
if form.instance.source_site == SourceSiteEnum.IN_SITE.value:
|
|
real_url = form.instance.get_absolute_url()
|
|
form.instance.source_url = real_url
|
|
form.instance.save()
|
|
except IntegrityError as e:
|
|
logger.error(e.__str__())
|
|
return HttpResponseServerError("integrity error")
|
|
else:
|
|
return render(
|
|
request,
|
|
'music/create_update_album.html',
|
|
{
|
|
'form': form,
|
|
'is_update': True,
|
|
'title': page_title,
|
|
'submit_url': reverse("music:update_album", args=[album.id]),
|
|
# provided for frontend js
|
|
'this_site_enum_value': SourceSiteEnum.IN_SITE.value,
|
|
}
|
|
)
|
|
return redirect(reverse("music:retrieve_album", args=[form.instance.id]))
|
|
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
# @login_required
|
|
def retrieve_album(request, id):
|
|
if request.method == 'GET':
|
|
album = get_object_or_404(Album, pk=id)
|
|
mark = None
|
|
mark_tags = None
|
|
review = None
|
|
|
|
def ms_to_readable(ms):
|
|
if not ms:
|
|
return
|
|
x = ms // 1000
|
|
seconds = x % 60
|
|
x //= 60
|
|
if x == 0:
|
|
return f"{seconds}秒"
|
|
minutes = x % 60
|
|
x //= 60
|
|
if x == 0:
|
|
return f"{minutes}分{seconds}秒"
|
|
hours = x % 24
|
|
return f"{hours}时{minutes}分{seconds}秒"
|
|
|
|
album.get_duration_display = ms_to_readable(album.duration)
|
|
|
|
# retrieve tags
|
|
album_tag_list = album.album_tags.values('content').annotate(
|
|
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER]
|
|
|
|
# retrieve user mark and initialize mark form
|
|
try:
|
|
if request.user.is_authenticated:
|
|
mark = AlbumMark.objects.get(owner=request.user, album=album)
|
|
except ObjectDoesNotExist:
|
|
mark = None
|
|
if mark:
|
|
mark_tags = mark.albummark_tags.all()
|
|
mark.get_status_display = MusicMarkStatusTranslator(mark.status)
|
|
mark_form = AlbumMarkForm(instance=mark, initial={
|
|
'tags': mark_tags
|
|
})
|
|
else:
|
|
mark_form = AlbumMarkForm(initial={
|
|
'album': album,
|
|
'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
|
|
'tags': mark_tags
|
|
})
|
|
|
|
# retrieve user review
|
|
try:
|
|
if request.user.is_authenticated:
|
|
review = AlbumReview.objects.get(
|
|
owner=request.user, album=album)
|
|
except ObjectDoesNotExist:
|
|
review = None
|
|
|
|
# retrieve other related reviews and marks
|
|
if request.user.is_anonymous:
|
|
# hide all marks and reviews for anonymous user
|
|
mark_list = None
|
|
review_list = None
|
|
mark_list_more = None
|
|
review_list_more = None
|
|
else:
|
|
mark_list = AlbumMark.get_available(album, request.user)
|
|
review_list = AlbumReview.get_available(album, request.user)
|
|
mark_list_more = True if len(mark_list) > MARK_NUMBER else False
|
|
mark_list = mark_list[:MARK_NUMBER]
|
|
for m in mark_list:
|
|
m.get_status_display = MusicMarkStatusTranslator(m.status)
|
|
review_list_more = True if len(
|
|
review_list) > REVIEW_NUMBER else False
|
|
review_list = review_list[:REVIEW_NUMBER]
|
|
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(album=album)))
|
|
|
|
# def strip_html_tags(text):
|
|
# import re
|
|
# regex = re.compile('<.*?>')
|
|
# return re.sub(regex, '', text)
|
|
|
|
# for r in review_list:
|
|
# r.content = strip_html_tags(r.content)
|
|
|
|
return render(
|
|
request,
|
|
'music/album_detail.html',
|
|
{
|
|
'album': album,
|
|
'mark': mark,
|
|
'review': review,
|
|
'status_enum': MarkStatusEnum,
|
|
'mark_form': mark_form,
|
|
'mark_list': mark_list,
|
|
'mark_list_more': mark_list_more,
|
|
'review_list': review_list,
|
|
'review_list_more': review_list_more,
|
|
'album_tag_list': album_tag_list,
|
|
'mark_tags': mark_tags,
|
|
'collection_list': collection_list,
|
|
}
|
|
)
|
|
else:
|
|
logger.warning('non-GET method at /album/<id>')
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@permission_required("music.delete_album")
|
|
@login_required
|
|
def delete_album(request, id):
|
|
if request.method == 'GET':
|
|
album = get_object_or_404(Album, pk=id)
|
|
return render(
|
|
request,
|
|
'music/delete_album.html',
|
|
{
|
|
'album': album,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
if request.user.is_staff:
|
|
# only staff has right to delete
|
|
album = get_object_or_404(Album, pk=id)
|
|
album.delete()
|
|
return redirect(reverse("common:home"))
|
|
else:
|
|
raise PermissionDenied()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
# user owned entites
|
|
###########################
|
|
@mastodon_request_included
|
|
@login_required
|
|
def create_update_album_mark(request):
|
|
# check list:
|
|
# clean rating if is wish
|
|
# transaction on updating album rating
|
|
# owner check(guarantee)
|
|
if request.method == 'POST':
|
|
pk = request.POST.get('id')
|
|
old_rating = None
|
|
old_tags = None
|
|
if not pk:
|
|
album_id = request.POST.get('album')
|
|
mark = AlbumMark.objects.filter(album_id=album_id, owner=request.user).first()
|
|
if mark:
|
|
pk = mark.id
|
|
if pk:
|
|
mark = get_object_or_404(AlbumMark, pk=pk)
|
|
if request.user != mark.owner:
|
|
return HttpResponseBadRequest()
|
|
old_rating = mark.rating
|
|
old_tags = mark.albummark_tags.all()
|
|
if mark.status != request.POST.get('status'):
|
|
mark.created_time = timezone.now()
|
|
# update
|
|
form = AlbumMarkForm(request.POST, instance=mark)
|
|
else:
|
|
# create
|
|
form = AlbumMarkForm(request.POST)
|
|
|
|
if form.is_valid():
|
|
if form.instance.status == MarkStatusEnum.WISH.value or form.instance.rating == 0:
|
|
form.instance.rating = None
|
|
form.cleaned_data['rating'] = None
|
|
form.instance.owner = request.user
|
|
form.instance.edited_time = timezone.now()
|
|
album = form.instance.album
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
# update album rating
|
|
album.update_rating(old_rating, form.instance.rating)
|
|
form.save()
|
|
# update tags
|
|
if old_tags:
|
|
for tag in old_tags:
|
|
tag.delete()
|
|
if form.cleaned_data['tags']:
|
|
for tag in form.cleaned_data['tags']:
|
|
AlbumTag.objects.create(
|
|
content=tag,
|
|
album=album,
|
|
mark=form.instance
|
|
)
|
|
except IntegrityError as e:
|
|
logger.error(e.__str__())
|
|
return HttpResponseServerError("integrity error")
|
|
|
|
if form.cleaned_data['share_to_mastodon']:
|
|
if not share_mark(form.instance):
|
|
return go_relogin(request)
|
|
else:
|
|
return HttpResponseBadRequest(f"invalid form data {form.errors}")
|
|
|
|
return redirect(reverse("music:retrieve_album", args=[form.instance.album.id]))
|
|
else:
|
|
return HttpResponseBadRequest("invalid method")
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def wish_album(request, id):
|
|
if request.method == 'POST':
|
|
album = get_object_or_404(Album, pk=id)
|
|
params = {
|
|
'owner': request.user,
|
|
'status': MarkStatusEnum.WISH,
|
|
'visibility': 0,
|
|
'album': album,
|
|
}
|
|
try:
|
|
AlbumMark.objects.create(**params)
|
|
except Exception:
|
|
pass
|
|
return HttpResponse("✔️")
|
|
else:
|
|
return HttpResponseBadRequest("invalid method")
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def retrieve_album_mark_list(request, album_id, following_only=False):
|
|
if request.method == 'GET':
|
|
album = get_object_or_404(Album, pk=album_id)
|
|
queryset = AlbumMark.get_available(album, request.user, following_only=following_only)
|
|
paginator = Paginator(queryset, MARK_PER_PAGE)
|
|
page_number = request.GET.get('page', default=1)
|
|
marks = paginator.get_page(page_number)
|
|
marks.pagination = PageLinksGenerator(
|
|
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
|
for m in marks:
|
|
m.get_status_display = MusicMarkStatusTranslator(m.status)
|
|
return render(
|
|
request,
|
|
'music/album_mark_list.html',
|
|
{
|
|
'marks': marks,
|
|
'album': album,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def delete_album_mark(request, id):
|
|
if request.method == 'POST':
|
|
mark = get_object_or_404(AlbumMark, pk=id)
|
|
if request.user != mark.owner:
|
|
return HttpResponseBadRequest()
|
|
album_id = mark.album.id
|
|
try:
|
|
with transaction.atomic():
|
|
# update album rating
|
|
mark.album.update_rating(mark.rating, None)
|
|
mark.delete()
|
|
except IntegrityError as e:
|
|
return HttpResponseServerError()
|
|
return redirect(reverse("music:retrieve_album", args=[album_id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def create_album_review(request, album_id):
|
|
if request.method == 'GET':
|
|
form = AlbumReviewForm(initial={'album': album_id})
|
|
album = get_object_or_404(Album, pk=album_id)
|
|
return render(
|
|
request,
|
|
'music/create_update_album_review.html',
|
|
{
|
|
'form': form,
|
|
'title': _("添加评论"),
|
|
'album': album,
|
|
'submit_url': reverse("music:create_album_review", args=[album_id]),
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
form = AlbumReviewForm(request.POST)
|
|
if form.is_valid():
|
|
form.instance.owner = request.user
|
|
form.save()
|
|
if form.cleaned_data['share_to_mastodon']:
|
|
if not share_review(form.instance):
|
|
return go_relogin(request)
|
|
return redirect(reverse("music:retrieve_album_review", args=[form.instance.id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def update_album_review(request, id):
|
|
if request.method == 'GET':
|
|
review = get_object_or_404(AlbumReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
form = AlbumReviewForm(instance=review)
|
|
album = review.album
|
|
return render(
|
|
request,
|
|
'music/create_update_album_review.html',
|
|
{
|
|
'form': form,
|
|
'title': _("编辑评论"),
|
|
'album': album,
|
|
'submit_url': reverse("music:update_album_review", args=[review.id]),
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
review = get_object_or_404(AlbumReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
form = AlbumReviewForm(request.POST, instance=review)
|
|
if form.is_valid():
|
|
form.instance.edited_time = timezone.now()
|
|
form.save()
|
|
if form.cleaned_data['share_to_mastodon']:
|
|
if not share_review(form.instance):
|
|
return go_relogin(request)
|
|
return redirect(reverse("music:retrieve_album_review", args=[form.instance.id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def delete_album_review(request, id):
|
|
if request.method == 'GET':
|
|
review = get_object_or_404(AlbumReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
review_form = AlbumReviewForm(instance=review)
|
|
return render(
|
|
request,
|
|
'music/delete_album_review.html',
|
|
{
|
|
'form': review_form,
|
|
'review': review,
|
|
}
|
|
)
|
|
elif request.method == 'POST':
|
|
review = get_object_or_404(AlbumReview, pk=id)
|
|
if request.user != review.owner:
|
|
return HttpResponseBadRequest()
|
|
album_id = review.album.id
|
|
review.delete()
|
|
return redirect(reverse("music:retrieve_album", args=[album_id]))
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
def retrieve_album_review(request, id):
|
|
if request.method == 'GET':
|
|
review = get_object_or_404(AlbumReview, pk=id)
|
|
if not review.is_visible_to(request.user):
|
|
msg = _("你没有访问这个页面的权限😥")
|
|
return render(
|
|
request,
|
|
'common/error.html',
|
|
{
|
|
'msg': msg,
|
|
}
|
|
)
|
|
review_form = AlbumReviewForm(instance=review)
|
|
album = review.album
|
|
try:
|
|
mark = AlbumMark.objects.get(owner=review.owner, album=album)
|
|
mark.get_status_display = MusicMarkStatusTranslator(mark.status)
|
|
except ObjectDoesNotExist:
|
|
mark = None
|
|
return render(
|
|
request,
|
|
'music/album_review_detail.html',
|
|
{
|
|
'form': review_form,
|
|
'review': review,
|
|
'album': album,
|
|
'mark': mark,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@mastodon_request_included
|
|
@login_required
|
|
def retrieve_album_review_list(request, album_id):
|
|
if request.method == 'GET':
|
|
album = get_object_or_404(Album, pk=album_id)
|
|
queryset = AlbumReview.get_available(album, request.user)
|
|
paginator = Paginator(queryset, REVIEW_PER_PAGE)
|
|
page_number = request.GET.get('page', default=1)
|
|
reviews = paginator.get_page(page_number)
|
|
reviews.pagination = PageLinksGenerator(
|
|
PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
|
return render(
|
|
request,
|
|
'music/album_review_list.html',
|
|
{
|
|
'reviews': reviews,
|
|
'album': album,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def scrape_album(request):
|
|
if request.method == 'GET':
|
|
keywords = request.GET.get('q')
|
|
form = AlbumForm()
|
|
return render(
|
|
request,
|
|
'music/scrape_album.html',
|
|
{
|
|
'q': keywords,
|
|
'form': form,
|
|
}
|
|
)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
@login_required
|
|
def click_to_scrape_album(request):
|
|
if request.method == "POST":
|
|
url = request.POST.get("url")
|
|
if url:
|
|
return jump_or_scrape(request, url)
|
|
else:
|
|
return HttpResponseBadRequest()
|
|
else:
|
|
return HttpResponseBadRequest()
|