From 3439fb93e8b582777195c2a35deb4a440f374ed5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Jan 2023 09:41:55 -0500 Subject: [PATCH] remove pre-0.5 apps --- .github/workflows/django.yml | 8 +- boofilsic/settings.py | 9 - books/__init__.py | 0 books/admin.py | 9 - books/apps.py | 10 - books/forms.py | 116 -- books/management/commands/fix-book-cover.py | 200 --- books/models.py | 188 --- books/templates/books/create_update.html | 89 -- .../templates/books/create_update_review.html | 110 -- books/templates/books/delete.html | 99 -- books/templates/books/delete_review.html | 102 -- books/templates/books/detail.html | 417 ------ books/templates/books/mark_list.html | 111 -- books/templates/books/review_detail.html | 124 -- books/templates/books/review_list.html | 131 -- books/templates/books/scrape.html | 111 -- books/tests.py | 3 - books/urls.py | 23 - books/views.py | 583 --------- collection/__init__.py | 0 collection/admin.py | 3 - collection/apps.py | 6 - collection/forms.py | 45 - collection/models.py | 128 -- collection/templates/add_to_list.html | 45 - collection/templates/create_update.html | 71 - collection/templates/delete.html | 117 -- collection/templates/detail.html | 150 --- collection/templates/edit_item_comment.html | 5 - collection/templates/entity_list.html | 21 - collection/templates/list.html | 99 -- collection/templates/share_collection.html | 56 - collection/templates/show_item_comment.html | 4 - collection/tests.py | 3 - collection/urls.py | 27 - collection/views.py | 432 ------ games/__init__.py | 0 games/admin.py | 8 - games/apps.py | 10 - games/forms.py | 85 -- games/models.py | 173 --- games/templates/games/create_update.html | 104 -- .../templates/games/create_update_review.html | 124 -- games/templates/games/delete.html | 99 -- games/templates/games/delete_review.html | 101 -- games/templates/games/detail.html | 421 ------ games/templates/games/mark_list.html | 129 -- games/templates/games/review_detail.html | 146 --- games/templates/games/review_list.html | 147 --- games/templates/games/scrape.html | 112 -- games/tests.py | 3 - games/urls.py | 24 - games/views.py | 585 --------- movies/__init__.py | 0 movies/admin.py | 8 - movies/apps.py | 10 - movies/forms.py | 137 -- .../management/commands/fix-movie-poster.py | 203 --- movies/models.py | 293 ----- movies/templates/movies/create_update.html | 102 -- .../movies/create_update_review.html | 168 --- movies/templates/movies/delete.html | 106 -- movies/templates/movies/delete_review.html | 101 -- movies/templates/movies/detail.html | 527 -------- movies/templates/movies/mark_list.html | 148 --- movies/templates/movies/review_detail.html | 160 --- movies/templates/movies/review_list.html | 169 --- movies/templates/movies/scrape.html | 109 -- movies/urls.py | 24 - movies/views.py | 584 --------- music/__init__.py | 0 music/admin.py | 12 - music/apps.py | 11 - music/forms.py | 157 --- music/management/commands/fix-album-cover.py | 205 --- music/models.py | 270 ---- music/templates/music/album_detail.html | 457 ------- music/templates/music/album_mark_list.html | 133 -- .../templates/music/album_review_detail.html | 151 --- music/templates/music/album_review_list.html | 160 --- .../templates/music/create_update_album.html | 103 -- .../music/create_update_album_review.html | 126 -- music/templates/music/create_update_song.html | 91 -- .../music/create_update_song_review.html | 130 -- music/templates/music/delete_album.html | 99 -- .../templates/music/delete_album_review.html | 101 -- music/templates/music/delete_song.html | 99 -- music/templates/music/delete_song_review.html | 101 -- music/templates/music/scrape_album.html | 112 -- music/templates/music/scrape_song.html | 109 -- music/templates/music/song_detail.html | 403 ------ music/templates/music/song_mark_list.html | 137 -- music/templates/music/song_review_detail.html | 147 --- music/templates/music/song_review_list.html | 152 --- music/tests.py | 3 - music/urls.py | 41 - music/views.py | 1154 ----------------- 98 files changed, 2 insertions(+), 13437 deletions(-) delete mode 100644 books/__init__.py delete mode 100644 books/admin.py delete mode 100644 books/apps.py delete mode 100644 books/forms.py delete mode 100644 books/management/commands/fix-book-cover.py delete mode 100644 books/models.py delete mode 100644 books/templates/books/create_update.html delete mode 100644 books/templates/books/create_update_review.html delete mode 100644 books/templates/books/delete.html delete mode 100644 books/templates/books/delete_review.html delete mode 100644 books/templates/books/detail.html delete mode 100644 books/templates/books/mark_list.html delete mode 100644 books/templates/books/review_detail.html delete mode 100644 books/templates/books/review_list.html delete mode 100644 books/templates/books/scrape.html delete mode 100644 books/tests.py delete mode 100644 books/urls.py delete mode 100644 books/views.py delete mode 100644 collection/__init__.py delete mode 100644 collection/admin.py delete mode 100644 collection/apps.py delete mode 100644 collection/forms.py delete mode 100644 collection/models.py delete mode 100644 collection/templates/add_to_list.html delete mode 100644 collection/templates/create_update.html delete mode 100644 collection/templates/delete.html delete mode 100644 collection/templates/detail.html delete mode 100644 collection/templates/edit_item_comment.html delete mode 100644 collection/templates/entity_list.html delete mode 100644 collection/templates/list.html delete mode 100644 collection/templates/share_collection.html delete mode 100644 collection/templates/show_item_comment.html delete mode 100644 collection/tests.py delete mode 100644 collection/urls.py delete mode 100644 collection/views.py delete mode 100644 games/__init__.py delete mode 100644 games/admin.py delete mode 100644 games/apps.py delete mode 100644 games/forms.py delete mode 100644 games/models.py delete mode 100644 games/templates/games/create_update.html delete mode 100644 games/templates/games/create_update_review.html delete mode 100644 games/templates/games/delete.html delete mode 100644 games/templates/games/delete_review.html delete mode 100644 games/templates/games/detail.html delete mode 100644 games/templates/games/mark_list.html delete mode 100644 games/templates/games/review_detail.html delete mode 100644 games/templates/games/review_list.html delete mode 100644 games/templates/games/scrape.html delete mode 100644 games/tests.py delete mode 100644 games/urls.py delete mode 100644 games/views.py delete mode 100644 movies/__init__.py delete mode 100644 movies/admin.py delete mode 100644 movies/apps.py delete mode 100644 movies/forms.py delete mode 100644 movies/management/commands/fix-movie-poster.py delete mode 100644 movies/models.py delete mode 100644 movies/templates/movies/create_update.html delete mode 100644 movies/templates/movies/create_update_review.html delete mode 100644 movies/templates/movies/delete.html delete mode 100644 movies/templates/movies/delete_review.html delete mode 100644 movies/templates/movies/detail.html delete mode 100644 movies/templates/movies/mark_list.html delete mode 100644 movies/templates/movies/review_detail.html delete mode 100644 movies/templates/movies/review_list.html delete mode 100644 movies/templates/movies/scrape.html delete mode 100644 movies/urls.py delete mode 100644 movies/views.py delete mode 100644 music/__init__.py delete mode 100644 music/admin.py delete mode 100644 music/apps.py delete mode 100644 music/forms.py delete mode 100644 music/management/commands/fix-album-cover.py delete mode 100644 music/models.py delete mode 100644 music/templates/music/album_detail.html delete mode 100644 music/templates/music/album_mark_list.html delete mode 100644 music/templates/music/album_review_detail.html delete mode 100644 music/templates/music/album_review_list.html delete mode 100644 music/templates/music/create_update_album.html delete mode 100644 music/templates/music/create_update_album_review.html delete mode 100644 music/templates/music/create_update_song.html delete mode 100644 music/templates/music/create_update_song_review.html delete mode 100644 music/templates/music/delete_album.html delete mode 100644 music/templates/music/delete_album_review.html delete mode 100644 music/templates/music/delete_song.html delete mode 100644 music/templates/music/delete_song_review.html delete mode 100644 music/templates/music/scrape_album.html delete mode 100644 music/templates/music/scrape_song.html delete mode 100644 music/templates/music/song_detail.html delete mode 100644 music/templates/music/song_mark_list.html delete mode 100644 music/templates/music/song_review_detail.html delete mode 100644 music/templates/music/song_review_list.html delete mode 100644 music/tests.py delete mode 100644 music/urls.py delete mode 100644 music/views.py diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 12f4158e..cfaf2a0c 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -23,7 +23,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.10'] + python-version: ['3.10', '3.11'] steps: - uses: actions/checkout@v3 @@ -35,11 +35,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - - run: | - sudo apt-get update - sudo apt-get install --yes --no-install-recommends postgresql-client - PGPASSWORD=admin123 psql template1 -U postgres -h localhost -c 'create extension hstore;' - name: Run Tests run: | - python manage.py makemigrations contenttypes auth mastodon users common management catalog journal social legacy books movies games music collection + python manage.py makemigrations contenttypes auth mastodon users common management catalog journal social legacy python manage.py test diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 5d6fbc4e..0c4415be 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -69,15 +69,6 @@ INSTALLED_APPS += [ "legacy.apps.LegacyConfig", ] -INSTALLED_APPS += [ - "books.apps.BooksConfig", - "movies.apps.MoviesConfig", - "music.apps.MusicConfig", - "games.apps.GamesConfig", - "collection.apps.CollectionConfig", - "upgrade_0_5.apps.Upgrade05Config", -] - MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", diff --git a/books/__init__.py b/books/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/books/admin.py b/books/admin.py deleted file mode 100644 index 75df663b..00000000 --- a/books/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib import admin -from .models import * -from simple_history.admin import SimpleHistoryAdmin - -admin.site.register(Book, SimpleHistoryAdmin) -admin.site.register(BookMark) -admin.site.register(BookReview) -admin.site.register(BookTag) - diff --git a/books/apps.py b/books/apps.py deleted file mode 100644 index b03e2d23..00000000 --- a/books/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig - - -class BooksConfig(AppConfig): - name = 'books' - - def ready(self): - from common.index import Indexer - from .models import Book - Indexer.update_model_indexable(Book) diff --git a/books/forms.py b/books/forms.py deleted file mode 100644 index 27abda07..00000000 --- a/books/forms.py +++ /dev/null @@ -1,116 +0,0 @@ -from django import forms -from django.utils.translation import gettext_lazy as _ -from .models import Book, BookMark, BookReview, BookMarkStatusTranslation -from common.models import MarkStatusEnum -from common.forms import * - - -def BookMarkStatusTranslator(status): - return BookMarkStatusTranslation[status] - - -class BookForm(forms.ModelForm): - pub_year = forms.IntegerField(required=False, max_value=9999, min_value=0, label=_("出版年份")) - pub_month = forms.IntegerField(required=False, max_value=12, min_value=1, label=_("出版月份")) - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - other_info = JSONField(required=False, label=_("其他信息")) - class Meta: - model = Book - fields = [ - 'id', - 'title', - 'source_site', - 'source_url', - 'isbn', - 'author', - 'pub_house', - 'subtitle', - 'translator', - 'orig_title', - 'language', - 'pub_month', - 'pub_year', - 'binding', - 'price', - 'pages', - 'cover', - 'brief', - 'contents', - 'other_info', - ] - labels = { - 'title': _("书名"), - 'isbn': _("ISBN"), - 'author': _("作者"), - 'pub_house': _("出版社"), - 'subtitle': _("副标题"), - 'translator': _("译者"), - 'orig_title': _("原作名"), - 'language': _("语言"), - 'pub_month': _("出版月份"), - 'pub_year': _("出版年份"), - 'binding': _("装帧"), - 'price': _("定价"), - 'pages': _("页数"), - 'cover': _("封面"), - 'brief': _("简介"), - 'contents': _("目录"), - 'other_info': _("其他信息"), - } - - widgets = { - 'author': forms.TextInput(attrs={'placeholder': _("多个作者使用英文逗号分隔")}), - 'translator': forms.TextInput(attrs={'placeholder': _("多个译者使用英文逗号分隔")}), - # 'cover': forms.FileInput(), - 'cover': PreviewImageInput(), - } - - def clean_isbn(self): - isbn = self.cleaned_data.get('isbn') - if isbn: - isbn = isbn.strip() - return isbn - - -class BookMarkForm(MarkForm): - - STATUS_CHOICES = [(v, BookMarkStatusTranslator(v)) - for v in MarkStatusEnum.values] - - status = forms.ChoiceField( - label=_(""), - widget=forms.RadioSelect(), - choices=STATUS_CHOICES - ) - - class Meta: - model = BookMark - fields = [ - 'id', - 'book', - 'status', - 'rating', - 'text', - 'visibility', - ] - widgets = { - 'book': forms.TextInput(attrs={"hidden": ""}), - } - - -class BookReviewForm(ReviewForm): - - class Meta: - model = BookReview - fields = [ - 'id', - 'book', - 'title', - 'content', - 'visibility' - ] - widgets = { - 'book': forms.TextInput(attrs={"hidden": ""}), - } - - diff --git a/books/management/commands/fix-book-cover.py b/books/management/commands/fix-book-cover.py deleted file mode 100644 index ae9227b5..00000000 --- a/books/management/commands/fix-book-cover.py +++ /dev/null @@ -1,200 +0,0 @@ -from django.core.management.base import BaseCommand -from django.core.files.uploadedfile import SimpleUploadedFile -from django.conf import settings -from common.scraper import * -from books.models import Book -from books.forms import BookForm -import requests -import re -import filetype -from lxml import html -from PIL import Image -from io import BytesIO - - -class DoubanPatcherMixin: - @classmethod - def download_page(cls, url, headers): - url = cls.get_effective_url(url) - r = None - error = 'DoubanScrapper: error occured when downloading ' + url - content = None - - def get(url, timeout): - nonlocal r - # print('Douban GET ' + url) - try: - r = requests.get(url, timeout=timeout) - except Exception as e: - r = requests.Response() - r.status_code = f"Exception when GET {url} {e}" + url - # print('Douban CODE ' + str(r.status_code)) - return r - - def check_content(): - nonlocal r, error, content - content = None - if r.status_code == 200: - content = r.content.decode('utf-8') - if content.find('关于豆瓣') == -1: - # with open('/tmp/temp.html', 'w', encoding='utf-8') as fp: - # fp.write(content) - content = None - error = error + 'Content not authentic' # response is garbage - elif re.search('不存在[^<]+', content, re.MULTILINE): - content = None - error = error + 'Not found or hidden by Douban' - else: - error = error + str(r.status_code) - - def fix_wayback_links(): - nonlocal content - # fix links - content = re.sub(r'href="http[^"]+http', r'href="http', content) - # https://img9.doubanio.com/view/subject/{l|m|s}/public/s1234.jpg - content = re.sub(r'src="[^"]+/(s\d+\.\w+)"', - r'src="https://img9.doubanio.com/view/subject/m/public/\1"', content) - # https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2681329386.jpg - # https://img9.doubanio.com/view/photo/{l|m|s}/public/p1234.webp - content = re.sub(r'src="[^"]+/(p\d+\.\w+)"', - r'src="https://img9.doubanio.com/view/photo/m/public/\1"', content) - - # Wayback Machine: get latest available - def wayback(): - nonlocal r, error, content - error = error + '\nWayback: ' - get('http://archive.org/wayback/available?url=' + url, 10) - if r.status_code == 200: - w = r.json() - if w['archived_snapshots'] and w['archived_snapshots']['closest']: - get(w['archived_snapshots']['closest']['url'], 10) - check_content() - if content is not None: - fix_wayback_links() - else: - error = error + 'No snapshot available' - else: - error = error + str(r.status_code) - - # Wayback Machine: guess via CDX API - def wayback_cdx(): - nonlocal r, error, content - error = error + '\nWayback: ' - get('http://web.archive.org/cdx/search/cdx?url=' + url, 10) - if r.status_code == 200: - dates = re.findall(r'[^\s]+\s+(\d+)\s+[^\s]+\s+[^\s]+\s+\d+\s+[^\s]+\s+\d{5,}', - r.content.decode('utf-8')) - # assume snapshots whose size >9999 contain real content, use the latest one of them - if len(dates) > 0: - get('http://web.archive.org/web/' + dates[-1] + '/' + url, 10) - check_content() - if content is not None: - fix_wayback_links() - else: - error = error + 'No snapshot available' - else: - error = error + str(r.status_code) - - def latest(): - nonlocal r, error, content - if settings.SCRAPESTACK_KEY is None: - error = error + '\nDirect: ' - get(url, 60) - else: - error = error + '\nScrapeStack: ' - get(f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}', 60) - check_content() - - wayback_cdx() - if content is None: - latest() - - if content is None: - logger.error(error) - content = '' - return html.fromstring(content) - - @classmethod - def download_image(cls, url, item_url=None): - if url is None: - logger.error(f"Douban: no image url for {item_url}") - return None, None - raw_img = None - ext = None - - dl_url = url - if settings.SCRAPESTACK_KEY is not None: - dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}' - - try: - img_response = requests.get(dl_url, timeout=90) - if img_response.status_code == 200: - raw_img = img_response.content - img = Image.open(BytesIO(raw_img)) - img.load() # corrupted image will trigger exception - content_type = img_response.headers.get('Content-Type') - ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension - else: - logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}") - # raise RuntimeError(f"Douban: download image failed {img_response.status_code} {dl_url}") - except Exception as e: - raw_img = None - ext = None - logger.error(f"Douban: download image failed {e} {dl_url} {item_url}") - if raw_img is None and settings.SCRAPESTACK_KEY is not None: - try: - img_response = requests.get(dl_url, timeout=90) - if img_response.status_code == 200: - raw_img = img_response.content - img = Image.open(BytesIO(raw_img)) - img.load() # corrupted image will trigger exception - content_type = img_response.headers.get('Content-Type') - ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension - else: - logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}") - except Exception as e: - raw_img = None - ext = None - logger.error(f"Douban: download image failed {e} {dl_url} {item_url}") - return raw_img, ext - - -class DoubanBookPatcher(DoubanPatcherMixin, AbstractScraper): - site_name = SourceSiteEnum.DOUBAN.value - host = 'book.douban.com' - data_class = Book - form_class = BookForm - - regex = re.compile(r"https://book\.douban\.com/subject/\d+/{0,1}") - - def scrape(self, url): - headers = DEFAULT_REQUEST_HEADERS.copy() - headers['Host'] = self.host - content = self.download_page(url, headers) - 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, ext = self.download_image(img_url, url) - return raw_img, ext - - -class Command(BaseCommand): - help = 'fix cover image' - - def add_arguments(self, parser): - parser.add_argument('threadId', type=int, help='% 8') - - def handle(self, *args, **options): - t = int(options['threadId']) - for m in Book.objects.filter(cover='book/default.svg', source_site='douban'): - if m.id % 8 == t: - self.stdout.write(f'Re-fetching {m.source_url}') - try: - raw_img, img_ext = DoubanBookPatcher.scrape(m.source_url) - if img_ext is not None: - m.cover = SimpleUploadedFile('temp.' + img_ext, raw_img) - m.save() - self.stdout.write(self.style.SUCCESS(f'Saved {m.source_url}')) - else: - self.stdout.write(self.style.ERROR(f'Skipped {m.source_url}')) - except Exception as e: - print(e) diff --git a/books/models.py b/books/models.py deleted file mode 100644 index a4f72ff6..00000000 --- a/books/models.py +++ /dev/null @@ -1,188 +0,0 @@ -import django.contrib.postgres.fields as postgres -from django.utils.translation import gettext_lazy as _ -from django.db import models -from django.shortcuts import reverse -from common.models import Entity, Mark, Review, Tag, MarkStatusEnum -from common.utils import GenerateDateUUIDMediaFilePath -from django.conf import settings -from django.db.models import Q -from simple_history.models import HistoricalRecords - - -BookMarkStatusTranslation = { - MarkStatusEnum.DO.value: _("在读"), - MarkStatusEnum.WISH.value: _("想读"), - MarkStatusEnum.COLLECT.value: _("读过") -} - - -def book_cover_path(instance, filename): - return GenerateDateUUIDMediaFilePath(instance, filename, settings.BOOK_MEDIA_PATH_ROOT) - - -class Book(Entity): - # widely recognized name, usually in Chinese - title = models.CharField(_("title"), max_length=500) - subtitle = models.CharField( - _("subtitle"), blank=True, default='', max_length=500) - # original name, for books in foreign language - orig_title = models.CharField( - _("original title"), blank=True, default='', max_length=500) - - author = postgres.ArrayField( - models.CharField(_("author"), blank=True, default='', max_length=200), - null=True, - blank=True, - default=list, - ) - translator = postgres.ArrayField( - models.CharField(_("translator"), blank=True, - default='', max_length=200), - null=True, - blank=True, - default=list, - ) - language = models.CharField( - _("language"), blank=True, default='', max_length=50) - pub_house = models.CharField( - _("publishing house"), blank=True, default='', max_length=200) - pub_year = models.IntegerField(_("published year"), null=True, blank=True) - pub_month = models.IntegerField( - _("published month"), null=True, blank=True) - binding = models.CharField( - _("binding"), blank=True, default='', max_length=200) - # since data origin is not formatted and might be CNY USD or other currency, use char instead - price = models.CharField(_("pricing"), blank=True, - default='', max_length=50) - pages = models.PositiveIntegerField(_("pages"), null=True, blank=True) - isbn = models.CharField(_("ISBN"), blank=True, null=False, - max_length=20, db_index=True, default='') - # to store previously scrapped data - cover = models.ImageField(_("cover picture"), upload_to=book_cover_path, - default=settings.DEFAULT_BOOK_IMAGE, blank=True) - contents = models.TextField(blank=True, default="") - history = HistoricalRecords() - - class Meta: - constraints = [ - models.CheckConstraint(check=models.Q( - pub_year__gte=0), name='pub_year_lowerbound'), - models.CheckConstraint(check=models.Q( - pub_month__lte=12), name='pub_month_upperbound'), - models.CheckConstraint(check=models.Q( - pub_month__gte=1), name='pub_month_lowerbound'), - ] - - def __str__(self): - return self.title - - def get_json(self): - r = { - 'subtitle': self.subtitle, - 'original_title': self.orig_title, - 'author': self.author, - 'translator': self.translator, - 'publisher': self.pub_house, - 'publish_year': self.pub_year, - 'publish_month': self.pub_month, - 'language': self.language, - 'isbn': self.isbn, - } - r.update(super().get_json()) - return r - - def get_absolute_url(self): - return reverse("books:retrieve", args=[self.id]) - - @property - def wish_url(self): - return reverse("books:wish", args=[self.id]) - - def get_tags_manager(self): - return self.book_tags - - def get_related_books(self): - qs = Q(orig_title=self.title) - if self.isbn: - qs = qs | Q(isbn=self.isbn) - if self.orig_title: - qs = qs | Q(title=self.orig_title) - qs = qs | Q(orig_title=self.orig_title) - qs = qs & ~Q(id=self.id) - return Book.objects.filter(qs) - - def get_identicals(self): - qs = Q(orig_title=self.title) - if self.isbn: - qs = Q(isbn=self.isbn) - # qs = qs & ~Q(id=self.id) - return Book.objects.filter(qs) - else: - return [self] # Book.objects.filter(id=self.id) - - @property - def year(self): - return self.pub_year - - @property - def verbose_category_name(self): - return _("书籍") - - @property - def mark_class(self): - return BookMark - - @property - def tag_class(self): - return BookTag - - -class BookMark(Mark): - book = models.ForeignKey( - Book, on_delete=models.CASCADE, related_name='book_marks', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'book'], name="unique_book_mark") - ] - - @property - def translated_status(self): - return BookMarkStatusTranslation[self.status] - - -class BookReview(Review): - book = models.ForeignKey( - Book, on_delete=models.CASCADE, related_name='book_reviews', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'book'], name="unique_book_review") - ] - - @property - def url(self): - return reverse("books:retrieve_review", args=[self.id]) - - @property - def item(self): - return self.book - - -class BookTag(Tag): - book = models.ForeignKey( - Book, on_delete=models.CASCADE, related_name='book_tags', null=True) - mark = models.ForeignKey( - BookMark, on_delete=models.CASCADE, related_name='bookmark_tags', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['content', 'mark'], name="unique_bookmark_tag") - ] - - @property - def item(self): - return self.book diff --git a/books/templates/books/create_update.html b/books/templates/books/create_update.html deleted file mode 100644 index f48cd4b9..00000000 --- a/books/templates/books/create_update.html +++ /dev/null @@ -1,89 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {{ title }} - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
- {% if is_update and form.source_site.value != 'in-site' %} -
-
-
-
{% trans '源网站' %}: {{ form.source_site.value }}
-
-
- {% csrf_token %} - -
-
-
-
-
- {% endif %} - -
- {% comment %} {% trans '>>> 试试一键剽取~ <<<' %} {% endcomment %} -
- {% csrf_token %} - {{ form.media }} - {{ form }} - -
-
- -
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/books/templates/books/create_update_review.html b/books/templates/books/create_update_review.html deleted file mode 100644 index d440b96f..00000000 --- a/books/templates/books/create_update_review.html +++ /dev/null @@ -1,110 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - {{ site_name }} - {{ title }} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - -
-
- -
{{ book.title }} - - {{ book.get_source_site_display }} - -
-
{% if book.isbn %}{% trans 'ISBN:' %}{{ book.isbn }}{% endif %}
-
{% if book.author %}{% trans '作者:' %} - {% for author in book.author %} - {{ author }} - {% endfor %} - {% endif %}
-
{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}
-
{%if book.pub_year %}{% trans '出版时间:' %}{{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{ book.pub_month }}{% trans '月' %}{% endif %}{% endif %}
- - {% if book.rating %} - {% trans '评分:' %} - {{ book.rating }} - {% endif %} -
-
-
- -
- {% csrf_token %} - {{ form.book }} -
- {{ form.title.label }} -
- {{ form.title }} -
- - {{ form.content.label }} - - - {% trans '预览' %} - -
-
- {{ form.content }} -
-
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
-
-
- - {{ form.visibility.label }}{{ form.visibility }} -
- -
-
- -
- {{ form.media }} -
- -
- -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - diff --git a/books/templates/books/delete.html b/books/templates/books/delete.html deleted file mode 100644 index c1792a20..00000000 --- a/books/templates/books/delete.html +++ /dev/null @@ -1,99 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {% trans '删除图书' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这本书吗?相关评论和标记将一并删除。' %}
- -
-
- - - -
-
- -
- {{ book.title }} - - {{ book.get_source_site_display }} - -
- - {% if book.rating %} - {% trans '评分:' %} - - {{ book.rating }} - {% else %} - {% trans '评分:暂无评分' %} - {% endif %} - - {% if book.last_editor %} -
- {% trans '最近编辑者:' %} - - {{ book.last_editor | default:"" }} - -
- {% endif %} - -
{% trans '上次编辑时间:' %}{{ book.edited_time }}
- - {% if book.book_marks.all %} -
{% trans '这个条目有' %} {{ book.book_marks.count }} 个标记
- {% endif %} - {% if book.book_reviews.all %} -
{% trans '这个条目有' %} {{ book.book_reviews.count }} 个评论
- {% endif %} - -
-
-
-
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/books/templates/books/delete_review.html b/books/templates/books/delete_review.html deleted file mode 100644 index 77c88340..00000000 --- a/books/templates/books/delete_review.html +++ /dev/null @@ -1,102 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '删除评论' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这篇评论吗?' %}
- -
- - -
- -
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} - -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- - -
-
- {{ form.content }} -
- {{ form.media }} - -
- -
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/books/templates/books/detail.html b/books/templates/books/detail.html deleted file mode 100644 index d9ebf2bd..00000000 --- a/books/templates/books/detail.html +++ /dev/null @@ -1,417 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load strip_scheme %} -{% load thumb %} - - - - - - - - - - - - - {% if book.author %} - - {% endif %} - {% if book.isbn %} - - {% endif %} - - {{ site_name }} - {% trans '书籍详情' %} | {{ book.title }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - {{ book.title }} - - -
-
- {{ book.title }} - - {{ book.get_source_site_display }} - -
- -
-
- {% if book.rating and book.rating_number >= 5 %} - - {{ book.rating }} - ({{ book.rating_number }}人评分) - {% else %} - {% trans '评分:评分人数不足' %} - {% endif %} -
-
{% if book.isbn %}{% trans 'ISBN:' %}{{ book.isbn }}{% endif %}
-
{% if book.author %}{% trans '作者:' %} - {% for author in book.author %} - {{ author }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
-
{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}
-
{% if book.subtitle %}{% trans '副标题:' %}{{ book.subtitle }}{% endif %}
-
{% if book.translator %}{% trans '译者:' %} - {% for translator in book.translator %} - {{ translator }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
-
{% if book.orig_title %}{% trans '原作名:' %}{{ book.orig_title }}{% endif %}
-
{% if book.language %}{% trans '语言:' %}{{ book.language }}{% endif %}
-
{%if book.pub_year %}{% trans '出版时间:' %}{{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{ book.pub_month }}{% trans '月' %}{% endif %}{% endif %}
-
-
- -
{% if book.binding %}{% trans '装帧:' %}{{ book.binding }}{% endif %}
-
{% if book.price %}{% trans '定价:' %}{{ book.price }}{% endif %}
-
{% if book.pages %}{% trans '页数:' %}{{ book.pages }}{% endif %}
- {% if book.other_info %} - {% for k, v in book.other_info.items %} -
- {{ k }}:{{ v | urlize }} -
- {% endfor %} - {% endif %} - - - {% if book.last_editor and book.last_editor.preference.show_last_edit or user.is_staff %} -
{% trans '最近编辑者:' %}{{ book.last_editor | default:"" }}
- {% endif %} - -
- {% trans '编辑这本书' %} - {% if user.is_staff %} - / {% trans '删除' %} - {% endif %} -
-
- -
- - {% for tag_dict in book_tag_list %} - {% for k, v in tag_dict.items %} - {% if k == 'content' %} - - {{ v }} - - {% endif %} - {% endfor %} - {% endfor %} - -
-
-
-
- {% if book.brief %} -
-
{% trans '简介' %}
- -

{{ book.brief | linebreaksbr }}

- - - -
- {% endif %} - - {% if book.contents %} -
-
{% trans '目录' %}
-

{{ book.contents | linebreaksbr }}

- -
- {% endif %} - -
-
{% trans '这本书的标记' %}
- {% trans '全部标记' %} - 关注的人的标记 - {% include "partial/mark_list.html" with mark_list=mark_list current_item=book %} -
-
-
{% trans '这本书的评论' %}
- {% if review_list_more %} - {% trans '全部评论' %} - {% endif %} - {% if review_list %} - - {% else %} -
{% trans '暂无评论' %}
- {% endif %} -
-
-
- -
-
- {% if mark %} -
- - {% trans '我' %}{{ mark.get_status_display }} - {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} - {% if mark.rating %} - - {% endif %} - {% endif %} - {% if mark.visibility > 0 %} - - {% endif %} - - {% trans '修改' %} -
- {% csrf_token %} - {% trans '删除' %} -
-
-
- -
{{ mark.created_time }}
- - {% if mark.text %} -

{{ mark.text }}

- {% endif %} -
- - {% for tag in mark_tags %} - {{ tag }} - {% endfor %} - -
-
- {% else %} -
-
{% trans '标记这本书' %}
-
- - - -
-
- {% endif %} -
- -
- {% if review %} -
- - {% trans '我的评论' %} - {% if review.visibility > 0 %} - - {% endif %} - - - {% trans '编辑' %} - {% trans '删除' %} - - -
{{ review.edited_time }}
- - - {{ review.title }} - -
- {% else %} - -
-
{% trans '我的评论' %}
- -
- - {% endif %} -
- - {% if book.get_related_books.count > 0 %} -
-
-
{% trans '相关书目' %}
-
- {% for b in book.get_related_books %} -

- {{ b.title }} - ({{ b.pub_house }} {{ b.pub_year }}) - {{ b.get_source_site_display }} -

- {% endfor %} -
-
-
- {% endif %} - - {% if book.isbn %} -
-
-
{% trans '借阅或购买' %}
- -
-
- {% endif %} - - {% if collection_list %} -
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %} -

- {{ c.title }} -

- {% endfor %} -
- -
-
-
-
- {% endif %} -
-
-
- -
- {% include "partial/_footer.html" %} -
- -
- - - -
-
- - - - - - diff --git a/books/templates/books/mark_list.html b/books/templates/books/mark_list.html deleted file mode 100644 index bd81a459..00000000 --- a/books/templates/books/mark_list.html +++ /dev/null @@ -1,111 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ book.title }}{% trans '的标记' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ book.title }}{% trans ' 的标记' %} -
- {% include "partial/mark_list.html" with mark_list=marks current_item=book %} -
- -
-
- -
-
-
-
- -
-
-
{{ book.title }} - - {{ book.get_source_site_display }} - -
- - {% if book.isbn %} -
ISBN: {{ book.isbn }}
- {% endif %} - -
{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}
- {% if book.rating %} - {% trans '评分: ' %} - {{ book.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/books/templates/books/review_detail.html b/books/templates/books/review_detail.html deleted file mode 100644 index a08d868e..00000000 --- a/books/templates/books/review_detail.html +++ /dev/null @@ -1,124 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - {{ site_name }}{% trans '书评' %} - {{ review.title }} - - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- {% if request.user == review.owner %} - {% trans '编辑' %} - {% trans '删除' %} - {% endif %} -
-
- -
- {{ form.content }} -
- {{ form.media }} - {% csrf_token %} -
-
- -
-
-
-
-
- -
-
-
- {{ book.title }} - - {{ book.get_source_site_display }} - -
- - {% if book.isbn %} -
ISBN: {{ book.isbn }}
- {% endif %} - -
{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}
- {% if book.rating %} - {% trans '评分: ' %} - {{ book.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/books/templates/books/review_list.html b/books/templates/books/review_list.html deleted file mode 100644 index 77f2db7e..00000000 --- a/books/templates/books/review_list.html +++ /dev/null @@ -1,131 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ book.title }}{% trans '的评论' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ book.title }}{% trans ' 的评论' %} -
- -
- -
-
- -
-
-
-
- -
-
-
{{ book.title }} - - {{ book.get_source_site_display }} - -
- - {% if book.isbn %} -
ISBN: {{ book.isbn }}
- {% endif %} - -
{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}
- {% if book.rating %} - {% trans '评分: ' %} - {{ book.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/books/templates/books/scrape.html b/books/templates/books/scrape.html deleted file mode 100644 index 81fa6a99..00000000 --- a/books/templates/books/scrape.html +++ /dev/null @@ -1,111 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '从豆瓣获取数据' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {% trans '根据豆瓣内容填写下方表单' %} -
- -
-
- - -
-
-
-
- {% csrf_token %} - {{ form.media }} - {{ form }} -
- {% trans '剽取!' %} -
-
-
-
- -
-
-
- {% trans '复制详情页链接' %} -
-
- {% csrf_token %} - - -
-
- -
-
-
- -
- {% include "partial/_footer.html" %} - -
- - - - - diff --git a/books/tests.py b/books/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/books/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/books/urls.py b/books/urls.py deleted file mode 100644 index 518ea096..00000000 --- a/books/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.urls import path, re_path -from .views import * - - -app_name = 'books' -urlpatterns = [ - path('create/', create, name='create'), - path('/', retrieve, name='retrieve'), - path('update//', update, name='update'), - path('delete//', delete, name='delete'), - path('rescrape//', rescrape, name='rescrape'), - path('mark/', create_update_mark, name='create_update_mark'), - path('wish//', wish, name='wish'), - re_path('(?P[0-9]+)/mark/list/(?:(?P\\d+))?', retrieve_mark_list, name='retrieve_mark_list'), - path('mark/delete//', delete_mark, name='delete_mark'), - path('/review/create/', create_review, name='create_review'), - path('review/update//', update_review, name='update_review'), - path('review/delete//', delete_review, name='delete_review'), - path('review//', retrieve_review, name='retrieve_review'), - path('/review/list/', retrieve_review_list, name='retrieve_review_list'), - path('scrape/', scrape, name='scrape'), - path('click_to_scrape/', click_to_scrape, name='click_to_scrape'), -] diff --git a/books/views.py b/books/views.py deleted file mode 100644 index de197471..00000000 --- a/books/views.py +++ /dev/null @@ -1,583 +0,0 @@ -import logging -from django.shortcuts import render, get_object_or_404, redirect, reverse -from django.contrib.auth.decorators import login_required, permission_required -from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import IntegrityError, transaction -from django.db.models import Count -from django.utils import timezone -from django.core.paginator import Paginator -from mastodon import mastodon_request_included -from mastodon.models import MastodonApplication -from mastodon.api import share_mark, share_review -from common.utils import PageLinksGenerator -from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin -from common.models import SourceSiteEnum -from .models import * -from .forms import * -from .forms import BookMarkStatusTranslator -from django.conf import settings -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(request): - if request.method == 'GET': - form = BookForm() - return render( - request, - 'books/create_update.html', - { - 'form': form, - 'title': _('添加书籍'), - 'submit_url': reverse("books:create"), - # 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 = BookForm(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("books:retrieve", args=[form.instance.id])) - else: - return render( - request, - 'books/create_update.html', - { - 'form': form, - 'title': _('添加书籍'), - 'submit_url': reverse("books:create"), - # 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(Book, 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("books:retrieve", args=[form.instance.id])) - - -@login_required -def update(request, id): - if request.method == 'GET': - book = get_object_or_404(Book, pk=id) - form = BookForm(instance=book) - return render( - request, - 'books/create_update.html', - { - 'form': form, - 'is_update': True, - 'title': _('修改书籍'), - 'submit_url': reverse("books:update", args=[book.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - elif request.method == 'POST': - book = get_object_or_404(Book, pk=id) - form = BookForm(request.POST, request.FILES, instance=book) - 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, - 'books/create_update.html', - { - 'form': form, - 'is_update': True, - 'title': _('修改书籍'), - 'submit_url': reverse("books:update", args=[book.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - return redirect(reverse("books:retrieve", args=[form.instance.id])) - - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -# @login_required -def retrieve(request, id): - if request.method == 'GET': - book = get_object_or_404(Book, pk=id) - mark = None - mark_tags = None - review = None - - # retreive tags - book_tag_list = book.book_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 = BookMark.objects.get(owner=request.user, book=book) - except ObjectDoesNotExist: - mark = None - if mark: - mark_tags = mark.bookmark_tags.all() - mark.get_status_display = BookMarkStatusTranslator(mark.status) - mark_form = BookMarkForm(instance=mark, initial={ - 'tags': mark_tags - }) - else: - mark_form = BookMarkForm(initial={ - 'book': book, - '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 = BookReview.objects.get(owner=request.user, book=book) - 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 = BookMark.get_available_for_identicals(book, request.user) - review_list = BookReview.get_available_for_identicals(book, 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 = BookMarkStatusTranslator(m.status) - review_list_more = True if len( - review_list) > REVIEW_NUMBER else False - review_list = review_list[:REVIEW_NUMBER] - all_collections = CollectionItem.objects.filter(book=book).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20] - collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections)) - - # 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, - 'books/detail.html', - { - 'book': book, - '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, - 'book_tag_list': book_tag_list, - 'mark_tags': mark_tags, - 'collection_list': collection_list, - } - ) - else: - logger.warning('non-GET method at /book/') - return HttpResponseBadRequest() - - -@permission_required('books.delete_book') -@login_required -def delete(request, id): - if request.method == 'GET': - book = get_object_or_404(Book, pk=id) - return render( - request, - 'books/delete.html', - { - 'book': book, - } - ) - elif request.method == 'POST': - if request.user.is_staff: - # only staff has right to delete - book = get_object_or_404(Book, pk=id) - book.delete() - return redirect(reverse("common:home")) - else: - raise PermissionDenied() - else: - return HttpResponseBadRequest() - - -# user owned entites -########################### -@mastodon_request_included -@login_required -def create_update_mark(request): - # check list: - # clean rating if is wish - # transaction on updating book rating - # owner check(guarantee) - if request.method == 'POST': - pk = request.POST.get('id') - old_rating = None - old_tags = None - if not pk: - book_id = request.POST.get('book') - mark = BookMark.objects.filter(book_id=book_id, owner=request.user).first() - if mark: - pk = mark.id - if pk: - mark = get_object_or_404(BookMark, pk=pk) - if request.user != mark.owner: - return HttpResponseBadRequest() - old_rating = mark.rating - old_tags = mark.bookmark_tags.all() - if mark.status != request.POST.get('status'): - mark.created_time = timezone.now() - # update - form = BookMarkForm(request.POST, instance=mark) - else: - # create - form = BookMarkForm(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() - book = form.instance.book - - try: - with transaction.atomic(): - # update book rating - book.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']: - BookTag.objects.create( - content=tag, - book=book, - 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("books:retrieve", args=[form.instance.book.id])) - else: - return HttpResponseBadRequest("invalid method") - - -@mastodon_request_included -@login_required -def wish(request, id): - if request.method == 'POST': - book = get_object_or_404(Book, pk=id) - params = { - 'owner': request.user, - 'status': MarkStatusEnum.WISH, - 'visibility': request.user.preference.default_visibility, - 'book': book, - } - try: - BookMark.objects.create(**params) - except Exception: - pass - return HttpResponse("✔️") - else: - return HttpResponseBadRequest("invalid method") - - -@mastodon_request_included -@login_required -def retrieve_mark_list(request, book_id, following_only=False): - if request.method == 'GET': - book = get_object_or_404(Book, pk=book_id) - queryset = BookMark.get_available_for_identicals(book, 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 = BookMarkStatusTranslator(m.status) - return render( - request, - 'books/mark_list.html', - { - 'marks': marks, - 'book': book, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def delete_mark(request, id): - if request.method == 'POST': - mark = get_object_or_404(BookMark, pk=id) - if request.user != mark.owner: - return HttpResponseBadRequest() - book_id = mark.book.id - try: - with transaction.atomic(): - # update book rating - mark.book.update_rating(mark.rating, None) - mark.delete() - except IntegrityError as e: - return HttpResponseServerError() - return redirect(reverse("books:retrieve", args=[book_id])) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def create_review(request, book_id): - if request.method == 'GET': - form = BookReviewForm(initial={'book': book_id}) - book = get_object_or_404(Book, pk=book_id) - return render( - request, - 'books/create_update_review.html', - { - 'form': form, - 'title': _("添加评论"), - 'book': book, - 'submit_url': reverse("books:create_review", args=[book_id]), - } - ) - elif request.method == 'POST': - form = BookReviewForm(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("books:retrieve_review", args=[form.instance.id])) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def update_review(request, id): - if request.method == 'GET': - review = get_object_or_404(BookReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - form = BookReviewForm(instance=review) - book = review.book - return render( - request, - 'books/create_update_review.html', - { - 'form': form, - 'title': _("编辑评论"), - 'book': book, - 'submit_url': reverse("books:update_review", args=[review.id]), - } - ) - elif request.method == 'POST': - review = get_object_or_404(BookReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - form = BookReviewForm(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("books:retrieve_review", args=[form.instance.id])) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() - - -@login_required -def delete_review(request, id): - if request.method == 'GET': - review = get_object_or_404(BookReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - review_form = BookReviewForm(instance=review) - return render( - request, - 'books/delete_review.html', - { - 'form': review_form, - 'review': review, - } - ) - elif request.method == 'POST': - review = get_object_or_404(BookReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - book_id = review.book.id - review.delete() - return redirect(reverse("books:retrieve", args=[book_id])) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -def retrieve_review(request, id): - if request.method == 'GET': - review = get_object_or_404(BookReview, pk=id) - if not review.is_visible_to(request.user): - msg = _("你没有访问这个页面的权限😥") - return render( - request, - 'common/error.html', - { - 'msg': msg, - } - ) - review_form = BookReviewForm(instance=review) - book = review.book - try: - mark = BookMark.objects.get(owner=review.owner, book=book) - mark.get_status_display = BookMarkStatusTranslator(mark.status) - except ObjectDoesNotExist: - mark = None - return render( - request, - 'books/review_detail.html', - { - 'form': review_form, - 'review': review, - 'book': book, - 'mark': mark, - } - ) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def retrieve_review_list(request, book_id): - if request.method == 'GET': - book = get_object_or_404(Book, pk=book_id) - queryset = BookReview.get_available_for_identicals(book, 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, - 'books/review_list.html', - { - 'reviews': reviews, - 'book': book, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def scrape(request): - if request.method == 'GET': - keywords = request.GET.get('q') - form = BookForm() - return render( - request, - 'books/scrape.html', - { - 'q': keywords, - 'form': form, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def click_to_scrape(request): - if request.method == "POST": - url = request.POST.get("url") - if url: - return jump_or_scrape(request, url) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() diff --git a/collection/__init__.py b/collection/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/collection/admin.py b/collection/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/collection/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/collection/apps.py b/collection/apps.py deleted file mode 100644 index 7edc77d1..00000000 --- a/collection/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CollectionConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'collection' diff --git a/collection/forms.py b/collection/forms.py deleted file mode 100644 index ed84f5a1..00000000 --- a/collection/forms.py +++ /dev/null @@ -1,45 +0,0 @@ -from django import forms -from django.utils.translation import gettext_lazy as _ -from .models import Collection -from common.forms import * - - -COLLABORATIVE_CHOICES = [ - (0, _("仅限创建者")), - (1, _("创建者及其互关用户")), -] - - -class CollectionForm(forms.ModelForm): - # id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - title = forms.CharField(label=_("标题")) - description = MarkdownxFormField(label=_("详细介绍 (Markdown)")) - # share_to_mastodon = forms.BooleanField(label=_("分享到联邦网络"), initial=True, required=False) - visibility = forms.TypedChoiceField( - label=_("可见性"), - initial=0, - coerce=int, - choices=VISIBILITY_CHOICES, - widget=forms.RadioSelect - ) - collaborative = forms.TypedChoiceField( - label=_("协作整理权限"), - initial=0, - coerce=int, - choices=COLLABORATIVE_CHOICES, - widget=forms.RadioSelect - ) - - class Meta: - model = Collection - fields = [ - 'title', - 'description', - 'cover', - 'visibility', - 'collaborative', - ] - - widgets = { - 'cover': PreviewImageInput(), - } diff --git a/collection/models.py b/collection/models.py deleted file mode 100644 index ad149f5c..00000000 --- a/collection/models.py +++ /dev/null @@ -1,128 +0,0 @@ -from django.db import models -from markdown import markdown -from common.models import UserOwnedEntity -from movies.models import Movie -from books.models import Book -from music.models import Song, Album -from games.models import Game -from markdownx.models import MarkdownxField -from django.utils.translation import gettext_lazy as _ -from django.conf import settings -from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath -from common.models import RE_HTML_TAG -from django.shortcuts import reverse - - -def collection_cover_path(instance, filename): - return GenerateDateUUIDMediaFilePath(instance, filename, settings.COLLECTION_MEDIA_PATH_ROOT) - - -class Collection(UserOwnedEntity): - title = models.CharField(max_length=200) - description = MarkdownxField() - cover = models.ImageField(_("封面"), upload_to=collection_cover_path, default=settings.DEFAULT_COLLECTION_IMAGE, blank=True) - collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers - - def __str__(self): - return f"Collection({self.id} {self.owner} {self.title})" - - @property - def translated_status(self): - return '创建了收藏单' - - @property - def collectionitem_list(self): - return sorted(list(self.collectionitem_set.all()), key=lambda i: i.position) - - @property - def item_list(self): - return map(lambda i: i.item, self.collectionitem_list) - - @property - def plain_description(self): - html = markdown(self.description) - return RE_HTML_TAG.sub(' ', html) - - def has_item(self, item): - return len(list(filter(lambda i: i.item == item, self.collectionitem_list))) > 0 - - def append_item(self, item, comment=""): - cl = self.collectionitem_list - if item is None or self.has_item(item): - return None - else: - i = CollectionItem(collection=self, position=cl[-1].position + 1 if len(cl) else 1, comment=comment) - i.set_item(item) - i.save() - return i - - @property - def item(self): - return self - - @property - def mark_class(self): - return CollectionMark - - @property - def url(self): - return reverse("collection:retrieve", args=[self.id]) - - @property - def wish_url(self): - return reverse("collection:wish", args=[self.id]) - - def is_editable_by(self, viewer): - if viewer.is_staff or viewer.is_superuser or viewer == self.owner: - return True - elif self.collaborative == 1 and viewer.is_following(self.owner) and viewer.is_followed_by(self.owner): - return True - else: - return False - - -class CollectionItem(models.Model): - movie = models.ForeignKey(Movie, on_delete=models.CASCADE, null=True) - album = models.ForeignKey(Album, on_delete=models.CASCADE, null=True) - song = models.ForeignKey(Song, on_delete=models.CASCADE, null=True) - book = models.ForeignKey(Book, on_delete=models.CASCADE, null=True) - game = models.ForeignKey(Game, on_delete=models.CASCADE, null=True) - collection = models.ForeignKey(Collection, on_delete=models.CASCADE) - position = models.PositiveIntegerField() - comment = models.TextField(_("备注"), default='') - - @property - def item(self): - items = list(filter(lambda i: i is not None, [self.movie, self.book, self.album, self.song, self.game])) - return items[0] if len(items) > 0 else None - - # @item.setter - def set_item(self, new_item): - old_item = self.item - if old_item == new_item: - return - if old_item is not None: - self.movie = None - self.book = None - self.album = None - self.song = None - self.game = None - setattr(self, new_item.__class__.__name__.lower(), new_item) - - -class CollectionMark(UserOwnedEntity): - collection = models.ForeignKey( - Collection, on_delete=models.CASCADE, related_name='collection_marks', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'collection'], name="unique_collection_mark") - ] - - def __str__(self): - return f"CollectionMark({self.id} {self.owner} {self.collection})" - - @property - def translated_status(self): - return '关注了收藏单' diff --git a/collection/templates/add_to_list.html b/collection/templates/add_to_list.html deleted file mode 100644 index 8ce1cae8..00000000 --- a/collection/templates/add_to_list.html +++ /dev/null @@ -1,45 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - diff --git a/collection/templates/create_update.html b/collection/templates/create_update.html deleted file mode 100644 index 4f7cd5ca..00000000 --- a/collection/templates/create_update.html +++ /dev/null @@ -1,71 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {{ title }} - - - - - - -
- {% include "partial/_navbar.html" %} -
-
-
-
-
- {% csrf_token %} - {{ form }} - -
- {{ form.media }} -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - \ No newline at end of file diff --git a/collection/templates/delete.html b/collection/templates/delete.html deleted file mode 100644 index c187bc4c..00000000 --- a/collection/templates/delete.html +++ /dev/null @@ -1,117 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - - {{ site_name }} {% trans '收藏单' %} - {{ collection.title }} - - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- 确认删除收藏单「{{ collection.title }}」吗? -
- {% if collection.visibility > 0 %} - - - - {% endif %} -
-
- - {{ collection.owner.mastodon_username }} - - - {{ collection.edited_time }} - -
-
-
-
-
- {{ form.description }} -
- {{ form.media }} -
-
-
- {% csrf_token %} - -
- -
- - -
-
-
-
- -
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/collection/templates/detail.html b/collection/templates/detail.html deleted file mode 100644 index 63d2a3d9..00000000 --- a/collection/templates/detail.html +++ /dev/null @@ -1,150 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - - - {{ site_name }} {% trans '收藏单' %} - {{ collection.title }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ collection.title }} -
- {% if collection.visibility > 0 %} - - - - {% endif %} -
-
- - {{ collection.owner.mastodon_username }} - - - {{ collection.edited_time }} - -
-
- {% if request.user == collection.owner %} - {% trans '编辑' %} - {% trans '删除' %} - {% elif editable %} - 可协作整理 - {% endif %} -
-
- - -
- {{ form.description }} -
- {{ form.media }} -
-
-
-
-
-
-
-
-
- - - -
-
-
- - {{ collection.title }} - -
- {% if follower_count %} - 被 {{ follower_count }} 人关注 - {% endif %} -
-
-
- - {% if request.user != collection.owner %} -
-
-
- {% if following %} -
- {% csrf_token %} - -
- {% else %} -
- {% csrf_token %} - -
- {% endif %} -
-
-
- {% endif %} - -
-
-
-
- -
-
-
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/collection/templates/edit_item_comment.html b/collection/templates/edit_item_comment.html deleted file mode 100644 index 674985e3..00000000 --- a/collection/templates/edit_item_comment.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - - -
\ No newline at end of file diff --git a/collection/templates/entity_list.html b/collection/templates/entity_list.html deleted file mode 100644 index f4df87f5..00000000 --- a/collection/templates/entity_list.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load thumb %} -{% load i18n %} -{% load l10n %} -
    - {% for collectionitem in collection.collectionitem_list %} - {% if collectionitem.item is not None %} - {% include "partial/list_item.html" with item=collectionitem.item %} - {% endif %} - {% empty %} - {% endfor %} - {% if editable %} -
  • -
    - {% csrf_token %} - - - -
    -
  • - {% endif %} -
diff --git a/collection/templates/list.html b/collection/templates/list.html deleted file mode 100644 index 027434a5..00000000 --- a/collection/templates/list.html +++ /dev/null @@ -1,99 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ title }} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ title }} -
-
    - - {% for collection in collections %} - -
  • - - {{ collection.title }} - {{ collection.edited_time }} - {% if collection.visibility > 0 %} - - {% endif %} -
  • - {% empty %} -
    {% trans '无结果' %}
    - {% endfor %} - -
-
- -
-
- - -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/collection/templates/share_collection.html b/collection/templates/share_collection.html deleted file mode 100644 index dc8e3beb..00000000 --- a/collection/templates/share_collection.html +++ /dev/null @@ -1,56 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - diff --git a/collection/templates/show_item_comment.html b/collection/templates/show_item_comment.html deleted file mode 100644 index f9add2bd..00000000 --- a/collection/templates/show_item_comment.html +++ /dev/null @@ -1,4 +0,0 @@ -{{ collectionitem.comment }} -{% if editable %} - -{% endif %} \ No newline at end of file diff --git a/collection/tests.py b/collection/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/collection/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/collection/urls.py b/collection/urls.py deleted file mode 100644 index f3c62663..00000000 --- a/collection/urls.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.urls import path, re_path -from .views import * - - -app_name = 'collection' -urlpatterns = [ - path('mine/', list, name='list'), - path('create/', create, name='create'), - path('/', retrieve, name='retrieve'), - path('/entity_list', retrieve_entity_list, name='retrieve_entity_list'), - path('update//', update, name='update'), - path('delete//', delete, name='delete'), - path('follow//', follow, name='follow'), - path('unfollow//', unfollow, name='unfollow'), - path('/append_item/', append_item, name='append_item'), - path('/delete_item/', delete_item, name='delete_item'), - path('/move_up_item/', move_up_item, name='move_up_item'), - path('/move_down_item/', move_down_item, name='move_down_item'), - path('/update_item_comment/', update_item_comment, name='update_item_comment'), - path('/show_item_comment/', show_item_comment, name='show_item_comment'), - path('with///', list_with, name='list_with'), - path('add_to_list///', add_to_list, name='add_to_list'), - path('share//', share, name='share'), - path('follow2//', wish, name='wish'), - - # TODO: tag -] diff --git a/collection/views.py b/collection/views.py deleted file mode 100644 index 11272e65..00000000 --- a/collection/views.py +++ /dev/null @@ -1,432 +0,0 @@ -import logging -from django.shortcuts import render, get_object_or_404, redirect, reverse -from django.contrib.auth.decorators import login_required, permission_required -from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import IntegrityError, transaction -from django.db.models import Count -from django.utils import timezone -from django.core.paginator import Paginator -from mastodon import mastodon_request_included -from mastodon.models import MastodonApplication -from mastodon.api import share_collection -from common.utils import PageLinksGenerator -from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin -from common.models import SourceSiteEnum -from .models import * -from .forms import * -from django.conf import settings -import re -from users.models import User -from django.http import HttpResponseRedirect - - -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 - - -class HTTPResponseHXRedirect(HttpResponseRedirect): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self['HX-Redirect'] = self['Location'] - status_code = 200 - - -# public data -########################### -@login_required -def create(request): - if request.method == 'GET': - form = CollectionForm() - return render( - request, - 'create_update.html', - { - 'form': form, - 'title': _('添加收藏单'), - 'submit_url': reverse("collection:create"), - # 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 = CollectionForm(request.POST, request.FILES) - form.instance.owner = request.user - if form.is_valid(): - form.instance.last_editor = request.user - try: - with transaction.atomic(): - form.save() - except IntegrityError as e: - logger.error(e.__str__()) - return HttpResponseServerError("integrity error") - return redirect(reverse("collection:retrieve", args=[form.instance.id])) - else: - return render( - request, - 'create_update.html', - { - 'form': form, - 'title': _('添加收藏单'), - 'submit_url': reverse("collection:create"), - # 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(request, id): - page_title = _("修改收藏单") - collection = get_object_or_404(Collection, pk=id) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - if request.method == 'GET': - form = CollectionForm(instance=collection) - return render( - request, - 'create_update.html', - { - 'form': form, - 'is_update': True, - 'title': page_title, - 'submit_url': reverse("collection:update", args=[collection.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - elif request.method == 'POST': - form = CollectionForm(request.POST, request.FILES, instance=collection) - if form.is_valid(): - form.instance.last_editor = request.user - form.instance.edited_time = timezone.now() - try: - with transaction.atomic(): - form.save() - except IntegrityError as e: - logger.error(e.__str__()) - return HttpResponseServerError("integrity error") - else: - return render( - request, - 'create_update.html', - { - 'form': form, - 'is_update': True, - 'title': page_title, - 'submit_url': reverse("collection:update", args=[collection.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - return redirect(reverse("collection:retrieve", args=[form.instance.id])) - - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -# @login_required -def retrieve(request, id): - if request.method == 'GET': - collection = get_object_or_404(Collection, pk=id) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - form = CollectionForm(instance=collection) - - following = True if request.user.is_authenticated and CollectionMark.objects.filter(owner=request.user, collection=collection).first() is not None else False - follower_count = CollectionMark.objects.filter(collection=collection).count() - - return render( - request, - 'detail.html', - { - 'collection': collection, - 'form': form, - 'editable': request.user.is_authenticated and collection.is_editable_by(request.user), - 'follower_count': follower_count, - 'following': following, - } - ) - else: - logger.warning('non-GET method at /collections/') - return HttpResponseBadRequest() - - -@mastodon_request_included -# @login_required -def retrieve_entity_list(request, id): - collection = get_object_or_404(Collection, pk=id) - if not collection.is_visible_to(request.user): - raise PermissionDenied() - form = CollectionForm(instance=collection) - - return render( - request, - 'entity_list.html', - { - 'collection': collection, - 'form': form, - 'editable': request.user.is_authenticated and collection.is_editable_by(request.user), - } - ) - - -@login_required -def delete(request, id): - collection = get_object_or_404(Collection, pk=id) - if request.user.is_staff or request.user == collection.owner: - if request.method == 'GET': - return render( - request, - 'delete.html', - { - 'collection': collection, - 'form': CollectionForm(instance=collection) - } - ) - elif request.method == 'POST': - collection.delete() - return redirect(reverse("common:home")) - else: - raise PermissionDenied() - - -@login_required -def wish(request, id): - try: - CollectionMark.objects.create(owner=request.user, collection=Collection.objects.get(id=id)) - except Exception: - pass - return HttpResponse("✔️") - - -@login_required -def follow(request, id): - CollectionMark.objects.create(owner=request.user, collection=Collection.objects.get(id=id)) - return redirect(reverse("collection:retrieve", args=[id])) - - -@login_required -def unfollow(request, id): - CollectionMark.objects.filter(owner=request.user, collection=Collection.objects.get(id=id)).delete() - return redirect(reverse("collection:retrieve", args=[id])) - - -@login_required -def list(request, user_id=None, marked=False): - if request.method == 'GET': - user = request.user if user_id is None else User.objects.get(id=user_id) - if marked: - title = user.mastodon_username + _('关注的收藏单') - queryset = Collection.objects.filter(pk__in=CollectionMark.objects.filter(owner=user).values_list('collection', flat=True)) - else: - title = user.mastodon_username + _('创建的收藏单') - queryset = Collection.objects.filter(owner=user) - paginator = Paginator(queryset, REVIEW_PER_PAGE) - page_number = request.GET.get('page', default=1) - collections = paginator.get_page(page_number) - collections.pagination = PageLinksGenerator( - PAGE_LINK_NUMBER, page_number, paginator.num_pages) - return render( - request, - 'list.html', - { - 'collections': collections, - 'title': title, - } - ) - else: - return HttpResponseBadRequest() - - -def get_entity_by_url(url): - m = re.findall(r'^/?(movies|books|games|music/album|music/song)/(\d+)/?', url.strip().lower().replace(settings.APP_WEBSITE.lower(), '')) - if len(m) > 0: - mapping = { - 'movies': Movie, - 'books': Book, - 'games': Game, - 'music/album': Album, - 'music/song': Song, - } - cls = mapping.get(m[0][0]) - id = int(m[0][1]) - if cls is not None: - return cls.objects.get(id=id) - return None - - -@login_required -def append_item(request, id): - collection = get_object_or_404(Collection, pk=id) - if request.method == 'POST' and collection.is_editable_by(request.user): - url = request.POST.get('url') - comment = request.POST.get('comment') - item = get_entity_by_url(url) - collection.append_item(item, comment) - collection.save() - # return redirect(reverse("collection:retrieve", args=[id])) - return retrieve_entity_list(request, id) - else: - return HttpResponseBadRequest() - - -@login_required -def delete_item(request, id, item_id): - collection = get_object_or_404(Collection, pk=id) - if request.method == 'POST' and collection.is_editable_by(request.user): - # item_id = int(request.POST.get('item_id')) - item = CollectionItem.objects.get(id=item_id) - if item is not None and item.collection == collection: - item.delete() - # collection.save() - # return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id])) - return retrieve_entity_list(request, id) - return HttpResponseBadRequest() - - -@login_required -def move_up_item(request, id, item_id): - collection = get_object_or_404(Collection, pk=id) - if request.method == 'POST' and collection.is_editable_by(request.user): - # item_id = int(request.POST.get('item_id')) - item = CollectionItem.objects.get(id=item_id) - if item is not None and item.collection == collection: - items = collection.collectionitem_list - idx = items.index(item) - if idx > 0: - o = items[idx - 1] - p = o.position - o.position = item.position - item.position = p - o.save() - item.save() - # collection.save() - # return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id])) - return retrieve_entity_list(request, id) - return HttpResponseBadRequest() - - -@login_required -def move_down_item(request, id, item_id): - collection = get_object_or_404(Collection, pk=id) - if request.method == 'POST' and collection.is_editable_by(request.user): - # item_id = int(request.POST.get('item_id')) - item = CollectionItem.objects.get(id=item_id) - if item is not None and item.collection == collection: - items = collection.collectionitem_list - idx = items.index(item) - if idx + 1 < len(items): - o = items[idx + 1] - p = o.position - o.position = item.position - item.position = p - o.save() - item.save() - # collection.save() - # return HTTPResponseHXRedirect(redirect_to=reverse("collection:retrieve", args=[id])) - return retrieve_entity_list(request, id) - return HttpResponseBadRequest() - - -def show_item_comment(request, id, item_id): - collection = get_object_or_404(Collection, pk=id) - item = CollectionItem.objects.get(id=item_id) - editable = request.user.is_authenticated and collection.is_editable_by(request.user) - return render(request, 'show_item_comment.html', {'collection': collection, 'collectionitem': item, 'editable': editable}) - - -@login_required -def update_item_comment(request, id, item_id): - collection = get_object_or_404(Collection, pk=id) - if collection.is_editable_by(request.user): - # item_id = int(request.POST.get('item_id')) - item = CollectionItem.objects.get(id=item_id) - if item is not None and item.collection == collection: - if request.method == 'POST': - item.comment = request.POST.get('comment', default='') - item.save() - return render(request, 'show_item_comment.html', {'collection': collection, 'collectionitem': item, 'editable': True}) - else: - return render(request, 'edit_item_comment.html', {'collection': collection, 'collectionitem': item}) - return retrieve_entity_list(request, id) - return HttpResponseBadRequest() - - -@login_required -def list_with(request, type, id): - pass - - -def get_entity_by_type_id(type, id): - mapping = { - 'movie': Movie, - 'book': Book, - 'game': Game, - 'album': Album, - 'song': Song, - } - cls = mapping.get(type) - if cls is not None: - return cls.objects.get(id=id) - return None - - -@login_required -def add_to_list(request, type, id): - item = get_entity_by_type_id(type, id) - if request.method == 'GET': - queryset = Collection.objects.filter(owner=request.user) - return render( - request, - 'add_to_list.html', - { - 'type': type, - 'id': id, - 'item': item, - 'collections': queryset, - } - ) - else: - cid = int(request.POST.get('collection_id', default=0)) - if not cid: - cid = Collection.objects.create(owner=request.user, title=f'{request.user.username}的收藏单').id - collection = Collection.objects.filter(owner=request.user, id=cid).first() - collection.append_item(item, request.POST.get('comment')) - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) - - -@login_required -def share(request, id): - collection = Collection.objects.filter(id=id).first() - if not collection: - return HttpResponseBadRequest() - if request.method == 'GET': - return render(request, 'share_collection.html', {'id': id, 'visibility': request.user.get_preference().default_visibility}) - else: - visibility = int(request.POST.get('visibility', default=0)) - comment = request.POST.get('comment') - if share_collection(collection, comment, request.user, visibility): - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) - else: - return go_relogin(request) diff --git a/games/__init__.py b/games/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/games/admin.py b/games/admin.py deleted file mode 100644 index 36cc44fa..00000000 --- a/games/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin -from .models import * -from simple_history.admin import SimpleHistoryAdmin - -admin.site.register(Game, SimpleHistoryAdmin) -admin.site.register(GameMark) -admin.site.register(GameReview) -admin.site.register(GameTag) diff --git a/games/apps.py b/games/apps.py deleted file mode 100644 index a204c094..00000000 --- a/games/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig - - -class GamesConfig(AppConfig): - name = 'games' - - def ready(self): - from common.index import Indexer - from .models import Game - Indexer.update_model_indexable(Game) diff --git a/games/forms.py b/games/forms.py deleted file mode 100644 index e758f9fd..00000000 --- a/games/forms.py +++ /dev/null @@ -1,85 +0,0 @@ -from django import forms -from django.contrib.postgres.forms import SimpleArrayField -from django.utils.translation import gettext_lazy as _ -from .models import Game, GameMark, GameReview, GameMarkStatusTranslation -from common.models import MarkStatusEnum -from common.forms import * - - -def GameMarkStatusTranslator(status): - return GameMarkStatusTranslation[status] - - -class GameForm(forms.ModelForm): - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - - other_info = JSONField(required=False, label=_("其他信息")) - - class Meta: - model = Game - fields = [ - 'id', - 'title', - 'source_site', - 'source_url', - 'other_title', - 'developer', - 'publisher', - 'release_date', - 'genre', - 'platform', - 'cover', - 'brief', - 'other_info' - ] - - widgets = { - 'other_title': forms.TextInput(attrs={'placeholder': _("多个别名使用英文逗号分隔")}), - 'developer': forms.TextInput(attrs={'placeholder': _("多个开发商使用英文逗号分隔")}), - 'publisher': forms.TextInput(attrs={'placeholder': _("多个发行商使用英文逗号分隔")}), - 'genre': forms.TextInput(attrs={'placeholder': _("多个类型使用英文逗号分隔")}), - 'platform': forms.TextInput(attrs={'placeholder': _("多个平台使用英文逗号分隔")}), - 'cover': PreviewImageInput(), - } - - -class GameMarkForm(MarkForm): - - STATUS_CHOICES = [(v, GameMarkStatusTranslator(v)) - for v in MarkStatusEnum.values] - - status = forms.ChoiceField( - label=_(""), - widget=forms.RadioSelect(), - choices=STATUS_CHOICES - ) - - class Meta: - model = GameMark - fields = [ - 'id', - 'game', - 'status', - 'rating', - 'text', - 'visibility', - ] - widgets = { - 'game': forms.TextInput(attrs={"hidden": ""}), - } - - -class GameReviewForm(ReviewForm): - - class Meta: - model = GameReview - fields = [ - 'id', - 'game', - 'title', - 'content', - 'visibility' - ] - widgets = { - 'game': forms.TextInput(attrs={"hidden": ""}), - } diff --git a/games/models.py b/games/models.py deleted file mode 100644 index 64b620d5..00000000 --- a/games/models.py +++ /dev/null @@ -1,173 +0,0 @@ -import uuid -import django.contrib.postgres.fields as postgres -from django.utils.translation import gettext_lazy as _ -from django.db import models -from django.core.serializers.json import DjangoJSONEncoder -from django.shortcuts import reverse -from common.models import Entity, Mark, Review, Tag, MarkStatusEnum -from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath -from django.utils import timezone -from django.conf import settings -from simple_history.models import HistoricalRecords - - -GameMarkStatusTranslation = { - MarkStatusEnum.DO.value: _("在玩"), - MarkStatusEnum.WISH.value: _("想玩"), - MarkStatusEnum.COLLECT.value: _("玩过") -} - - -def game_cover_path(instance, filename): - return GenerateDateUUIDMediaFilePath(instance, filename, settings.GAME_MEDIA_PATH_ROOT) - - -class Game(Entity): - """ - """ - - title = models.CharField(_("名称"), max_length=500) - - other_title = postgres.ArrayField( - models.CharField(blank=True,default='', max_length=500), - null=True, - blank=True, - default=list, - verbose_name=_("别名") - ) - - developer = postgres.ArrayField( - models.CharField(blank=True, default='', max_length=500), - null=True, - blank=True, - default=list, - verbose_name=_("开发商") - ) - - publisher = postgres.ArrayField( - models.CharField(blank=True, default='', max_length=500), - null=True, - blank=True, - default=list, - verbose_name=_("发行商") - ) - - release_date = models.DateField( - _('发行日期'), - auto_now=False, - auto_now_add=False, - null=True, - blank=True - ) - - genre = postgres.ArrayField( - models.CharField(blank=True, default='', max_length=200), - null=True, - blank=True, - default=list, - verbose_name=_("类型") - ) - - platform = postgres.ArrayField( - models.CharField(blank=True, default='', max_length=200), - null=True, - blank=True, - default=list, - verbose_name=_("平台") - ) - - cover = models.ImageField(_("封面"), upload_to=game_cover_path, default=settings.DEFAULT_GAME_IMAGE, blank=True) - - history = HistoricalRecords() - - def __str__(self): - return self.title - - def get_json(self): - r = { - 'developer': self.developer, - 'other_title': self.other_title, - 'publisher': self.publisher, - 'release_date': self.release_date, - 'platform': self.platform, - 'genre': self.genre, - } - r.update(super().get_json()) - return r - - def get_absolute_url(self): - return reverse("games:retrieve", args=[self.id]) - - @property - def year(self): - return self.release_date.year if self.release_date else None - - @property - def wish_url(self): - return reverse("games:wish", args=[self.id]) - - def get_tags_manager(self): - return self.game_tags - - @property - def verbose_category_name(self): - return _("游戏") - - @property - def mark_class(self): - return GameMark - - @property - def tag_class(self): - return GameTag - - -class GameMark(Mark): - game = models.ForeignKey( - Game, on_delete=models.CASCADE, related_name='game_marks', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'game'], name='unique_game_mark') - ] - - @property - def translated_status(self): - return GameMarkStatusTranslation[self.status] - - -class GameReview(Review): - game = models.ForeignKey( - Game, on_delete=models.CASCADE, related_name='game_reviews', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'game'], name='unique_game_review') - ] - - @property - def url(self): - return reverse("games:retrieve_review", args=[self.id]) - - @property - def item(self): - return self.game - - -class GameTag(Tag): - game = models.ForeignKey( - Game, on_delete=models.CASCADE, related_name='game_tags', null=True) - mark = models.ForeignKey( - GameMark, on_delete=models.CASCADE, related_name='gamemark_tags', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['content', 'mark'], name="unique_gamemark_tag") - ] - - @property - def item(self): - return self.game diff --git a/games/templates/games/create_update.html b/games/templates/games/create_update.html deleted file mode 100644 index 7365d687..00000000 --- a/games/templates/games/create_update.html +++ /dev/null @@ -1,104 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {{ title }} - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
- {% if is_update and form.source_site.value != 'in-site' %} -
-
-
-
{% trans '源网站' %}: {{ form.source_site.value }}
-
-
- {% csrf_token %} - -
-
-
-
-
- {% endif %} - -
- {% comment %} {% trans '>>> 试试一键剽取~ <<<' %} {% endcomment %} -
- {% csrf_token %} - {{ form.media }} - {% for field in form %} - - {% if field.name == 'release_date' %} - {{ field.label_tag }} - - - - {% else %} - {% if field.name != 'id' %} - {{ field.label_tag }} - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - - -
-
- -
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/games/templates/games/create_update_review.html b/games/templates/games/create_update_review.html deleted file mode 100644 index 93b9bd55..00000000 --- a/games/templates/games/create_update_review.html +++ /dev/null @@ -1,124 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ title }} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - -
-
- -
- {{ game.title }} - - {{ game.get_source_site_display }} -
- -
- {% if game.genre %}{% trans '类型:' %} - {% for genre in game.genre %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
- {% if game.developer %}{% trans '开发商:' %} - {% for developer in game.developer %} - {{ developer }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
{% if game.release_date %} - {% trans '发行日期:' %}{{ game.release_date }} - {% endif %} -
- - {% if game.rating %} - {% trans '评分:' %} - {{ game.rating }} - {% endif %} -
-
-
- -
- {% csrf_token %} - {{ form.game }} -
- {{ form.title.label }} -
- {{ form.title }} -
- - {{ form.content.label }} - - - {% trans '预览' %} - -
-
- {{ form.content }} -
-
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
-
-
- - {{ form.visibility.label }}{{ form.visibility }} -
- -
-
- -
- {{ form.media }} -
- -
- -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - diff --git a/games/templates/games/delete.html b/games/templates/games/delete.html deleted file mode 100644 index f7cd512a..00000000 --- a/games/templates/games/delete.html +++ /dev/null @@ -1,99 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {% trans '删除电影/剧集' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这个游戏吗?相关评论和标记将一并删除。' %}
- -
-
- - - -
-
- -
- {{ game.title }} - {{ game.get_source_site_display }} -
- - {% if game.rating %} - {% trans '评分:' %} - - {{ game.rating }} - {% else %} - {% trans '评分:暂无评分' %} - {% endif %} - - {% if game.last_editor %} -
- {% trans '最近编辑者:' %} - - {{ game.last_editor | default:"" }} - -
- {% endif %} - -
{% trans '上次编辑时间:' %}{{ game.edited_time }}
- - {% if game.game_marks.all %} -
{% trans '这个条目有' %} {{ game.game_marks.count }} 个标记
- {% endif %} - {% if game.game_reviews.all %} -
{% trans '这个条目有' %} {{ game.game_reviews.count }} 个评论
- {% endif %} - -
-
-
-
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/games/templates/games/delete_review.html b/games/templates/games/delete_review.html deleted file mode 100644 index 948850b8..00000000 --- a/games/templates/games/delete_review.html +++ /dev/null @@ -1,101 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '删除评论' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这篇评论吗?' %}
- -
- - -
- -
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} - -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- - -
-
- {{ form.content }} -
- {{ form.media }} - -
- -
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/games/templates/games/detail.html b/games/templates/games/detail.html deleted file mode 100644 index 60ed0f85..00000000 --- a/games/templates/games/detail.html +++ /dev/null @@ -1,421 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load strip_scheme %} -{% load thumb %} - - - - - - - - - - - - - - {{ site_name }} - {% trans '游戏详情' %} | {{ game.title }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - {{ game.title }} - - -
-
- {{ game.title }} - {{ game.get_source_site_display }} -
- -
-
- {% if game.rating and game.rating_number >= 5 %} - - {{ game.rating }} - ({{ game.rating_number }}人评分) - {% else %} - {% trans '评分:评分人数不足' %} - {% endif %} -
- -
{% if game.other_title %}{% trans '别名:' %} - {% for other_title in game.other_title %} - 5 %}style="display: none;" {% endif %}> - {{ other_title }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if game.other_title|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
- -
- {% if game.genre %}{% trans '类型:' %} - {% for genre in game.genre %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
- {% if game.developer %}{% trans '开发商:' %} - {% for developer in game.developer %} - {{ developer }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
- {% if game.publisher %}{% trans '发行商:' %} - {% for publisher in game.publisher %} - {{ publisher }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
{% if game.release_date %} - {% trans '发行日期:' %}{{ game.release_date }} - {% endif %} -
- - {% if game.other_info %} - {% for k, v in game.other_info.items %} -
- {{ k }}:{{ v | urlize }} -
- {% endfor %} - {% endif %} - -
-
- -
- {% if game.platform %}{% trans '平台:' %} - {% for platform in game.platform %} - {{ platform }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- - - - {% if game.last_editor and game.last_editor.preference.show_last_edit or user.is_staff %} -
{% trans '最近编辑者:' %}{{ game.last_editor | default:"" }}
- {% endif %} - -
- {% trans '编辑这个游戏' %} - - {% if user.is_staff %} - / {% trans '删除' %} - {% endif %} -
-
- -
- - {% for tag_dict in game_tag_list %} - {% for k, v in tag_dict.items %} - {% if k == 'content' %} - - {{ v }} - - {% endif %} - {% endfor %} - {% endfor %} - -
-
-
-
- {% if game.brief %} -
-
{% trans '简介' %}
- -

{{ game.brief | linebreaksbr }}

- - -
- {% endif %} - - -
- -
{% trans '这个游戏的标记' %}
- {% if mark_list_more %} - {% trans '全部标记' %} - {% endif %} - 关注的人的标记 - {% include "partial/mark_list.html" with mark_list=mark_list current_item=game %} -
-
-
{% trans '这个游戏的评论' %}
- - {% if review_list_more %} - {% trans '全部评论' %} - {% endif %} - {% if review_list %} - - {% else %} -
{% trans '暂无评论' %}
- {% endif %} -
-
-
- -
-
- - {% if mark %} -
- - {% trans '我' %}{{ mark.get_status_display }} - {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} - {% if mark.rating %} - - {% endif %} - {% endif %} - {% if mark.visibility > 0 %} - - {% endif %} - - {% trans '修改' %} -
- {% csrf_token %} - {% trans '删除' %} -
-
-
- -
{{ mark.created_time }}
- - {% if mark.text %} -

{{ mark.text }}

- {% endif %} -
- - {% for tag in mark_tags %} - {{ tag }} - {% endfor %} - -
-
- {% else %} -
- -
{% trans '标记这个游戏' %}
-
- - - -
-
- {% endif %} - -
- -
- {% if review %} -
- - {% trans '我的评论' %} - {% if review.visibility > 0 %} - - {% endif %} - - - {% trans '编辑' %} - {% trans '删除' %} - - -
{{ review.edited_time }}
- - - {{ review.title }} - -
- {% else %} - -
-
{% trans '我的评论' %}
- -
- - {% endif %} -
- - {% if collection_list %} -
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %} -

- {{ c.title }} -

- {% endfor %} -
- -
-
-
-
- {% endif %} - -
-
-
- -
- {% include "partial/_footer.html" %} -
- -
- - - -
-
- - - - - - diff --git a/games/templates/games/mark_list.html b/games/templates/games/mark_list.html deleted file mode 100644 index d80e6409..00000000 --- a/games/templates/games/mark_list.html +++ /dev/null @@ -1,129 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ game.title }}{% trans '的标记' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ game.title }}{% trans ' 的标记' %} -
- {% include "partial/mark_list.html" with mark_list=marks current_item=game %} -
- -
-
- -
-
-
-
- -
-
-
- {{ game.title }} - - {{ game.get_source_site_display }} -
- -
- {% if game.genre %}{% trans '类型:' %} - {% for genre in game.genre %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
- {% if game.developer %}{% trans '开发商:' %} - {% for developer in game.developer %} - {{ developer }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
{% if game.release_date %} - {% trans '发行日期:' %}{{ game.release_date }} - {% endif %} -
- - {% if game.rating %} - {% trans '评分:' %} - {{ game.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/games/templates/games/review_detail.html b/games/templates/games/review_detail.html deleted file mode 100644 index fcadc6d4..00000000 --- a/games/templates/games/review_detail.html +++ /dev/null @@ -1,146 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - {{ site_name }}游戏评论 - {{ review.title }} - - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- {% if request.user == review.owner %} - {% trans '编辑' %} - {% trans '删除' %} - {% endif %} -
-
- -
- {{ form.content }} -
- {{ form.media }} - {% csrf_token %} -
-
- -
-
-
-
-
- -
-
-
- - {{ game.title }} - - - - {{ game.get_source_site_display }} - - -
- -
- {% if game.genre %}{% trans '类型:' %} - {% for genre in game.genre %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
- {% if game.developer %}{% trans '开发商:' %} - {% for developer in game.developer %} - {{ developer }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
{% if game.release_date %} - {% trans '发行日期:' %}{{ game.release_date }} - {% endif %} -
- - {% if game.rating %} - {% trans '评分:' %} - {{ game.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/games/templates/games/review_list.html b/games/templates/games/review_list.html deleted file mode 100644 index 6b84ae7a..00000000 --- a/games/templates/games/review_list.html +++ /dev/null @@ -1,147 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ game.title }}{% trans '的评论' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ game.title }}{% trans ' 的评论' %} -
-
    - - {% for review in reviews %} - -
  • - - {{ review.owner.username }} - {% if review.visibility > 0 %} - - {% endif %} - {{ review.edited_time }} - - - {{ review.title }} - -
  • - {% empty %} -
    {% trans '无结果' %}
    - {% endfor %} - -
-
- -
-
- -
-
-
-
- -
-
-
- {{ game.title }} - - {{ game.get_source_site_display }} -
- -
- {% if game.genre %}{% trans '类型:' %} - {% for genre in game.genre %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
- {% if game.developer %}{% trans '开发商:' %} - {% for developer in game.developer %} - {{ developer }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %} -
- -
{% if game.release_date %} - {% trans '发行日期:' %}{{ game.release_date }} - {% endif %} -
- - {% if game.rating %} - {% trans '评分:' %} - {{ game.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/games/templates/games/scrape.html b/games/templates/games/scrape.html deleted file mode 100644 index 0a3370da..00000000 --- a/games/templates/games/scrape.html +++ /dev/null @@ -1,112 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '从豆瓣获取数据' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {% trans '根据豆瓣内容填写下方表单' %} -
- -
-
-
- {% csrf_token %} - - {{ form.media }} - - {% for field in form %} - - {% if field.name == 'release_date' %} - {{ field.label_tag }} - - - - {% else %} - {% if field.name != 'id' %} - {{ field.label_tag }} - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - -
- {% trans '剽取!' %} -
-
-
-
- -
-
-
- {% trans '复制详情页链接' %} -
-
- {% csrf_token %} - - -
-
- -
-
-
- -
- {% include "partial/_footer.html" %} - -
- - - - - diff --git a/games/tests.py b/games/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/games/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/games/urls.py b/games/urls.py deleted file mode 100644 index 88ce1629..00000000 --- a/games/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.urls import path, re_path -from .views import * - - -app_name = 'games' -urlpatterns = [ - path('create/', create, name='create'), - path('/', retrieve, name='retrieve'), - path('update//', update, name='update'), - path('delete//', delete, name='delete'), - path('rescrape//', rescrape, name='rescrape'), - path('mark/', create_update_mark, name='create_update_mark'), - path('wish//', wish, name='wish'), - re_path('(?P[0-9]+)/mark/list/(?:(?P\\d+))?', retrieve_mark_list, name='retrieve_mark_list'), - path('mark/delete//', delete_mark, name='delete_mark'), - path('/review/create/', create_review, name='create_review'), - path('review/update//', update_review, name='update_review'), - path('review/delete//', delete_review, name='delete_review'), - path('review//', retrieve_review, name='retrieve_review'), - path('/review/list/', - retrieve_review_list, name='retrieve_review_list'), - path('scrape/', scrape, name='scrape'), - path('click_to_scrape/', click_to_scrape, name='click_to_scrape'), -] diff --git a/games/views.py b/games/views.py deleted file mode 100644 index 00e8c489..00000000 --- a/games/views.py +++ /dev/null @@ -1,585 +0,0 @@ -import logging -from django.shortcuts import render, get_object_or_404, redirect, reverse -from django.contrib.auth.decorators import login_required, permission_required -from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import IntegrityError, transaction -from django.db.models import Count -from django.utils import timezone -from django.core.paginator import Paginator -from mastodon import mastodon_request_included -from mastodon.models import MastodonApplication -from mastodon.api import share_mark, share_review -from common.utils import PageLinksGenerator -from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin -from common.models import SourceSiteEnum -from .models import * -from .forms import * -from django.conf import settings -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(request): - if request.method == 'GET': - form = GameForm() - return render( - request, - 'games/create_update.html', - { - 'form': form, - 'title': _('添加游戏'), - 'submit_url': reverse("games:create"), - # 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 = GameForm(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("games:retrieve", args=[form.instance.id])) - else: - return render( - request, - 'games/create_update.html', - { - 'form': form, - 'title': _('添加游戏'), - 'submit_url': reverse("games:create"), - # 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(Game, 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("games:retrieve", args=[form.instance.id])) - - -@login_required -def update(request, id): - if request.method == 'GET': - game = get_object_or_404(Game, pk=id) - form = GameForm(instance=game) - page_title = _('修改游戏') - return render( - request, - 'games/create_update.html', - { - 'form': form, - 'is_update': True, - 'title': page_title, - 'submit_url': reverse("games:update", args=[game.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - elif request.method == 'POST': - game = get_object_or_404(Game, pk=id) - form = GameForm(request.POST, request.FILES, instance=game) - 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, - 'games/create_update.html', - { - 'form': form, - 'is_update': True, - 'title': page_title, - 'submit_url': reverse("games:update", args=[game.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - return redirect(reverse("games:retrieve", args=[form.instance.id])) - - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -# @login_required -def retrieve(request, id): - if request.method == 'GET': - game = get_object_or_404(Game, pk=id) - mark = None - mark_tags = None - review = None - - # retreive tags - game_tag_list = game.game_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 = GameMark.objects.get(owner=request.user, game=game) - except ObjectDoesNotExist: - mark = None - if mark: - mark_tags = mark.gamemark_tags.all() - mark.get_status_display = GameMarkStatusTranslator(mark.status) - mark_form = GameMarkForm(instance=mark, initial={ - 'tags': mark_tags - }) - else: - mark_form = GameMarkForm(initial={ - 'game': game, - '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 = GameReview.objects.get( - owner=request.user, game=game) - 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 = GameMark.get_available(game, request.user) - review_list = GameReview.get_available(game, 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 = GameMarkStatusTranslator(m.status) - review_list_more = True if len( - review_list) > REVIEW_NUMBER else False - review_list = review_list[:REVIEW_NUMBER] - all_collections = CollectionItem.objects.filter(game=game).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20] - collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections)) - - # 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, - 'games/detail.html', - { - 'game': game, - '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, - 'game_tag_list': game_tag_list, - 'mark_tags': mark_tags, - 'collection_list': collection_list, - } - ) - else: - logger.warning('non-GET method at /games/') - return HttpResponseBadRequest() - - -@permission_required("games.delete_game") -@login_required -def delete(request, id): - if request.method == 'GET': - game = get_object_or_404(Game, pk=id) - return render( - request, - 'games/delete.html', - { - 'game': game, - } - ) - elif request.method == 'POST': - if request.user.is_staff: - # only staff has right to delete - game = get_object_or_404(Game, pk=id) - game.delete() - return redirect(reverse("common:home")) - else: - raise PermissionDenied() - else: - return HttpResponseBadRequest() - - -# user owned entites -########################### -@mastodon_request_included -@login_required -def create_update_mark(request): - # check list: - # clean rating if is wish - # transaction on updating game rating - # owner check(guarantee) - if request.method == 'POST': - pk = request.POST.get('id') - old_rating = None - old_tags = None - if not pk: - game_id = request.POST.get('game') - mark = GameMark.objects.filter(game_id=game_id, owner=request.user).first() - if mark: - pk = mark.id - if pk: - mark = get_object_or_404(GameMark, pk=pk) - if request.user != mark.owner: - return HttpResponseBadRequest() - old_rating = mark.rating - old_tags = mark.gamemark_tags.all() - if mark.status != request.POST.get('status'): - mark.created_time = timezone.now() - # update - form = GameMarkForm(request.POST, instance=mark) - else: - # create - form = GameMarkForm(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() - game = form.instance.game - - try: - with transaction.atomic(): - # update game rating - game.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']: - GameTag.objects.create( - content=tag, - game=game, - 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("games:retrieve", args=[form.instance.game.id])) - else: - return HttpResponseBadRequest("invalid method") - - -@mastodon_request_included -@login_required -def wish(request, id): - if request.method == 'POST': - game = get_object_or_404(Game, pk=id) - params = { - 'owner': request.user, - 'status': MarkStatusEnum.WISH, - 'visibility': request.user.preference.default_visibility, - 'game': game, - } - try: - GameMark.objects.create(**params) - except Exception: - pass - return HttpResponse("✔️") - else: - return HttpResponseBadRequest("invalid method") - - -@mastodon_request_included -@login_required -def retrieve_mark_list(request, game_id, following_only=False): - if request.method == 'GET': - game = get_object_or_404(Game, pk=game_id) - queryset = GameMark.get_available(game, 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 = GameMarkStatusTranslator(m.status) - return render( - request, - 'games/mark_list.html', - { - 'marks': marks, - 'game': game, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def delete_mark(request, id): - if request.method == 'POST': - mark = get_object_or_404(GameMark, pk=id) - if request.user != mark.owner: - return HttpResponseBadRequest() - game_id = mark.game.id - try: - with transaction.atomic(): - # update game rating - mark.game.update_rating(mark.rating, None) - mark.delete() - except IntegrityError as e: - return HttpResponseServerError() - return redirect(reverse("games:retrieve", args=[game_id])) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def create_review(request, game_id): - if request.method == 'GET': - form = GameReviewForm(initial={'game': game_id}) - game = get_object_or_404(Game, pk=game_id) - return render( - request, - 'games/create_update_review.html', - { - 'form': form, - 'title': _("添加评论"), - 'game': game, - 'submit_url': reverse("games:create_review", args=[game_id]), - } - ) - elif request.method == 'POST': - form = GameReviewForm(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("games:retrieve_review", args=[form.instance.id])) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def update_review(request, id): - if request.method == 'GET': - review = get_object_or_404(GameReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - form = GameReviewForm(instance=review) - game = review.game - return render( - request, - 'games/create_update_review.html', - { - 'form': form, - 'title': _("编辑评论"), - 'game': game, - 'submit_url': reverse("games:update_review", args=[review.id]), - } - ) - elif request.method == 'POST': - review = get_object_or_404(GameReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - form = GameReviewForm(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("games:retrieve_review", args=[form.instance.id])) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() - - -@login_required -def delete_review(request, id): - if request.method == 'GET': - review = get_object_or_404(GameReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - review_form = GameReviewForm(instance=review) - return render( - request, - 'games/delete_review.html', - { - 'form': review_form, - 'review': review, - } - ) - elif request.method == 'POST': - review = get_object_or_404(GameReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - game_id = review.game.id - review.delete() - return redirect(reverse("games:retrieve", args=[game_id])) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -def retrieve_review(request, id): - if request.method == 'GET': - review = get_object_or_404(GameReview, pk=id) - if not review.is_visible_to(request.user): - msg = _("你没有访问这个页面的权限😥") - return render( - request, - 'common/error.html', - { - 'msg': msg, - } - ) - review_form = GameReviewForm(instance=review) - game = review.game - try: - mark = GameMark.objects.get(owner=review.owner, game=game) - mark.get_status_display = GameMarkStatusTranslator(mark.status) - except ObjectDoesNotExist: - mark = None - return render( - request, - 'games/review_detail.html', - { - 'form': review_form, - 'review': review, - 'game': game, - 'mark': mark, - } - ) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def retrieve_review_list(request, game_id): - if request.method == 'GET': - game = get_object_or_404(Game, pk=game_id) - queryset = GameReview.get_available(game, 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, - 'games/review_list.html', - { - 'reviews': reviews, - 'game': game, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def scrape(request): - if request.method == 'GET': - keywords = request.GET.get('q') - form = GameForm() - return render( - request, - 'games/scrape.html', - { - 'q': keywords, - 'form': form, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def click_to_scrape(request): - if request.method == "POST": - url = request.POST.get("url") - if url: - return jump_or_scrape(request, url) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() diff --git a/movies/__init__.py b/movies/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/movies/admin.py b/movies/admin.py deleted file mode 100644 index bb2b1a54..00000000 --- a/movies/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin -from .models import * -from simple_history.admin import SimpleHistoryAdmin - -admin.site.register(Movie, SimpleHistoryAdmin) -admin.site.register(MovieMark) -admin.site.register(MovieReview) -admin.site.register(MovieTag) diff --git a/movies/apps.py b/movies/apps.py deleted file mode 100644 index 3b025da4..00000000 --- a/movies/apps.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.apps import AppConfig - - -class MoviesConfig(AppConfig): - name = 'movies' - - def ready(self): - from common.index import Indexer - from .models import Movie - Indexer.update_model_indexable(Movie) diff --git a/movies/forms.py b/movies/forms.py deleted file mode 100644 index 587d92e9..00000000 --- a/movies/forms.py +++ /dev/null @@ -1,137 +0,0 @@ -from django import forms -from django.contrib.postgres.forms import SimpleArrayField -from django.utils.translation import gettext_lazy as _ -from .models import Movie, MovieMark, MovieReview, MovieGenreEnum, MovieMarkStatusTranslation -from common.models import MarkStatusEnum -from common.forms import * - - -def MovieMarkStatusTranslator(status): - return MovieMarkStatusTranslation[status] - - -class MovieForm(forms.ModelForm): - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - genre = forms.MultipleChoiceField( - required=False, - choices=MovieGenreEnum.choices, - widget= MultiSelect, - label=_("类型") - ) - showtime = HstoreField( - required=False, - label=_("上映时间"), - widget=HstoreInput( - attrs={ - 'placeholder-key': _("日期"), - 'placeholder-value': _("地区"), - } - ) - ) - other_info = JSONField(required=False, label=_("其他信息")) - - class Meta: - model = Movie - fields = [ - 'id', - 'title', - 'source_site', - 'source_url', - 'orig_title', - 'other_title', - 'imdb_code', - 'director', - 'playwright', - 'actor', - 'genre', - 'showtime', - 'site', - 'area', - 'language', - 'year', - 'duration', - 'season', - 'episodes', - 'single_episode_length', - 'cover', - 'is_series', - 'brief', - 'other_info', - ] - labels = { - 'title': _("标题"), - 'orig_title': _("原名"), - 'other_title': _("又名"), - 'imdb_code': _("IMDb编号"), - 'director': _("导演"), - 'playwright': _("编剧"), - 'actor': _("主演"), - 'genre': _("类型"), - 'showtime': _("上映时间"), - 'site': _("官方网站"), - 'area': _("国家/地区"), - 'language': _("语言"), - 'year': _("年份"), - 'duration': _("片长"), - 'season': _("季数"), - 'episodes': _("集数"), - 'single_episode_length': _("单集片长"), - 'cover': _("封面"), - 'brief': _("简介"), - 'other_info': _("其他信息"), - 'is_series': _("是否为剧集"), - } - - widgets = { - 'other_title': forms.TextInput(attrs={'placeholder': _("多个别名使用英文逗号分隔")}), - 'director': forms.TextInput(attrs={'placeholder': _("多个导演使用英文逗号分隔")}), - 'actor': forms.TextInput(attrs={'placeholder': _("多个主演使用英文逗号分隔")}), - 'playwright': forms.TextInput(attrs={'placeholder': _("多个编剧使用英文逗号分隔")}), - 'area': forms.TextInput(attrs={'placeholder': _("多个国家/地区使用英文逗号分隔")}), - 'language': forms.TextInput(attrs={'placeholder': _("多种语言使用英文逗号分隔")}), - 'cover': PreviewImageInput(), - 'is_series': forms.CheckboxInput(attrs={'style': 'width: auto; position: relative; top: 2px'}) - } - - -class MovieMarkForm(MarkForm): - - STATUS_CHOICES = [(v, MovieMarkStatusTranslator(v)) - for v in MarkStatusEnum.values] - - status = forms.ChoiceField( - label=_(""), - widget=forms.RadioSelect(), - choices=STATUS_CHOICES - ) - - - class Meta: - model = MovieMark - fields = [ - 'id', - 'movie', - 'status', - 'rating', - 'text', - 'visibility', - ] - widgets = { - 'movie': forms.TextInput(attrs={"hidden": ""}), - } - - -class MovieReviewForm(ReviewForm): - - class Meta: - model = MovieReview - fields = [ - 'id', - 'movie', - 'title', - 'content', - 'visibility' - ] - widgets = { - 'movie': forms.TextInput(attrs={"hidden": ""}), - } diff --git a/movies/management/commands/fix-movie-poster.py b/movies/management/commands/fix-movie-poster.py deleted file mode 100644 index b8a35f85..00000000 --- a/movies/management/commands/fix-movie-poster.py +++ /dev/null @@ -1,203 +0,0 @@ -from django.core.management.base import BaseCommand -from django.core.files.uploadedfile import SimpleUploadedFile -from common.scraper import * -from django.conf import settings -from movies.models import Movie -from movies.forms import MovieForm -import requests -import re -import filetype -from lxml import html -from PIL import Image -from io import BytesIO - - -class DoubanPatcherMixin: - @classmethod - def download_page(cls, url, headers): - url = cls.get_effective_url(url) - r = None - error = 'DoubanScrapper: error occured when downloading ' + url - content = None - - def get(url, timeout): - nonlocal r - # print('Douban GET ' + url) - try: - r = requests.get(url, timeout=timeout) - except Exception as e: - r = requests.Response() - r.status_code = f"Exception when GET {url} {e}" + url - # print('Douban CODE ' + str(r.status_code)) - return r - - def check_content(): - nonlocal r, error, content - content = None - if r.status_code == 200: - content = r.content.decode('utf-8') - if content.find('关于豆瓣') == -1: - if content.find('你的 IP 发出') == -1: - error = error + 'Content not authentic' # response is garbage - else: - error = error + 'IP banned' - content = None - elif re.search('不存在[^<]+', content, re.MULTILINE): - content = None - error = error + 'Not found or hidden by Douban' - else: - error = error + str(r.status_code) - - def fix_wayback_links(): - nonlocal content - # fix links - content = re.sub(r'href="http[^"]+http', r'href="http', content) - # https://img9.doubanio.com/view/subject/{l|m|s}/public/s1234.jpg - content = re.sub(r'src="[^"]+/(s\d+\.\w+)"', - r'src="https://img9.doubanio.com/view/subject/m/public/\1"', content) - # https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2681329386.jpg - # https://img9.doubanio.com/view/photo/{l|m|s}/public/p1234.webp - content = re.sub(r'src="[^"]+/(p\d+\.\w+)"', - r'src="https://img9.doubanio.com/view/photo/m/public/\1"', content) - - # Wayback Machine: get latest available - def wayback(): - nonlocal r, error, content - error = error + '\nWayback: ' - get('http://archive.org/wayback/available?url=' + url, 10) - if r.status_code == 200: - w = r.json() - if w['archived_snapshots'] and w['archived_snapshots']['closest']: - get(w['archived_snapshots']['closest']['url'], 10) - check_content() - if content is not None: - fix_wayback_links() - else: - error = error + 'No snapshot available' - else: - error = error + str(r.status_code) - - # Wayback Machine: guess via CDX API - def wayback_cdx(): - nonlocal r, error, content - error = error + '\nWayback: ' - get('http://web.archive.org/cdx/search/cdx?url=' + url, 10) - if r.status_code == 200: - dates = re.findall(r'[^\s]+\s+(\d+)\s+[^\s]+\s+[^\s]+\s+\d+\s+[^\s]+\s+\d{5,}', - r.content.decode('utf-8')) - # assume snapshots whose size >9999 contain real content, use the latest one of them - if len(dates) > 0: - get('http://web.archive.org/web/' + dates[-1] + '/' + url, 10) - check_content() - if content is not None: - fix_wayback_links() - else: - error = error + 'No snapshot available' - else: - error = error + str(r.status_code) - - def latest(): - nonlocal r, error, content - if settings.SCRAPESTACK_KEY is None: - error = error + '\nDirect: ' - get(url, 60) - else: - error = error + '\nScraperAPI: ' - get(f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}', 60) - check_content() - - # wayback_cdx() - # if content is None: - latest() - - if content is None: - logger.error(error) - content = '' - # with open('/tmp/temp.html', 'w', encoding='utf-8') as fp: - # fp.write(content) - return html.fromstring(content) - - @classmethod - def download_image(cls, url, item_url=None): - if url is None: - return None, None - raw_img = None - ext = None - - dl_url = url - if settings.SCRAPESTACK_KEY is not None: - dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}' - - try: - img_response = requests.get(dl_url, timeout=90) - if img_response.status_code == 200: - raw_img = img_response.content - img = Image.open(BytesIO(raw_img)) - img.load() # corrupted image will trigger exception - content_type = img_response.headers.get('Content-Type') - ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension - else: - logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}") - # raise RuntimeError(f"Douban: download image failed {img_response.status_code} {dl_url}") - except Exception as e: - raw_img = None - ext = None - logger.error(f"Douban: download image failed {e} {dl_url} {item_url}") - if raw_img is None and settings.SCRAPESTACK_KEY is not None: - try: - img_response = requests.get(dl_url, timeout=90) - if img_response.status_code == 200: - raw_img = img_response.content - img = Image.open(BytesIO(raw_img)) - img.load() # corrupted image will trigger exception - content_type = img_response.headers.get('Content-Type') - ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension - else: - logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}") - except Exception as e: - raw_img = None - ext = None - logger.error(f"Douban: download image failed {e} {dl_url} {item_url}") - return raw_img, ext - - -class DoubanMoviePatcher(DoubanPatcherMixin, AbstractScraper): - site_name = SourceSiteEnum.DOUBAN.value - host = 'movie.douban.com' - data_class = Movie - form_class = MovieForm - - regex = re.compile(r"https://movie\.douban\.com/subject/\d+/{0,1}") - - def scrape(self, url): - headers = DEFAULT_REQUEST_HEADERS.copy() - headers['Host'] = self.host - content = self.download_page(url, headers) - 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, ext = self.download_image(img_url, url) - return raw_img, ext - - -class Command(BaseCommand): - help = 'fix cover image' - - def add_arguments(self, parser): - parser.add_argument('threadId', type=int, help='% 8') - - def handle(self, *args, **options): - t = int(options['threadId']) - for m in Movie.objects.filter(cover='movie/default.svg', source_site='douban'): - if m.id % 8 == t: - print(f'Re-fetching {m.source_url}') - try: - raw_img, img_ext = DoubanMoviePatcher.scrape(m.source_url) - if img_ext is not None: - m.cover = SimpleUploadedFile('temp.' + img_ext, raw_img) - m.save() - print(f'Saved {m.source_url}') - else: - print(f'Skipped {m.source_url}') - except Exception as e: - print(e) - # return diff --git a/movies/models.py b/movies/models.py deleted file mode 100644 index 53748cd6..00000000 --- a/movies/models.py +++ /dev/null @@ -1,293 +0,0 @@ -import uuid -import django.contrib.postgres.fields as postgres -from django.utils.translation import gettext_lazy as _ -from django.db import models -from django.core.serializers.json import DjangoJSONEncoder -from django.shortcuts import reverse -from common.models import Entity, Mark, Review, Tag, MarkStatusEnum -from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath -from django.utils import timezone -from django.conf import settings -from django.db.models import Q -import re -from simple_history.models import HistoricalRecords - - -MovieMarkStatusTranslation = { - MarkStatusEnum.DO.value: _("在看"), - MarkStatusEnum.WISH.value: _("想看"), - MarkStatusEnum.COLLECT.value: _("看过") -} - - -def movie_cover_path(instance, filename): - return GenerateDateUUIDMediaFilePath(instance, filename, settings.MOVIE_MEDIA_PATH_ROOT) - - -class MovieGenreEnum(models.TextChoices): - DRAMA = 'Drama', _('剧情') - KIDS = 'Kids', _('儿童') - COMEDY = 'Comedy', _('喜剧') - BIOGRAPHY = 'Biography', _('传记') - ACTION = 'Action', _('动作') - HISTORY = 'History', _('历史') - ROMANCE = 'Romance', _('爱情') - WAR = 'War', _('战争') - SCI_FI = 'Sci-Fi', _('科幻') - CRIME = 'Crime', _('犯罪') - ANIMATION = 'Animation', _('动画') - WESTERN = 'Western', _('西部') - MYSTERY = 'Mystery', _('悬疑') - FANTASY = 'Fantasy', _('奇幻') - THRILLER = 'Thriller', _('惊悚') - ADVENTURE = 'Adventure', _('冒险') - HORROR = 'Horror', _('恐怖') - DISASTER = 'Disaster', _('灾难') - DOCUMENTARY = 'Documentary', _('纪录片') - MARTIAL_ARTS = 'Martial-Arts', _('武侠') - SHORT = 'Short', _('短片') - ANCIENT_COSTUM = 'Ancient-Costum', _('古装') - EROTICA = 'Erotica', _('情色') - SPORT = 'Sport', _('运动') - GAY_LESBIAN = 'Gay/Lesbian', _('同性') - OPERA = 'Opera', _('戏曲') - MUSIC = 'Music', _('音乐') - FILM_NOIR = 'Film-Noir', _('黑色电影') - MUSICAL = 'Musical', _('歌舞') - REALITY_TV = 'Reality-TV', _('真人秀') - FAMILY = 'Family', _('家庭') - TALK_SHOW = 'Talk-Show', _('脱口秀') - NEWS = 'News', _('新闻') - SOAP = 'Soap', _('肥皂剧') - TV_MOVIE = 'TV Movie', _('电视电影') - THEATRE = 'Theatre', _('舞台艺术') - OTHER = 'Other', _('其他') - - -MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum) - - -class Movie(Entity): - ''' - Can either be movie or series. - ''' - # widely recognized name, usually in Chinese - title = models.CharField(_("title"), max_length=500) - # original name, for books in foreign language - orig_title = models.CharField( - _("original title"), blank=True, default='', max_length=500) - other_title = postgres.ArrayField( - models.CharField(_("other title"), blank=True, - default='', max_length=500), - null=True, - blank=True, - default=list, - ) - imdb_code = models.CharField( - blank=True, max_length=10, null=False, db_index=True, default='') - director = postgres.ArrayField( - models.CharField(_("director"), blank=True, - default='', max_length=200), - null=True, - blank=True, - default=list, - ) - playwright = postgres.ArrayField( - models.CharField(_("playwright"), blank=True, - default='', max_length=200), - null=True, - blank=True, - default=list, - ) - actor = postgres.ArrayField( - models.CharField(_("actor"), blank=True, - default='', max_length=200), - null=True, - blank=True, - default=list, - ) - genre = postgres.ArrayField( - models.CharField( - _("genre"), - blank=True, - default='', - choices=MovieGenreEnum.choices, - max_length=50 - ), - null=True, - blank=True, - default=list, - ) - showtime = postgres.ArrayField( - # HStoreField stores showtime-region pair - postgres.HStoreField(), - null=True, - blank=True, - default=list, - ) - site = models.URLField(_('site url'), blank=True, default='', max_length=200) - - # country or region - area = postgres.ArrayField( - models.CharField( - _("country or region"), - blank=True, - default='', - max_length=100, - ), - null=True, - blank=True, - default=list, - ) - - language = postgres.ArrayField( - models.CharField( - blank=True, - default='', - max_length=100, - ), - null=True, - blank=True, - default=list, - ) - - year = models.PositiveIntegerField(null=True, blank=True) - duration = models.CharField(blank=True, default='', max_length=200) - - cover = models.ImageField(_("poster"), upload_to=movie_cover_path, default=settings.DEFAULT_MOVIE_IMAGE, blank=True) - - ############################################ - # exclusive fields to series - ############################################ - season = models.PositiveSmallIntegerField(null=True, blank=True) - # how many episodes in the season - episodes = models.PositiveIntegerField(null=True, blank=True) - # deprecated - # tv_station = models.CharField(blank=True, default='', max_length=200) - single_episode_length = models.CharField(blank=True, default='', max_length=100) - - ############################################ - # category identifier - ############################################ - is_series = models.BooleanField(default=False) - - history = HistoricalRecords() - - def __str__(self): - if self.year: - return self.title + f"({self.year})" - else: - return self.title - - def get_json(self): - r = { - 'other_title': self.other_title, - 'original_title': self.orig_title, - 'director': self.director, - 'playwright': self.playwright, - 'actor': self.actor, - 'release_year': self.year, - 'genre': self.genre, - 'language': self.language, - 'season': self.season, - 'duration': self.duration, - 'imdb_code': self.imdb_code, - } - r.update(super().get_json()) - return r - - def get_absolute_url(self): - return reverse("movies:retrieve", args=[self.id]) - - @property - def wish_url(self): - return reverse("movies:wish", args=[self.id]) - - def get_tags_manager(self): - return self.movie_tags - - def get_genre_display(self): - translated_genre = [] - for g in self.genre: - translated_genre.append(MovieGenreTranslator[g]) - return translated_genre - - def get_related_movies(self): - imdb = 'no match' if self.imdb_code is None or self.imdb_code == '' else self.imdb_code - qs = Q(imdb_code=imdb) - if self.is_series: - prefix = re.sub(r'\d+', '', re.sub(r'\s+第.+季', '', self.title)) - if not prefix: - prefix = self.title - qs = qs | Q(title__startswith=prefix) - qs = qs & ~Q(id=self.id) - return Movie.objects.filter(qs).order_by('season') - - def get_identicals(self): - qs = Q(orig_title=self.title) - if self.imdb_code: - qs = Q(imdb_code=self.imdb_code) - # qs = qs & ~Q(id=self.id) - return Movie.objects.filter(qs) - else: - return [self] # Book.objects.filter(id=self.id) - - @property - def verbose_category_name(self): - if self.is_series: - return _("剧集") - else: - return _("电影") - - @property - def mark_class(self): - return MovieMark - - @property - def tag_class(self): - return MovieTag - - -class MovieMark(Mark): - movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_marks', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['owner', 'movie'], name='unique_movie_mark') - ] - - @property - def translated_status(self): - return MovieMarkStatusTranslation[self.status] - - -class MovieReview(Review): - movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_reviews', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'movie'], name='unique_movie_review') - ] - - @property - def url(self): - return reverse("movies:retrieve_review", args=[self.id]) - - @property - def item(self): - return self.movie - - -class MovieTag(Tag): - movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='movie_tags', null=True) - mark = models.ForeignKey(MovieMark, on_delete=models.CASCADE, related_name='moviemark_tags', null=True) - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['content', 'mark'], name="unique_moviemark_tag") - ] - - @property - def item(self): - return self.movie diff --git a/movies/templates/movies/create_update.html b/movies/templates/movies/create_update.html deleted file mode 100644 index 471f664d..00000000 --- a/movies/templates/movies/create_update.html +++ /dev/null @@ -1,102 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {{ title }} - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
- {% if is_update and form.source_site.value != 'in-site' %} -
-
-
-
{% trans '源网站' %}: {{ form.source_site.value }}
-
-
- {% csrf_token %} - -
-
-
-
-
- {% endif %} - -
- {% comment %} {% trans '>>> 试试一键剽取~ <<<' %} {% endcomment %} -
- {% csrf_token %} - {{ form.media }} - {% for field in form %} - - {% if field.id_for_label == 'id_is_series' %} - - {{ field }} - {% else %} - {% if field.id_for_label != 'id_id' %} - - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - - -
-
- -
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/movies/templates/movies/create_update_review.html b/movies/templates/movies/create_update_review.html deleted file mode 100644 index fca3183b..00000000 --- a/movies/templates/movies/create_update_review.html +++ /dev/null @@ -1,168 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ title }} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - -
-
- -
- {% if movie.season %} - {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season - {{ movie.season }} - - {% if movie.year %}({{ movie.year }}){% endif %} - - {% else %} - {{ movie.title }} {{ movie.orig_title }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% endif %} - - {{ movie.get_source_site_display }} -
-
{% if movie.director %}{% trans '导演:' %} - {% for director in movie.director %} - 5 %}style="display: none;" {% endif %}> - {{ director }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if movie.director|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if movie.genre %}{% trans '类型:' %} - {% for genre in movie.get_genre_display %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- -
{% if movie.actor %}{% trans '主演:' %} - {% for actor in movie.actor %} - 5 %}style="display: none;" {% endif %}> - {{ actor }} - {% if not forloop.last %} / {% endif %} - - {% if forloop.counter <= 5 %} - {% endif %} - {% endfor %} - - {% if movie.actor|length > 5 %} - {% trans '更多' %} - - {% endif %} - - {% endif %}
-
{% if movie.showtime %}{% trans '上映时间:' %} - {% for showtime in movie.showtime %} - {% for time, region in showtime.items %} - {{ time }}({{ region }}) - {% endfor %} - {% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- - {% if movie.rating %} - {% trans '评分:' %} - {{ movie.rating }} - {% endif %} -
-
-
- -
- {% csrf_token %} - {{ form.movie }} -
- {{ form.title.label }} -
- {{ form.title }} -
- - {{ form.content.label }} - - - {% trans '预览' %} - -
-
- {{ form.content }} -
-
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
-
-
- - {{ form.visibility.label }}{{ form.visibility }} -
- -
-
- -
- {{ form.media }} -
- -
- -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - diff --git a/movies/templates/movies/delete.html b/movies/templates/movies/delete.html deleted file mode 100644 index a4d8f843..00000000 --- a/movies/templates/movies/delete.html +++ /dev/null @@ -1,106 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {% trans '删除电影/剧集' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这部电影/剧集吗?相关评论和标记将一并删除。' %}
- -
-
- - - -
-
- -
- {% if movie.season %} - {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season - {{ movie.season }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% else %} - {{ movie.title }} {{ movie.orig_title }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% endif %} - {{ movie.get_source_site_display }} -
- - {% if movie.rating %} - {% trans '评分:' %} - - {{ movie.rating }} - {% else %} - {% trans '评分:暂无评分' %} - {% endif %} - - {% if movie.last_editor %} -
- {% trans '最近编辑者:' %} - - {{ movie.last_editor | default:"" }} - -
- {% endif %} - -
{% trans '上次编辑时间:' %}{{ movie.edited_time }}
- - {% if movie.movie_marks.all %} -
{% trans '这个条目有' %} {{ movie.movie_marks.count }} 个标记
- {% endif %} - {% if movie.movie_reviews.all %} -
{% trans '这个条目有' %} {{ movie.movie_reviews.count }} 个评论
- {% endif %} - -
-
-
-
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/movies/templates/movies/delete_review.html b/movies/templates/movies/delete_review.html deleted file mode 100644 index d51f63cf..00000000 --- a/movies/templates/movies/delete_review.html +++ /dev/null @@ -1,101 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '删除评论' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这篇评论吗?' %}
- -
- - -
- -
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} - -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- - -
-
- {{ form.content }} -
- {{ form.media }} - -
- -
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html deleted file mode 100644 index 6496ddcf..00000000 --- a/movies/templates/movies/detail.html +++ /dev/null @@ -1,527 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load strip_scheme %} -{% load thumb %} - - - - - - - - - - - - - - - {% if movie.is_series %} - {{ site_name }} - {% trans '剧集详情' %} | {{ movie.title }} - {% else %} - {{ site_name }} - {% trans '电影详情' %} | {{ movie.title }} - {% endif %} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - {{ movie.title }} - - -
-
- - {% if movie.season %} - {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season {{ movie.season }} - - {% if movie.year %}({{ movie.year }}){% endif %} - - {% else %} - {{ movie.title }} {{ movie.orig_title }} - - {% if movie.year %}({{ movie.year }}){% endif %} - - {% endif %} - {{ movie.get_source_site_display }} -
- -
-
- {% if movie.rating and movie.rating_number >= 5 %} - - {{ movie.rating }} - ({{ movie.rating_number }}人评分) - {% else %} - {% trans '评分:评分人数不足' %} - {% endif %} -
-
{% if movie.imdb_code %} - {% trans 'IMDb:' %}{{ movie.imdb_code }} - {% endif %} -
-
{% if movie.director %}{% trans '导演:' %} - {% for director in movie.director %} - 5 %}style="display: none;" {% endif %}> - {{ director }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if movie.director|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if movie.playwright %}{% trans '编剧:' %} - {% for playwright in movie.playwright %} - 5 %}style="display: none;" {% endif %}> - {{ playwright }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if movie.playwright|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if movie.actor %}{% trans '主演:' %} - {% for actor in movie.actor %} - 5 %}style="display: none;"{% endif %}> - {{ actor }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - - {% if movie.actor|length > 5 %} - {% trans '更多' %} - - {% endif %} - - {% endif %}
-
{% if movie.genre %}{% trans '类型:' %} - {% for genre in movie.get_genre_display %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
-
{% if movie.area %}{% trans '制片国家/地区:' %} - {% for area in movie.area %} - {{ area }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
-
{% if movie.language %}{% trans '语言:' %} - {% for language in movie.language %} - {{ language }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- -
-
-
{% if movie.duration %}{% trans '片长:' %}{{ movie.duration }}{% endif %}
-
{% if movie.season %}{% trans '季数:' %}{{ movie.season }}{% endif %}
-
{% if movie.episodes %}{% trans '集数:' %}{{ movie.episodes }}{% endif %}
-
{% if movie.single_episode_length %}{% trans '单集长度:' %}{{ movie.single_episode_length }}{% endif %}
- -
{% if movie.showtime %}{% trans '上映时间:' %} - {% for showtime in movie.showtime %} - {% for time, region in showtime.items %} - {{ time }}{% if region != '' %}({{ region }}){% endif %} - {% endfor %} - {% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
-
{% if movie.other_title %}{% trans '又名:' %} - {% for other_title in movie.other_title %} - {{ other_title }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
-
{% if movie.site %}{% trans '网站:' %} - {{ movie.site|strip_scheme }} - {% endif %}
- {% if movie.other_info %} - {% for k, v in movie.other_info.items %} -
- {{ k }}:{{ v | urlize }} -
- {% endfor %} - {% endif %} - - - {% if movie.last_editor and movie.last_editor.preference.show_last_edit or user.is_staff %} -
{% trans '最近编辑者:' %}{{ movie.last_editor | default:"" }}
- {% endif %} - -
- {% if movie.is_series %} - {% trans '编辑这部剧集' %} - {% else %} - {% trans '编辑这部电影' %} - {% endif %} - {% if user.is_staff %} - / {% trans '删除' %} - {% endif %} -
-
- -
- - {% for tag_dict in movie_tag_list %} - {% for k, v in tag_dict.items %} - {% if k == 'content' %} - - {{ v }} - - {% endif %} - {% endfor %} - {% endfor %} - -
-
-
-
- {% if movie.brief %} -
-
{% trans '简介' %}
- -

{{ movie.brief | linebreaksbr }}

- - -
- {% endif %} - - -
- - {% if movie.is_series %} -
{% trans '这部剧集的标记' %}
- {% else %} -
{% trans '这部电影的标记' %}
- {% endif %} - {% if mark_list_more %} - {% trans '全部标记' %} - {% endif %} - 关注的人的标记 - {% include "partial/mark_list.html" with mark_list=mark_list current_item=movie %} -
-
- {% if movie.is_series %} -
{% trans '这部剧集的评论' %}
- {% else %} -
{% trans '这部电影的评论' %}
- {% endif %} - - {% if review_list_more %} - {% trans '全部评论' %} - {% endif %} - {% if review_list %} - - {% else %} -
{% trans '暂无评论' %}
- {% endif %} -
-
-
- -
-
- - {% if mark %} -
- - {% trans '我' %}{{ mark.get_status_display }} - {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} - {% if mark.rating %} - - {% endif %} - {% endif %} - {% if mark.visibility > 0 %} - - {% endif %} - - {% trans '修改' %} -
- {% csrf_token %} - {% trans '删除' %} -
-
-
- -
{{ mark.created_time }}
- - {% if mark.text %} -

{{ mark.text }}

- {% endif %} -
- - {% for tag in mark_tags %} - {{ tag }} - {% endfor %} - -
-
- {% else %} -
- - {% if movie.is_series %} -
{% trans '标记这部剧集' %}
- {% else %} -
{% trans '标记这部电影' %}
- {% endif %} -
- - - -
-
- {% endif %} - -
- -
- {% if review %} -
- - {% trans '我的评论' %} - {% if review.visibility > 0 %} - - {% endif %} - - - {% trans '编辑' %} - {% trans '删除' %} - - -
{{ review.edited_time }}
- - - {{ review.title }} - -
- {% else %} - -
-
{% trans '我的评论' %}
- -
- - {% endif %} -
- - {% if movie.get_related_movies.count > 0 %} -
-
-
{% trans '相关条目' %}
-
- {% for m in movie.get_related_movies %} -

- {{ m.title }} - {% if movie.source_site != m.source_site %} - {{ m.get_source_site_display }} - {% endif %} -

- {% endfor %} -
-
-
- {% endif %} - - {% if collection_list %} -
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %} -

- {{ c.title }} -

- {% endfor %} -
- -
-
-
-
- {% endif %} -
-
-
- -
- {% include "partial/_footer.html" %} -
- -
- - - -
-
- - - - - - diff --git a/movies/templates/movies/mark_list.html b/movies/templates/movies/mark_list.html deleted file mode 100644 index 20da04e6..00000000 --- a/movies/templates/movies/mark_list.html +++ /dev/null @@ -1,148 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ movie.title }}{% trans '的标记' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ movie.title }}{% trans ' 的标记' %} -
- {% include "partial/mark_list.html" with mark_list=marks current_item=movie %} -
- -
-
- -
-
-
-
- -
-
-
- {% if movie.season %} - {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season - {{ movie.season }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% else %} - {{ movie.title }} {{ movie.orig_title }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% endif %} - - {{ movie.get_source_site_display }} -
- -
{% if movie.director %}{% trans '导演:' %} - {% for director in movie.director %} - 5 %}style="display: none;" {% endif %}> - {{ director }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if movie.director|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if movie.genre %}{% trans '类型:' %} - {% for genre in movie.get_genre_display %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- -
{% if movie.showtime %}{% trans '上映时间:' %} - {% for showtime in movie.showtime %} - {% for time, region in showtime.items %} - {{ time }}({{ region }}) - {% endfor %} - {% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- {% if movie.rating %} - {% trans '评分: ' %} - {{ movie.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/movies/templates/movies/review_detail.html b/movies/templates/movies/review_detail.html deleted file mode 100644 index 53e213f3..00000000 --- a/movies/templates/movies/review_detail.html +++ /dev/null @@ -1,160 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - {{ site_name }}影评 - {{ review.title }} - - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- {% if request.user == review.owner %} - {% trans '编辑' %} - {% trans '删除' %} - {% endif %} -
-
- -
- {{ form.content }} -
- {{ form.media }} - {% csrf_token %} -
-
- -
-
-
-
-
- -
-
-
- {% if movie.season %} - {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season - {{ movie.season }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% else %} - {{ movie.title }} {{ movie.orig_title }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% endif %} - - {{ movie.get_source_site_display }} -
- -
{% if movie.director %}{% trans '导演:' %} - {% for director in movie.director %} - 5 %}style="display: none;" {% endif %}> - {{ director }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if movie.director|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if movie.genre %}{% trans '类型:' %} - {% for genre in movie.get_genre_display %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- -
{% if movie.showtime %}{% trans '上映时间:' %} - {% for showtime in movie.showtime %} - {% for time, region in showtime.items %} - {{ time }}({{ region }}) - {% endfor %} - {% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- {% if movie.rating %} - {% trans '评分: ' %} - {{ movie.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/movies/templates/movies/review_list.html b/movies/templates/movies/review_list.html deleted file mode 100644 index d9541285..00000000 --- a/movies/templates/movies/review_list.html +++ /dev/null @@ -1,169 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ movie.title }}{% trans '的评论' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ movie.title }}{% trans ' 的评论' %} -
- -
- -
-
- -
-
-
-
- -
-
-
- {% if movie.season %} - {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %} {{ movie.orig_title }} Season - {{ movie.season }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% else %} - {{ movie.title }} {{ movie.orig_title }} - {% if movie.year %}({{ movie.year }}){% endif %} - {% endif %} - - {{ movie.get_source_site_display }} -
- -
{% if movie.director %}{% trans '导演:' %} - {% for director in movie.director %} - 5 %}style="display: none;" {% endif %}> - {{ director }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if movie.director|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if movie.genre %}{% trans '类型:' %} - {% for genre in movie.get_genre_display %} - {{ genre }}{% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- -
{% if movie.showtime %}{% trans '上映时间:' %} - {% for showtime in movie.showtime %} - {% for time, region in showtime.items %} - {{ time }}({{ region }}) - {% endfor %} - {% if not forloop.last %} / {% endif %} - {% endfor %} - {% endif %}
- {% if movie.rating %} - {% trans '评分: ' %} - {{ movie.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/movies/templates/movies/scrape.html b/movies/templates/movies/scrape.html deleted file mode 100644 index 32a94f8a..00000000 --- a/movies/templates/movies/scrape.html +++ /dev/null @@ -1,109 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '从豆瓣获取数据' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {% trans '根据豆瓣内容填写下方表单' %} -
- -
-
-
- {% csrf_token %} - - {{ form.media }} - - {% for field in form %} - - {% if field.id_for_label == 'id_is_series' %} - - {{ field }} - {% else %} - {% if field.id_for_label != 'id_id' %} - - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - -
- {% trans '剽取!' %} -
-
-
-
- -
-
-
- {% trans '复制详情页链接' %} -
-
- {% csrf_token %} - - -
-
- -
-
-
- -
- {% include "partial/_footer.html" %} - -
- - - - - diff --git a/movies/urls.py b/movies/urls.py deleted file mode 100644 index 0495a9f0..00000000 --- a/movies/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.urls import path, re_path -from .views import * - - -app_name = 'movies' -urlpatterns = [ - path('create/', create, name='create'), - path('/', retrieve, name='retrieve'), - path('update//', update, name='update'), - path('delete//', delete, name='delete'), - path('rescrape//', rescrape, name='rescrape'), - path('mark/', create_update_mark, name='create_update_mark'), - path('wish//', wish, name='wish'), - re_path('(?P[0-9]+)/mark/list/(?:(?P\\d+))?', retrieve_mark_list, name='retrieve_mark_list'), - path('mark/delete//', delete_mark, name='delete_mark'), - path('/review/create/', create_review, name='create_review'), - path('review/update//', update_review, name='update_review'), - path('review/delete//', delete_review, name='delete_review'), - path('review//', retrieve_review, name='retrieve_review'), - path('/review/list/', - retrieve_review_list, name='retrieve_review_list'), - path('scrape/', scrape, name='scrape'), - path('click_to_scrape/', click_to_scrape, name='click_to_scrape'), -] diff --git a/movies/views.py b/movies/views.py deleted file mode 100644 index 8a5cccaa..00000000 --- a/movies/views.py +++ /dev/null @@ -1,584 +0,0 @@ -import logging -from django.shortcuts import render, get_object_or_404, redirect, reverse -from django.contrib.auth.decorators import login_required, permission_required -from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import IntegrityError, transaction -from django.db.models import Count -from django.utils import timezone -from django.core.paginator import Paginator -from mastodon import mastodon_request_included -from mastodon.models import MastodonApplication -from mastodon.api import share_mark, share_review -from common.utils import PageLinksGenerator -from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin -from common.models import SourceSiteEnum -from .models import * -from .forms import * -from django.conf import settings -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(request): - if request.method == 'GET': - form = MovieForm() - return render( - request, - 'movies/create_update.html', - { - 'form': form, - 'title': _('添加电影/剧集'), - 'submit_url': reverse("movies:create"), - # 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 = MovieForm(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("movies:retrieve", args=[form.instance.id])) - else: - return render( - request, - 'movies/create_update.html', - { - 'form': form, - 'title': _('添加电影/剧集'), - 'submit_url': reverse("movies:create"), - # 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(Movie, 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("movies:retrieve", args=[form.instance.id])) - - -@login_required -def update(request, id): - if request.method == 'GET': - movie = get_object_or_404(Movie, pk=id) - form = MovieForm(instance=movie) - page_title = _('修改剧集') if movie.is_series else _("修改电影") - return render( - request, - 'movies/create_update.html', - { - 'form': form, - 'is_update': True, - 'title': page_title, - 'submit_url': reverse("movies:update", args=[movie.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - elif request.method == 'POST': - movie = get_object_or_404(Movie, pk=id) - form = MovieForm(request.POST, request.FILES, instance=movie) - page_title = _('修改剧集') if movie.is_series else _("修改电影") - 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, - 'movies/create_update.html', - { - 'form': form, - 'is_update': True, - 'title': page_title, - 'submit_url': reverse("movies:update", args=[movie.id]), - # provided for frontend js - 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, - } - ) - return redirect(reverse("movies:retrieve", args=[form.instance.id])) - - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -# @login_required -def retrieve(request, id): - if request.method == 'GET': - movie = get_object_or_404(Movie, pk=id) - mark = None - mark_tags = None - review = None - - # retreive tags - movie_tag_list = movie.movie_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 = MovieMark.objects.get(owner=request.user, movie=movie) - except ObjectDoesNotExist: - mark = None - if mark: - mark_tags = mark.moviemark_tags.all() - mark.get_status_display = MovieMarkStatusTranslator(mark.status) - mark_form = MovieMarkForm(instance=mark, initial={ - 'tags': mark_tags - }) - else: - mark_form = MovieMarkForm(initial={ - 'movie': movie, - '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 = MovieReview.objects.get(owner=request.user, movie=movie) - 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 = MovieMark.get_available_for_identicals(movie, request.user) - review_list = MovieReview.get_available_for_identicals(movie, 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 = MovieMarkStatusTranslator(m.status) - review_list_more = True if len( - review_list) > REVIEW_NUMBER else False - review_list = review_list[:REVIEW_NUMBER] - all_collections = CollectionItem.objects.filter(movie=movie).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20] - collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections)) - - # 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, - 'movies/detail.html', - { - 'movie': movie, - '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, - 'movie_tag_list': movie_tag_list, - 'mark_tags': mark_tags, - 'collection_list': collection_list, - } - ) - else: - logger.warning('non-GET method at /movies/') - return HttpResponseBadRequest() - - -@permission_required("movies.delete_movie") -@login_required -def delete(request, id): - if request.method == 'GET': - movie = get_object_or_404(Movie, pk=id) - return render( - request, - 'movies/delete.html', - { - 'movie': movie, - } - ) - elif request.method == 'POST': - if request.user.is_staff: - # only staff has right to delete - movie = get_object_or_404(Movie, pk=id) - movie.delete() - return redirect(reverse("common:home")) - else: - raise PermissionDenied() - else: - return HttpResponseBadRequest() - - -# user owned entites -########################### -@mastodon_request_included -@login_required -def create_update_mark(request): - # check list: - # clean rating if is wish - # transaction on updating movie rating - # owner check(guarantee) - if request.method == 'POST': - pk = request.POST.get('id') - old_rating = None - old_tags = None - if not pk: - movie_id = request.POST.get('movie') - mark = MovieMark.objects.filter(movie_id=movie_id, owner=request.user).first() - if mark: - pk = mark.id - if pk: - mark = get_object_or_404(MovieMark, pk=pk) - if request.user != mark.owner: - return HttpResponseBadRequest() - old_rating = mark.rating - old_tags = mark.moviemark_tags.all() - if mark.status != request.POST.get('status'): - mark.created_time = timezone.now() - # update - form = MovieMarkForm(request.POST, instance=mark) - else: - # create - form = MovieMarkForm(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() - movie = form.instance.movie - - try: - with transaction.atomic(): - # update movie rating - movie.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']: - MovieTag.objects.create( - content=tag, - movie=movie, - 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("movies:retrieve", args=[form.instance.movie.id])) - else: - return HttpResponseBadRequest("invalid method") - - -@mastodon_request_included -@login_required -def wish(request, id): - if request.method == 'POST': - movie = get_object_or_404(Movie, pk=id) - params = { - 'owner': request.user, - 'status': MarkStatusEnum.WISH, - 'visibility': request.user.preference.default_visibility, - 'movie': movie, - } - try: - MovieMark.objects.create(**params) - except Exception: - pass - return HttpResponse("✔️") - else: - return HttpResponseBadRequest("invalid method") - - -@mastodon_request_included -@login_required -def retrieve_mark_list(request, movie_id, following_only=False): - if request.method == 'GET': - movie = get_object_or_404(Movie, pk=movie_id) - queryset = MovieMark.get_available_for_identicals(movie, 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 = MovieMarkStatusTranslator(m.status) - return render( - request, - 'movies/mark_list.html', - { - 'marks': marks, - 'movie': movie, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def delete_mark(request, id): - if request.method == 'POST': - mark = get_object_or_404(MovieMark, pk=id) - if request.user != mark.owner: - return HttpResponseBadRequest() - movie_id = mark.movie.id - try: - with transaction.atomic(): - # update movie rating - mark.movie.update_rating(mark.rating, None) - mark.delete() - except IntegrityError as e: - return HttpResponseServerError() - return redirect(reverse("movies:retrieve", args=[movie_id])) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def create_review(request, movie_id): - if request.method == 'GET': - form = MovieReviewForm(initial={'movie': movie_id}) - movie = get_object_or_404(Movie, pk=movie_id) - return render( - request, - 'movies/create_update_review.html', - { - 'form': form, - 'title': _("添加评论"), - 'movie': movie, - 'submit_url': reverse("movies:create_review", args=[movie_id]), - } - ) - elif request.method == 'POST': - form = MovieReviewForm(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("movies:retrieve_review", args=[form.instance.id])) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def update_review(request, id): - if request.method == 'GET': - review = get_object_or_404(MovieReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - form = MovieReviewForm(instance=review) - movie = review.movie - return render( - request, - 'movies/create_update_review.html', - { - 'form': form, - 'title': _("编辑评论"), - 'movie': movie, - 'submit_url': reverse("movies:update_review", args=[review.id]), - } - ) - elif request.method == 'POST': - review = get_object_or_404(MovieReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - form = MovieReviewForm(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("movies:retrieve_review", args=[form.instance.id])) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() - - -@login_required -def delete_review(request, id): - if request.method == 'GET': - review = get_object_or_404(MovieReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - review_form = MovieReviewForm(instance=review) - return render( - request, - 'movies/delete_review.html', - { - 'form': review_form, - 'review': review, - } - ) - elif request.method == 'POST': - review = get_object_or_404(MovieReview, pk=id) - if request.user != review.owner: - return HttpResponseBadRequest() - movie_id = review.movie.id - review.delete() - return redirect(reverse("movies:retrieve", args=[movie_id])) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -def retrieve_review(request, id): - if request.method == 'GET': - review = get_object_or_404(MovieReview, pk=id) - if not review.is_visible_to(request.user): - msg = _("你没有访问这个页面的权限😥") - return render( - request, - 'common/error.html', - { - 'msg': msg, - } - ) - review_form = MovieReviewForm(instance=review) - movie = review.movie - try: - mark = MovieMark.objects.get(owner=review.owner, movie=movie) - mark.get_status_display = MovieMarkStatusTranslator(mark.status) - except ObjectDoesNotExist: - mark = None - return render( - request, - 'movies/review_detail.html', - { - 'form': review_form, - 'review': review, - 'movie': movie, - 'mark': mark, - } - ) - else: - return HttpResponseBadRequest() - - -@mastodon_request_included -@login_required -def retrieve_review_list(request, movie_id): - if request.method == 'GET': - movie = get_object_or_404(Movie, pk=movie_id) - queryset = MovieReview.get_available_for_identicals(movie, 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, - 'movies/review_list.html', - { - 'reviews': reviews, - 'movie': movie, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def scrape(request): - if request.method == 'GET': - keywords = request.GET.get('q') - form = MovieForm() - return render( - request, - 'movies/scrape.html', - { - 'q': keywords, - 'form': form, - } - ) - else: - return HttpResponseBadRequest() - - -@login_required -def click_to_scrape(request): - if request.method == "POST": - url = request.POST.get("url") - if url: - return jump_or_scrape(request, url) - else: - return HttpResponseBadRequest() - else: - return HttpResponseBadRequest() diff --git a/music/__init__.py b/music/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/music/admin.py b/music/admin.py deleted file mode 100644 index 33eb056d..00000000 --- a/music/admin.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.contrib import admin -from .models import * -from simple_history.admin import SimpleHistoryAdmin - -admin.site.register(Song, SimpleHistoryAdmin) -admin.site.register(SongMark) -admin.site.register(SongReview) -admin.site.register(SongTag) -admin.site.register(Album, SimpleHistoryAdmin) -admin.site.register(AlbumMark) -admin.site.register(AlbumReview) -admin.site.register(AlbumTag) diff --git a/music/apps.py b/music/apps.py deleted file mode 100644 index 6fb97b37..00000000 --- a/music/apps.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.apps import AppConfig - - -class MusicConfig(AppConfig): - name = 'music' - - def ready(self): - from common.index import Indexer - from .models import Album, Song - Indexer.update_model_indexable(Album) - Indexer.update_model_indexable(Song) diff --git a/music/forms.py b/music/forms.py deleted file mode 100644 index 9e487592..00000000 --- a/music/forms.py +++ /dev/null @@ -1,157 +0,0 @@ -from django import forms -from django.contrib.postgres.forms import SimpleArrayField -from django.utils.translation import gettext_lazy as _ -from .models import * -from common.models import MarkStatusEnum -from common.forms import * - - -def MusicMarkStatusTranslator(status): - return MusicMarkStatusTranslation[status] - - -class SongForm(forms.ModelForm): - - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - other_info = JSONField(required=False, label=_("其他信息")) - duration = DurationField(required=False) - - class Meta: - model = Song - # fields = '__all__' - fields = [ - 'id', - 'title', - 'source_site', - 'source_url', - 'artist', - 'release_date', - 'duration', - 'isrc', - 'genre', - 'cover', - 'album', - 'brief', - 'other_info', - ] - widgets = { - 'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}), - 'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}), - 'cover': PreviewImageInput(), - } - - -class SongMarkForm(MarkForm): - - STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v)) - for v in MarkStatusEnum.values] - - status = forms.ChoiceField( - label=_(""), - widget=forms.RadioSelect(), - choices=STATUS_CHOICES - ) - - class Meta: - model = SongMark - fields = [ - 'id', - 'song', - 'status', - 'rating', - 'text', - 'visibility', - ] - widgets = { - 'song': forms.TextInput(attrs={"hidden": ""}), - } - - -class SongReviewForm(ReviewForm): - - class Meta: - model = SongReview - fields = [ - 'id', - 'song', - 'title', - 'content', - 'visibility' - ] - widgets = { - 'song': forms.TextInput(attrs={"hidden": ""}), - } - - -class AlbumForm(forms.ModelForm): - - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - other_info = JSONField(required=False, label=_("其他信息")) - duration = DurationField(required=False) - - class Meta: - model = Album - # fields = '__all__' - fields = [ - 'id', - 'title', - 'source_site', - 'source_url', - 'artist', - 'company', - 'release_date', - 'duration', - 'genre', - 'cover', - 'brief', - 'track_list', - 'other_info', - ] - widgets = { - 'artist': forms.TextInput(attrs={'placeholder': _("多个艺术家使用英文逗号分隔")}), - 'company': forms.TextInput(attrs={'placeholder': _("多个发行方使用英文逗号分隔")}), - 'duration': forms.TextInput(attrs={'placeholder': _("毫秒")}), - 'cover': PreviewImageInput(), - } - - -class AlbumMarkForm(MarkForm): - - STATUS_CHOICES = [(v, MusicMarkStatusTranslator(v)) - for v in MarkStatusEnum.values] - - status = forms.ChoiceField( - label=_(""), - widget=forms.RadioSelect(), - choices=STATUS_CHOICES - ) - - class Meta: - model = AlbumMark - fields = [ - 'id', - 'album', - 'status', - 'rating', - 'text', - 'visibility', - ] - widgets = { - 'album': forms.TextInput(attrs={"hidden": ""}), - } - - -class AlbumReviewForm(ReviewForm): - - class Meta: - model = AlbumReview - fields = [ - 'id', - 'album', - 'title', - 'content', - 'visibility' - ] - widgets = { - 'album': forms.TextInput(attrs={"hidden": ""}), - } diff --git a/music/management/commands/fix-album-cover.py b/music/management/commands/fix-album-cover.py deleted file mode 100644 index 8cd40fdc..00000000 --- a/music/management/commands/fix-album-cover.py +++ /dev/null @@ -1,205 +0,0 @@ -from django.core.management.base import BaseCommand -from django.core.files.uploadedfile import SimpleUploadedFile -from common.scraper import * -from django.conf import settings -from music.models import Album -from music.forms import AlbumForm -import requests -import re -import filetype -from lxml import html -from PIL import Image -from io import BytesIO - - -class DoubanPatcherMixin: - @classmethod - def download_page(cls, url, headers): - url = cls.get_effective_url(url) - r = None - error = 'DoubanScrapper: error occured when downloading ' + url - content = None - - def get(url, timeout): - nonlocal r - # print('Douban GET ' + url) - try: - r = requests.get(url, timeout=timeout) - except Exception as e: - r = requests.Response() - r.status_code = f"Exception when GET {url} {e}" + url - # print('Douban CODE ' + str(r.status_code)) - return r - - def check_content(): - nonlocal r, error, content - content = None - if r.status_code == 200: - content = r.content.decode('utf-8') - if content.find('关于豆瓣') == -1: - content = None - error = error + 'Content not authentic' # response is garbage - elif re.search('不存在[^<]+', content, re.MULTILINE): - content = None - error = error + 'Not found or hidden by Douban' - else: - error = error + str(r.status_code) - - def fix_wayback_links(): - nonlocal content - # fix links - content = re.sub(r'href="http[^"]+http', r'href="http', content) - # https://img9.doubanio.com/view/subject/{l|m|s}/public/s1234.jpg - content = re.sub(r'src="[^"]+/(s\d+\.\w+)"', - r'src="https://img9.doubanio.com/view/subject/m/public/\1"', content) - # https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2681329386.jpg - # https://img9.doubanio.com/view/photo/{l|m|s}/public/p1234.webp - content = re.sub(r'src="[^"]+/(p\d+\.\w+)"', - r'src="https://img9.doubanio.com/view/photo/m/public/\1"', content) - - # Wayback Machine: get latest available - def wayback(): - nonlocal r, error, content - error = error + '\nWayback: ' - get('http://archive.org/wayback/available?url=' + url, 10) - if r.status_code == 200: - w = r.json() - if w['archived_snapshots'] and w['archived_snapshots']['closest']: - get(w['archived_snapshots']['closest']['url'], 10) - check_content() - if content is not None: - fix_wayback_links() - else: - error = error + 'No snapshot available' - else: - error = error + str(r.status_code) - - # Wayback Machine: guess via CDX API - def wayback_cdx(): - nonlocal r, error, content - error = error + '\nWayback: ' - get('http://web.archive.org/cdx/search/cdx?url=' + url, 10) - if r.status_code == 200: - dates = re.findall(r'[^\s]+\s+(\d+)\s+[^\s]+\s+[^\s]+\s+\d+\s+[^\s]+\s+\d{5,}', - r.content.decode('utf-8')) - # assume snapshots whose size >9999 contain real content, use the latest one of them - if len(dates) > 0: - get('http://web.archive.org/web/' + dates[-1] + '/' + url, 10) - check_content() - if content is not None: - fix_wayback_links() - else: - error = error + 'No snapshot available' - else: - error = error + str(r.status_code) - - def latest(): - nonlocal r, error, content - if settings.SCRAPESTACK_KEY is None: - error = error + '\nDirect: ' - get(url, 60) - else: - error = error + '\nScraperAPI: ' - - if settings.SCRAPESTACK_KEY is not None: - dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}' - elif settings.SCRAPERAPI_KEY is not None: - dl_url = f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}' - get(dl_url, 60) - check_content() - - wayback_cdx() - if content is None: - latest() - - if content is None: - logger.error(error) - content = '' - return html.fromstring(content) - - @classmethod - def download_image(cls, url, item_url=None): - if url is None: - return None, None - raw_img = None - ext = None - - dl_url = url - - if settings.SCRAPESTACK_KEY is not None: - dl_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}' - elif settings.SCRAPERAPI_KEY is not None: - dl_url = f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}' - - try: - img_response = requests.get(dl_url, timeout=90) - if img_response.status_code == 200: - raw_img = img_response.content - img = Image.open(BytesIO(raw_img)) - img.load() # corrupted image will trigger exception - content_type = img_response.headers.get('Content-Type') - ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension - else: - logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}") - # raise RuntimeError(f"Douban: download image failed {img_response.status_code} {dl_url}") - except Exception as e: - raw_img = None - ext = None - logger.error(f"Douban: download image failed {e} {dl_url} {item_url}") - if raw_img is None and settings.SCRAPESTACK_KEY is not None: - try: - img_response = requests.get(dl_url, timeout=90) - if img_response.status_code == 200: - raw_img = img_response.content - img = Image.open(BytesIO(raw_img)) - img.load() # corrupted image will trigger exception - content_type = img_response.headers.get('Content-Type') - ext = filetype.get_type(mime=content_type.partition(';')[0].strip()).extension - else: - logger.error(f"Douban: download image failed {img_response.status_code} {dl_url} {item_url}") - except Exception as e: - raw_img = None - ext = None - logger.error(f"Douban: download image failed {e} {dl_url} {item_url}") - return raw_img, ext - - -class DoubanAlbumPatcher(DoubanPatcherMixin, AbstractScraper): - site_name = SourceSiteEnum.DOUBAN.value - host = 'music.douban.com' - data_class = Album - form_class = AlbumForm - - regex = re.compile(r"https://music\.douban\.com/subject/\d+/{0,1}") - - def scrape(self, url): - headers = DEFAULT_REQUEST_HEADERS.copy() - headers['Host'] = self.host - content = self.download_page(url, headers) - img_url_elem = content.xpath("//div[@id='mainpic']//img/@src") - img_url = img_url_elem[0].strip() if img_url_elem else None - raw_img, ext = self.download_image(img_url, url) - return raw_img, ext - - -class Command(BaseCommand): - help = 'fix cover image' - - def add_arguments(self, parser): - parser.add_argument('threadId', type=int, help='% 8') - - def handle(self, *args, **options): - t = int(options['threadId']) - for m in Album.objects.filter(cover='album/default.svg', source_site='douban'): - if m.id % 8 == t: - self.stdout.write(f'Re-fetching {m.source_url}') - try: - raw_img, img_ext = DoubanAlbumPatcher.scrape(m.source_url) - if img_ext is not None: - m.cover = SimpleUploadedFile('temp.' + img_ext, raw_img) - m.save() - self.stdout.write(self.style.SUCCESS(f'Saved {m.source_url}')) - else: - self.stdout.write(self.style.ERROR(f'Skipped {m.source_url}')) - except Exception as e: - print(e) diff --git a/music/models.py b/music/models.py deleted file mode 100644 index d7a9d252..00000000 --- a/music/models.py +++ /dev/null @@ -1,270 +0,0 @@ -import uuid -import django.contrib.postgres.fields as postgres -from django.utils.translation import gettext_lazy as _ -from django.db import models -from django.core.serializers.json import DjangoJSONEncoder -from django.shortcuts import reverse -from common.models import Entity, Mark, Review, Tag, SourceSiteEnum, MarkStatusEnum -from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath -from django.utils import timezone -from django.conf import settings -from simple_history.models import HistoricalRecords - - -MusicMarkStatusTranslation = { - MarkStatusEnum.DO.value: _("在听"), - MarkStatusEnum.WISH.value: _("想听"), - MarkStatusEnum.COLLECT.value: _("听过") -} - - -def song_cover_path(instance, filename): - return GenerateDateUUIDMediaFilePath(instance, filename, settings.SONG_MEDIA_PATH_ROOT) - - -def album_cover_path(instance, filename): - return GenerateDateUUIDMediaFilePath(instance, filename, settings.ALBUM_MEDIA_PATH_ROOT) - - -class Album(Entity): - title = models.CharField(_("标题"), max_length=500) - release_date = models.DateField( - _('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True) - cover = models.ImageField( - _("封面"), upload_to=album_cover_path, default=settings.DEFAULT_ALBUM_IMAGE, blank=True) - duration = models.PositiveIntegerField(_("时长"), null=True, blank=True) - artist = postgres.ArrayField( - models.CharField(_("artist"), blank=True, - default='', max_length=200), - null=True, - blank=True, - default=list, - verbose_name=_("艺术家") - ) - genre = models.CharField(_("流派"), blank=True, - default='', max_length=100) - company = postgres.ArrayField( - models.CharField(blank=True, - default='', max_length=500), - null=True, - blank=True, - default=list, - verbose_name=_("发行方") - ) - track_list = models.TextField(_("曲目"), blank=True, default="") - - history = HistoricalRecords() - - @property - def year(self): - return self.release_date.year if self.release_date else None - - def __str__(self): - return self.title - - def get_json(self): - r = { - 'artist': self.artist, - 'release_date': self.release_date, - 'genre': self.genre, - 'publisher': self.company, - } - r.update(super().get_json()) - return r - - def get_embed_link(self): - if self.source_site == SourceSiteEnum.SPOTIFY.value: - return self.source_url.replace("open.spotify.com/", "open.spotify.com/embed/") - elif self.source_site == SourceSiteEnum.BANDCAMP.value and self.other_info and 'bandcamp_album_id' in self.other_info: - return f"https://bandcamp.com/EmbeddedPlayer/album={self.other_info['bandcamp_album_id']}/size=large/bgcol=ffffff/linkcol=19A2CA/artwork=small/transparent=true/" - else: - return None - - def get_absolute_url(self): - return reverse("music:retrieve_album", args=[self.id]) - - @property - def wish_url(self): - return reverse("music:wish_album", args=[self.id]) - - def get_tags_manager(self): - return self.album_tags - - @property - def verbose_category_name(self): - return _("专辑") - - @property - def mark_class(self): - return AlbumMark - - @property - def tag_class(self): - return AlbumTag - - -class Song(Entity): - ''' - Song(track) entity, can point to entity Album - ''' - title = models.CharField(_("标题"), max_length=500) - release_date = models.DateField(_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True) - isrc = models.CharField(_("ISRC"), - blank=True, max_length=15, db_index=True, default='') - # duration in ms - duration = models.PositiveIntegerField(_("时长"), null=True, blank=True) - cover = models.ImageField( - _("封面"), upload_to=song_cover_path, default=settings.DEFAULT_SONG_IMAGE, blank=True) - artist = postgres.ArrayField( - models.CharField(blank=True, - default='', max_length=100), - null=True, - blank=True, - default=list, - verbose_name=_("艺术家") - ) - genre = models.CharField(_("流派"), blank=True, default='', max_length=100) - - album = models.ForeignKey( - Album, models.SET_NULL, "album_songs", null=True, blank=True, verbose_name=_("所属专辑")) - - history = HistoricalRecords() - - def __str__(self): - return self.title - - def get_json(self): - r = { - 'artist': self.artist, - 'release_date': self.release_date, - 'genre': self.genre, - } - r.update(super().get_json()) - return r - - def get_embed_link(self): - return self.source_url.replace("open.spotify.com/", "open.spotify.com/embed/") if self.source_site == SourceSiteEnum.SPOTIFY.value else None - - def get_absolute_url(self): - return reverse("music:retrieve_song", args=[self.id]) - - @property - def wish_url(self): - return reverse("music:wish_song", args=[self.id]) - - def get_tags_manager(self): - return self.song_tags - - @property - def verbose_category_name(self): - return _("单曲") - - @property - def mark_class(self): - return SongMark - - @property - def tag_class(self): - return SongTag - - -class SongMark(Mark): - song = models.ForeignKey( - Song, on_delete=models.CASCADE, related_name='song_marks', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'song'], name='unique_song_mark') - ] - - @property - def translated_status(self): - return MusicMarkStatusTranslation[self.status] - - -class SongReview(Review): - song = models.ForeignKey( - Song, on_delete=models.CASCADE, related_name='song_reviews', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'song'], name='unique_song_review') - ] - - @property - def url(self): - return reverse("music:retrieve_song_review", args=[self.id]) - - @property - def item(self): - return self.song - - -class SongTag(Tag): - song = models.ForeignKey( - Song, on_delete=models.CASCADE, related_name='song_tags', null=True) - mark = models.ForeignKey( - SongMark, on_delete=models.CASCADE, related_name='songmark_tags', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['content', 'mark'], name="unique_songmark_tag") - ] - - @property - def item(self): - return self.song - - -class AlbumMark(Mark): - album = models.ForeignKey( - Album, on_delete=models.CASCADE, related_name='album_marks', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'album'], name='unique_album_mark') - ] - - @property - def translated_status(self): - return MusicMarkStatusTranslation[self.status] - - -class AlbumReview(Review): - album = models.ForeignKey( - Album, on_delete=models.CASCADE, related_name='album_reviews', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['owner', 'album'], name='unique_album_review') - ] - - @property - def url(self): - return reverse("music:retrieve_album_review", args=[self.id]) - - @property - def item(self): - return self.album - - -class AlbumTag(Tag): - album = models.ForeignKey( - Album, on_delete=models.CASCADE, related_name='album_tags', null=True) - mark = models.ForeignKey( - AlbumMark, on_delete=models.CASCADE, related_name='albummark_tags', null=True) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['content', 'mark'], name="unique_albummark_tag") - ] - - @property - def item(self): - return self.album diff --git a/music/templates/music/album_detail.html b/music/templates/music/album_detail.html deleted file mode 100644 index 5a83672a..00000000 --- a/music/templates/music/album_detail.html +++ /dev/null @@ -1,457 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load strip_scheme %} -{% load thumb %} - - - - - - - - - - - - - - {{ site_name }} - {% trans '音乐详情' %} | {{ album.title }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - - {{ album.title }} - - -
-
- - {{ album.title }} - {{ album.get_source_site_display }} -
- -
-
- {% if album.rating and album.rating_number >= 5 %} - - {{ album.rating }} - ({{ album.rating_number }}人评分) - {% else %} - {% trans '评分:评分人数不足' %} - {% endif %} -
-
{% if album.artist %}{% trans '艺术家:' %} - {% for artist in album.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if album.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if album.company %}{% trans '发行方:' %} - {% for company in album.company %} - 5 %}style="display: none;" {% endif %}> - {{ company }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if album.company|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if album.release_date %} - {% trans '发行日期:' %}{{ album.release_date }} - {% endif %} -
-
{% if album.duration %} - {% trans '时长:' %}{{ album.get_duration_display }} - {% endif %} -
-
{% if album.genre %} - {% trans '流派:' %}{{ album.genre }} - {% endif %} -
- - -
-
- {% if album.other_info %} - {% for k, v in album.other_info.items %} -
- {{ k }}:{{ v | urlize }} -
- {% endfor %} - {% endif %} - - - {% if album.last_editor and album.last_editor.preference.show_last_edit or user.is_staff %} -
{% trans '最近编辑者:' %}{{ album.last_editor | default:"" }}
- {% endif %} - -
- {% trans '编辑这个作品' %} - {% if user.is_staff %} - / {% trans '删除' %} - {% endif %} -
-
- -
- - {% for tag_dict in album_tag_list %} - {% for k, v in tag_dict.items %} - {% if k == 'content' %} - - {{ v }} - - {% endif %} - {% endfor %} - {% endfor %} - -
-
-
-
- {% if album.brief %} -
-
{% trans '简介' %}
- -

{{ album.brief | linebreaksbr }}

- -
- {% endif %} - - {% if album.track_list %} -
-
{% trans '曲目' %}
-

{{ album.track_list | linebreaksbr }}

- -
- {% endif %} - - {% if album.album_songs.count %} -
-
{% trans '关联单曲' %}
- - - -
- {% endif %} - - -
- -
{% trans '这部作品的标记' %}
- {% if mark_list_more %} - {% trans '全部标记' %} - {% endif %} - 关注的人的标记 - {% include "partial/mark_list.html" with mark_list=mark_list current_item=album %} -
-
-
{% trans '这部作品的评论' %}
- - {% if review_list_more %} - {% trans '全部评论' %} - {% endif %} - {% if review_list %} - - {% else %} -
{% trans '暂无评论' %}
- {% endif %} -
-
-
- -
-
- - {% if mark %} -
- - {% trans '我' %}{{ mark.get_status_display }} - {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} - {% if mark.rating %} - - {% endif %} - {% endif %} - {% if mark.visibility > 0 %} - - {% endif %} - - {% trans '修改' %} -
- {% csrf_token %} - {% trans '删除' %} -
-
-
- -
{{ mark.created_time }}
- - {% if mark.text %} -

{{ mark.text }}

- {% endif %} -
- - {% for tag in mark_tags %} - {{ tag }} - {% endfor %} - -
-
- {% else %} -
-
{% trans '标记这部作品' %}
-
- - - -
-
- {% endif %} - -
- -
- {% if review %} -
- - {% trans '我的评论' %} - {% if review.visibility > 0 %} - - {% endif %} - - - {% trans '编辑' %} - {% trans '删除' %} - - -
{{ review.edited_time }}
- - - {{ review.title }} - -
- {% else %} - -
-
{% trans '我的评论' %}
- -
- - {% endif %} -
- - {% if collection_list %} -
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %} -

- {{ c.title }} -

- {% endfor %} -
- -
-
-
-
- {% endif %} - - {% if album.get_embed_link %} - - {% endif %} -
-
-
- -
- {% include "partial/_footer.html" %} -
- -
- - - -
-
- - - - - - diff --git a/music/templates/music/album_mark_list.html b/music/templates/music/album_mark_list.html deleted file mode 100644 index afc59663..00000000 --- a/music/templates/music/album_mark_list.html +++ /dev/null @@ -1,133 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ album.title }}{% trans '的标记' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ album.title }}{% trans '的标记' %} -
- {% include "partial/mark_list.html" with mark_list=marks current_item=album %} -
- -
-
- -
-
-
-
- -
-
-
- {{ album.title }} - - - {{ album.get_source_site_display }} -
- -
{% if album.artist %}{% trans '艺术家:' %} - {% for artist in album.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if album.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
- -
{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
- {% if album.rating %} - {% trans '评分: ' %} - {{ album.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/album_review_detail.html b/music/templates/music/album_review_detail.html deleted file mode 100644 index 33ca6f28..00000000 --- a/music/templates/music/album_review_detail.html +++ /dev/null @@ -1,151 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - {{ site_name }}乐评 - {{ review.title }} - - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- {% if request.user == review.owner %} - {% trans '编辑' %} - {% trans '删除' %} - {% endif %} -
-
- -
- {{ form.content }} -
- {{ form.media }} - {% csrf_token %} -
-
- -
-
-
-
-
- -
-
-
- {{ album.title }} - - - - {{ album.get_source_site_display }} - - -
- -
{% if album.artist %}{% trans '艺术家:' %} - {% for artist in album.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if album.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
- -
{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
- {% if album.rating %} - {% trans '评分: ' %} - {{ album.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/album_review_list.html b/music/templates/music/album_review_list.html deleted file mode 100644 index 88035e96..00000000 --- a/music/templates/music/album_review_list.html +++ /dev/null @@ -1,160 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ album.title }}{% trans '的评论' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ album.title }}{% trans '的评论' %} -
-
    - - {% for review in reviews %} - -
  • - - {{ review.owner.username }} - {% if review.visibility > 0 %} - - - - {% endif %} - {{ review.edited_time }} - - - {{ review.title - }} - -
  • - {% empty %} -
    {% trans '无结果' %}
    - {% endfor %} - -
-
- -
-
- -
-
-
-
- -
-
-
- {{ album.title }} - - - {{ album.get_source_site_display }} -
- -
{% if album.artist %}{% trans '艺术家:' %} - {% for artist in album.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if album.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
- -
{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
- {% if album.rating %} - {% trans '评分: ' %} - {{ album.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/create_update_album.html b/music/templates/music/create_update_album.html deleted file mode 100644 index cceb04d9..00000000 --- a/music/templates/music/create_update_album.html +++ /dev/null @@ -1,103 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {{ title }} - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
- {% if is_update and form.source_site.value != 'in-site' %} -
-
-
-
{% trans '源网站' %}: {{ form.source_site.value }}
-
-
- {% csrf_token %} - -
-
-
-
-
- {% endif %} - -
- {% comment %} {% trans '>>> 试试一键剽取~ <<<' %} {% endcomment %} -
- {% csrf_token %} - {{ form.media }} - {% for field in form %} - {% if field.name == 'release_date' %} - {{ field.label_tag }} - - - - {% else %} - {% if field.name != 'id' %} - {{ field.label_tag }} - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - - -
-
- -
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/create_update_album_review.html b/music/templates/music/create_update_album_review.html deleted file mode 100644 index fab74aac..00000000 --- a/music/templates/music/create_update_album_review.html +++ /dev/null @@ -1,126 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ title }} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - -
-
- -
- {{ album.title }} - - {{ album.get_source_site_display }} -
-
{% if album.artist %}{% trans '艺术家:' %} - {% for artist in album.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if album.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if album.genre %}{% trans '流派:' %}{{ album.genre }}{% endif %}
- -
{% if album.release_date %}{% trans '发行日期:' %}{{ album.release_date}}{% endif %}
- - {% if album.rating %} - {% trans '评分:' %} - {{ album.rating }} - {% endif %} -
-
-
- -
- {% csrf_token %} - {{ form.album }} -
- {{ form.title.label }} -
- {{ form.title }} -
- - {{ form.content.label }} - - - {% trans '预览' %} - -
-
- {{ form.content }} -
-
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
-
-
- - {{ form.visibility.label }}{{ form.visibility }} -
- -
-
- -
- {{ form.media }} -
- -
- -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - diff --git a/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html deleted file mode 100644 index 9338ea29..00000000 --- a/music/templates/music/create_update_song.html +++ /dev/null @@ -1,91 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {{ title }} - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
- - {% comment %} - {% trans '>>> 试试一键剽取~ <<<' %} - - {% endcomment %} - -
- {% csrf_token %} - {{ form.media }} - {% for field in form %} - {% if field.name == 'release_date' %} - {{ field.label_tag }} - - {% else %} - {% if field.name != 'id' %} - {{ field.label_tag }} - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - - -
-
- -
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/create_update_song_review.html b/music/templates/music/create_update_song_review.html deleted file mode 100644 index a26ecfd1..00000000 --- a/music/templates/music/create_update_song_review.html +++ /dev/null @@ -1,130 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ title }} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - -
-
- -
- {{ song.title }} - - {{ song.get_source_site_display }} -
-
{% if song.artist %}{% trans '艺术家:' %} - {% for artist in song.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if song.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
-
{% if song.album %}{% trans '所属专辑:' %} - {{ song.album }} - {% endif %} -
- -
{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
- - {% if song.rating %} - {% trans '评分:' %} - {{ song.rating }} - {% endif %} -
-
-
- -
- {% csrf_token %} - {{ form.song }} -
- {{ form.title.label }} -
- {{ form.title }} -
- - {{ form.content.label }} - - - {% trans '预览' %} - -
-
- {{ form.content }} -
-
{% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
-
-
- - {{ form.visibility.label }}{{ form.visibility }} -
- -
-
- -
- {{ form.media }} -
- -
- -
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - diff --git a/music/templates/music/delete_album.html b/music/templates/music/delete_album.html deleted file mode 100644 index fa7929a9..00000000 --- a/music/templates/music/delete_album.html +++ /dev/null @@ -1,99 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {% trans '删除音乐' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}
- -
-
- - - -
-
- -
- {{ album.title }} - {{ album.get_source_site_display }} -
- - {% if album.rating %} - {% trans '评分:' %} - - {{ album.rating }} - {% else %} - {% trans '评分:暂无评分' %} - {% endif %} - - {% if album.last_editor %} -
- {% trans '最近编辑者:' %} - - {{ album.last_editor | default:"" }} - -
- {% endif %} - -
{% trans '上次编辑时间:' %}{{ album.edited_time }}
- - {% if album.album_marks.all %} -
{% trans '这个条目有' %} {{ album.album_marks.count }} 个标记
- {% endif %} - {% if album.album_reviews.all %} -
{% trans '这个条目有' %} {{ album.album_reviews.count }} 个评论
- {% endif %} - -
-
-
-
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html deleted file mode 100644 index 1c2cf8b5..00000000 --- a/music/templates/music/delete_album_review.html +++ /dev/null @@ -1,101 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '删除评论' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这篇评论吗?' %}
- -
- - -
- -
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} - -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- - -
-
- {{ form.content }} -
- {{ form.media }} - -
- -
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/delete_song.html b/music/templates/music/delete_song.html deleted file mode 100644 index 5fc7a2bc..00000000 --- a/music/templates/music/delete_song.html +++ /dev/null @@ -1,99 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {% trans '删除音乐' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这个作品吗?相关评论和标记将一并删除。' %}
- -
-
- - - -
-
- -
- {{ song.title }} - {{ song.get_source_site_display }} -
- - {% if song.rating %} - {% trans '评分:' %} - - {{ song.rating }} - {% else %} - {% trans '评分:暂无评分' %} - {% endif %} - - {% if song.last_editor %} -
- {% trans '最近编辑者:' %} - - {{ song.last_editor | default:"" }} - -
- {% endif %} - -
{% trans '上次编辑时间:' %}{{ song.edited_time }}
- - {% if song.song_marks.all %} -
{% trans '这个条目有' %} {{ song.song_marks.count }} 个标记
- {% endif %} - {% if song.song_reviews.all %} -
{% trans '这个条目有' %} {{ song.song_reviews.count }} 个评论
- {% endif %} - -
-
-
-
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html deleted file mode 100644 index 3b983290..00000000 --- a/music/templates/music/delete_song_review.html +++ /dev/null @@ -1,101 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '删除评论' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
{% trans '确认删除这篇评论吗?' %}
- -
- - -
- -
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} - -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- - -
-
- {{ form.content }} -
- {{ form.media }} - -
- -
-
- {% csrf_token %} - -
- -
-
-
-
- -
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html deleted file mode 100644 index c58faf7a..00000000 --- a/music/templates/music/scrape_album.html +++ /dev/null @@ -1,112 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '从豆瓣获取数据' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {% trans '根据豆瓣内容填写下方表单' %} -
- -
-
-
- {% csrf_token %} - - {{ form.media }} - - {% for field in form %} - - {% if field.name == 'release_date' %} - {{ field.label_tag }} - - - - {% else %} - {% if field.name != 'id' %} - {{ field.label_tag }} - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - -
- {% trans '剽取!' %} -
-
-
-
- -
-
-
- {% trans '复制详情页链接' %} -
-
- {% csrf_token %} - - -
-
- -
-
-
- -
- {% include "partial/_footer.html" %} - -
- - - - - diff --git a/music/templates/music/scrape_song.html b/music/templates/music/scrape_song.html deleted file mode 100644 index 83557681..00000000 --- a/music/templates/music/scrape_song.html +++ /dev/null @@ -1,109 +0,0 @@ -{% load static %} -{% load i18n %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} - - - - - - - {{ site_name }} - {% trans '从豆瓣获取数据' %} - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {% trans '根据豆瓣内容填写下方表单' %} -
- -
-
-
- {% csrf_token %} - - {{ form.media }} - - {% for field in form %} - - {% if field.id_for_label == 'id_is_series' %} - - {{ field }} - {% else %} - {% if field.id_for_label != 'id_id' %} - - {% endif %} - {{ field }} - {% endif %} - - {% endfor %} - -
- {% trans '剽取!' %} -
-
-
-
- -
-
-
- {% trans '复制详情页链接' %} -
-
- {% csrf_token %} - - -
-
- -
-
-
- -
- {% include "partial/_footer.html" %} - -
- - - - - diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html deleted file mode 100644 index e66cce68..00000000 --- a/music/templates/music/song_detail.html +++ /dev/null @@ -1,403 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load strip_scheme %} -{% load thumb %} - - - - - - - - - - - - - - {{ site_name }} - {% trans '音乐详情' %} | {{ song.title }} - - {% include "partial/_common_libs.html" with jquery=1 %} - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
- - - {{ song.title }} - - -
-
- - {{ song.title }} - {{ song.get_source_site_display }} -
- -
-
- {% if song.rating and song.rating_number >= 5 %} - - {{ song.rating }} - ({{ song.rating_number }}人评分) - {% else %} - {% trans '评分:评分人数不足' %} - {% endif %} -
-
{% if song.artist %}{% trans '艺术家:' %} - {% for artist in song.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if song.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %}
-
{% if song.release_date %} - {% trans '发行日期:' %}{{ song.release_date }} - {% endif %} -
-
{% if song.duration %} - {% trans '时长:' %}{{ song.get_duration_display }} - {% endif %} -
-
{% if song.genre %} - {% trans '流派:' %}{{ song.genre }} - {% endif %} -
- - -
-
-
{% if song.isrc %} - {% trans 'ISRC:' %}{{ song.isrc }} - {% endif %} -
-
{% if song.album %} - {% trans '所属专辑:' %}{{ song.album }} - {% endif %} -
- {% if song.other_info %} - {% for k, v in song.other_info.items %} -
- {{ k }}:{{ v | urlize }} -
- {% endfor %} - {% endif %} - - - {% if song.last_editor and song.last_editor.preference.show_last_edit or user.is_staff %} -
{% trans '最近编辑者:' %}{{ song.last_editor | default:"" }}
- {% endif %} - -
- {% trans '编辑这个作品' %} - {% if user.is_staff %} - / {% trans '删除' %} - {% endif %} -
-
- -
- - {% for tag_dict in song_tag_list %} - {% for k, v in tag_dict.items %} - {% if k == 'content' %} - - {{ v }} - - {% endif %} - {% endfor %} - {% endfor %} - -
-
-
-
- {% if song.brief %} -
-
{% trans '简介' %}
- -

{{ song.brief | linebreaksbr }}

- - - -
- {% endif %} - - -
- -
{% trans '这部作品的标记' %}
- {% if mark_list_more %} - {% trans '全部标记' %} - {% endif %} - 关注的人的标记 - {% include "partial/mark_list.html" with mark_list=mark_list current_item=song %} -
-
-
{% trans '这部作品的评论' %}
- - {% if review_list_more %} - {% trans '全部评论' %} - {% endif %} - {% if review_list %} - - {% else %} -
{% trans '暂无评论' %}
- {% endif %} -
-
-
- -
-
- - {% if mark %} -
- - {% trans '我' %}{{ mark.get_status_display }} - {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} - {% if mark.rating %} - - {% endif %} - {% endif %} - {% if mark.visibility > 0 %} - - {% endif %} - - {% trans '修改' %} -
- {% csrf_token %} - {% trans '删除' %} -
-
-
- -
{{ mark.created_time }}
- - {% if mark.text %} -

{{ mark.text }}

- {% endif %} -
- - {% for tag in mark_tags %} - {{ tag }} - {% endfor %} - -
-
- {% else %} -
-
{% trans '标记这部作品' %}
-
- - - -
-
- {% endif %} - -
- -
- {% if review %} -
- - {% trans '我的评论' %} - {% if review.visibility > 0 %} - - {% endif %} - - - {% trans '编辑' %} - {% trans '删除' %} - - -
{{ review.edited_time }}
- - - {{ review.title }} - -
- {% else %} - -
-
{% trans '我的评论' %}
- -
- - {% endif %} -
- - {% if collection_list %} -
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %} -

- {{ c.title }} -

- {% endfor %} -
- -
-
-
-
- {% endif %} - - {% if song.source_site == "spotify" %} - - {% endif %} -
-
-
- -
- {% include "partial/_footer.html" %} -
- -
- - - -
-
- - - - - - diff --git a/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html deleted file mode 100644 index 16499d75..00000000 --- a/music/templates/music/song_mark_list.html +++ /dev/null @@ -1,137 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load highlight %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ song.title }}{% trans '的标记' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ song.title }}{% trans '的标记' %} -
- {% include "partial/mark_list.html" with mark_list=marks current_item=song %} -
- -
-
- -
-
-
-
- -
-
-
- {{ song.title }} - - {{song.get_source_site_display }} -
- -
{% if song.artist %}{% trans '艺术家:' %} - {% for artist in song.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if song.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
-
{% if song.album %}{% trans '所属专辑:' %} - {{ song.album }} - {% endif %} -
- -
{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %} -
- {% if song.rating %} - {% trans '评分: ' %} - {{ song.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - \ No newline at end of file diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html deleted file mode 100644 index ea3608ee..00000000 --- a/music/templates/music/song_review_detail.html +++ /dev/null @@ -1,147 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - - - - - - {{ site_name }}乐评 - {{ review.title }} - - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ review.title }} -
- {% if review.visibility > 0 %} - - - - {% endif %} -
-
- - {{ review.owner.username }} - - {% if mark %} - - {% if mark.rating %} - - {% endif %} - - {% endif %} - - {{ review.edited_time }} - -
-
- {% if request.user == review.owner %} - {% trans '编辑' %} - {% trans '删除' %} - {% endif %} -
-
- -
- {{ form.content }} -
- {{ form.media }} - {% csrf_token %} -
-
- -
-
-
-
-
- -
-
-
- {{ song.title }} - - {{ song.get_source_site_display }} -
- -
{% if song.artist %}{% trans '艺术家:' %} - {% for artist in song.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if song.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
-
{% if song.album %}{% trans '所属专辑:' %} - {{ song.album }} - {% endif %} -
- -
{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
- {% if song.rating %} - {% trans '评分: ' %} - {{ song.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html deleted file mode 100644 index bc59bd58..00000000 --- a/music/templates/music/song_review_list.html +++ /dev/null @@ -1,152 +0,0 @@ -{% load static %} -{% load i18n %} -{% load l10n %} -{% load humanize %} -{% load admin_url %} -{% load mastodon %} -{% load oauth_token %} -{% load truncate %} -{% load thumb %} - - - - - - - {{ site_name }} - {{ song.title }}{% trans '的评论' %} - - - - - - - - -
-
- {% include "partial/_navbar.html" %} - -
-
-
-
-
-
- {{ song.title }}{% trans ' 的评论' %} -
-
    - - {% for review in reviews %} - -
  • - - {{ review.owner.username }} - {% if review.visibility > 0 %} - - {% endif %} - {{ review.edited_time }} - - - {{ review.title }} - -
  • - {% empty %} -
    {% trans '无结果' %}
    - {% endfor %} - -
-
- -
-
- -
-
-
-
- -
-
-
- {{ song.title }} - - {{ song.get_source_site_display }} -
- -
{% if song.artist %}{% trans '艺术家:' %} - {% for artist in song.artist %} - 5 %}style="display: none;" {% endif %}> - {{ artist }} - {% if not forloop.last %} / {% endif %} - - {% endfor %} - {% if song.artist|length > 5 %} - {% trans '更多' %} - - {% endif %} - {% endif %} -
-
{% if song.genre %}{% trans '流派:' %}{{ song.genre }}{% endif %}
-
{% if song.album %}{% trans '所属专辑:' %} - {{ song.album }} - {% endif %} -
- -
{% if song.release_date %}{% trans '发行日期:' %}{{ song.release_date }}{% endif %}
- {% if song.rating %} - {% trans '评分: ' %} - {{ song.rating }} - {% endif %} -
- -
-
-
-
-
-
- {% include "partial/_footer.html" %} -
- - - - - - - - diff --git a/music/tests.py b/music/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/music/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/music/urls.py b/music/urls.py deleted file mode 100644 index 09b180ca..00000000 --- a/music/urls.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.urls import path, re_path -from .views import * - - -app_name = 'music' -urlpatterns = [ - path('song/create/', create_song, name='create_song'), - path('song//', retrieve_song, name='retrieve_song'), - path('song/update//', update_song, name='update_song'), - path('song/delete//', delete_song, name='delete_song'), - path('song/mark/', create_update_song_mark, name='create_update_song_mark'), - path('song/wish//', wish_song, name='wish_song'), - path('song//mark/list/', - retrieve_song_mark_list, name='retrieve_song_mark_list'), - path('song/mark/delete//', delete_song_mark, name='delete_song_mark'), - path('song//review/create/', create_song_review, name='create_song_review'), - path('song/review/update//', update_song_review, name='update_song_review'), - path('song/review/delete//', delete_song_review, name='delete_song_review'), - path('song/review//', retrieve_song_review, name='retrieve_song_review'), - re_path('song/(?P[0-9]+)/mark/list/(?:(?P\\d+))?', retrieve_song_mark_list, name='retrieve_song_mark_list'), -# path('song/scrape/', scrape_song, name='scrape_song'), - path('song/click_to_scrape/', click_to_scrape_song, name='click_to_scrape_song'), - - path('album/create/', create_album, name='create_album'), - path('album//', retrieve_album, name='retrieve_album'), - path('album/update//', update_album, name='update_album'), - path('album/delete//', delete_album, name='delete_album'), - path('rescrape//', rescrape, name='rescrape'), - path('album/mark/', create_update_album_mark, name='create_update_album_mark'), - path('album/wish//', wish_album, name='wish_album'), - re_path('album/(?P[0-9]+)/mark/list/(?:(?P\\d+))?', retrieve_album_mark_list, name='retrieve_album_mark_list'), - path('album/mark/delete//', delete_album_mark, name='delete_album_mark'), - path('album//review/create/', create_album_review, name='create_album_review'), - path('album/review/update//', update_album_review, name='update_album_review'), - path('album/review/delete//', delete_album_review, name='delete_album_review'), - path('album/review//', retrieve_album_review, name='retrieve_album_review'), - path('album//review/list/', - retrieve_album_review_list, name='retrieve_album_review_list'), - path('album/scrape/', scrape_album, name='scrape_album'), - path('album/click_to_scrape/', click_to_scrape_album, name='click_to_scrape_album'), -] diff --git a/music/views.py b/music/views.py deleted file mode 100644 index 190c1599..00000000 --- a/music/views.py +++ /dev/null @@ -1,1154 +0,0 @@ -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] - all_collections = CollectionItem.objects.filter(song=song).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20] - collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections)) - - # 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/') - 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': request.user.preference.default_visibility, - '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] - all_collections = CollectionItem.objects.filter(album=album).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20] - collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections)) - - # 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/') - 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': request.user.preference.default_visibility, - '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()