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' %}
-
- {% endif %}
-
-
- {% comment %}
{% trans '>>> 试试一键剽取~ <<<' %} {% endcomment %}
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
{% 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 %}
-
-
-
-
-
-
-
-
-
-
-
- {% 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 '确认删除这本书吗?相关评论和标记将一并删除。' %}
-
-
-
-
-
-
-
- {% if book.rating %}
- {% trans '评分:' %}
-
-
{{ book.rating }}
- {% else %}
-
{% trans '评分:暂无评分' %}
- {% endif %}
-
- {% if book.last_editor %}
-
- {% endif %}
-
-
{% trans '上次编辑时间:' %}{{ book.edited_time }}
-
- {% if book.book_marks.all %}
-
- {% endif %}
- {% if book.book_reviews.all %}
-
- {% endif %}
-
-
-
-
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 }}
-
-
-
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 %}
-
- {% 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 '修改' %}
-
-
-
-
-
{{ mark.created_time }}
-
- {% if mark.text %}
-
{{ mark.text }}
- {% endif %}
-
-
- {% for tag in mark_tags %}
- {{ tag }}
- {% endfor %}
-
-
-
- {% else %}
-
-
{% trans '标记这本书' %}
-
- {% trans '想读' %}
- {% trans '在读' %}
- {% trans '读过' %}
-
-
- {% endif %}
-
-
-
- {% if review %}
-
- {% else %}
-
-
-
- {% 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 %}
-
- {% trans '添加到收藏单' %}
-
-
-
-
- {% endif %}
-
-
-
-
-
- {% include "partial/_footer.html" %}
-
-
-
-
-
- {% if not mark %}
-
-
- {% else %}
-
{% trans '我的标记' %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% trans '确定要删除你的标记吗?' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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" %}
-
-
-
-
-
-
-
- {% include "partial/mark_list.html" with mark_list=marks current_item=book %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 }}
-
-
-
-
-
-
- {{ form.content }}
-
- {{ form.media }}
- {% csrf_token %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 '根据豆瓣内容填写下方表单' %}
-
-
-
-
- {% trans '解析器:' %}
-
-
-
-
-
-
-
-
-
-
-
- {% trans '复制详情页链接' %}
-
-
-
-
-
-
-
-
-
- {% 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 %}
-
-
-
-
-
-
{% trans '添加到收藏单' %}
-
-
-
-
-
-
-
-
-
-
-
-
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 %}
-
-
- {{ form.description }}
-
- {{ form.media }}
-
-
-
- {% csrf_token %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
-
-
-
-
- {% 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 %}
-
-
-
-
- {{ form.description }}
-
- {{ form.media }}
-
-
-
-
-
-
-
-
-
-
-
- {% if follower_count %}
- 被 {{ follower_count }} 人关注
- {% endif %}
-
-
-
-
- {% if request.user != collection.owner %}
-
-
-
- {% if following %}
-
- {% csrf_token %}
- {% trans '取消关注' %}
-
- {% else %}
-
- {% csrf_token %}
- {% trans '关注' %}
-
- {% endif %}
-
-
-
- {% endif %}
-
-
-
-
-
- {% trans '分享到联邦网络' %}
-
-
-
-
-
-
-
-
- {% 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 %}
-
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 %}
-
-
-
-
-
-
{% trans '分享收藏单' %}
-
-
-
-
-
-
-
-
-
-
-
- {% csrf_token %}
-
-
分享可见性(不同于收藏单本身的权限):
-
-
-
-
-
-
-
-
-
-
-
-
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' %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
-
-
-
-
- {% endif %}
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
- {% 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 }}
-
-
-
-
-
-
- {{ 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 '确认删除这个游戏吗?相关评论和标记将一并删除。' %}
-
-
-
-
-
-
-
- {% if game.rating %}
- {% trans '评分:' %}
-
-
{{ game.rating }}
- {% else %}
-
{% trans '评分:暂无评分' %}
- {% endif %}
-
- {% if game.last_editor %}
-
- {% endif %}
-
-
{% trans '上次编辑时间:' %}{{ game.edited_time }}
-
- {% if game.game_marks.all %}
-
- {% endif %}
- {% if game.game_reviews.all %}
-
- {% endif %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 %}
-
- {% 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 '标记这个游戏' %}
-
- {% trans '想玩' %}
- {% trans '在玩' %}
- {% trans '玩过' %}
-
-
- {% endif %}
-
-
-
-
- {% if review %}
-
- {% else %}
-
-
-
- {% endif %}
-
-
- {% if collection_list %}
-
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %}
-
- {{ c.title }}
-
- {% endfor %}
-
- {% trans '添加到收藏单' %}
-
-
-
-
- {% endif %}
-
-
-
-
-
-
- {% include "partial/_footer.html" %}
-
-
-
-
-
-
- {% if not mark %}
-
-
-
-
- {% else %}
-
{% trans '我的标记' %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ mark_form.media }}
- {% csrf_token %}
- {{ mark_form.id }}
- {{ mark_form.game }}
- {% if mark.rating %}
- {% endif %}
-
-
- {{ mark_form.rating }}
-
- {{ mark_form.status }}
-
-
-
-
- {{ mark_form.text }}
-
-
- {{ mark_form.tags.label }}
- {{ mark_form.tags }}
-
-
-
-
- {{ mark_form.visibility.label }}:
- {{ mark_form.visibility }}
-
-
- {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
-
-
-
-
-
-
-
-
-
-
-
-
{% trans '确定要删除你的标记吗?' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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" %}
-
-
-
-
-
-
-
- {% include "partial/mark_list.html" with mark_list=marks current_item=game %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 }}
-
-
-
-
-
-
- {{ form.content }}
-
- {{ form.media }}
- {% csrf_token %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
- {% for review in reviews %}
-
-
-
- {{ review.owner.username }}
- {% if review.visibility > 0 %}
-
- {% endif %}
- {{ review.edited_time }}
-
-
- {{ review.title }}
-
-
- {% empty %}
- {% trans '无结果' %}
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 '根据豆瓣内容填写下方表单' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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' %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
-
-
-
-
- {% endif %}
-
-
- {% comment %}
{% trans '>>> 试试一键剽取~ <<<' %} {% endcomment %}
-
- {% csrf_token %}
- {{ form.media }}
- {% for field in form %}
-
- {% if field.id_for_label == 'id_is_series' %}
- {{ field.label }}
- {{ field }}
- {% else %}
- {% if field.id_for_label != 'id_id' %}
- {{ field.label }}
- {% 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.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 }}
-
-
-
-
-
-
- {{ 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 '确认删除这部电影/剧集吗?相关评论和标记将一并删除。' %}
-
-
-
-
-
- {% csrf_token %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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.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.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 %}
-
- {% 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 %}
-
- {% trans '想看' %}
- {% trans '在看' %}
- {% trans '看过' %}
-
-
- {% endif %}
-
-
-
-
- {% if review %}
-
- {% else %}
-
-
-
- {% 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 %}
-
- {% trans '添加到收藏单' %}
-
-
-
-
- {% endif %}
-
-
-
-
-
- {% include "partial/_footer.html" %}
-
-
-
-
-
-
- {% if not mark %}
-
- {% if movie.is_series %}
-
- {% else %}
-
- {% endif %}
-
-
- {% else %}
-
{% trans '我的标记' %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ mark_form.media }}
- {% csrf_token %}
- {{ mark_form.id }}
- {{ mark_form.movie }}
- {% if mark.rating %}
- {% endif %}
-
-
- {{ mark_form.rating }}
-
- {{ mark_form.status }}
-
-
-
-
- {{ mark_form.text }}
-
-
- {{ mark_form.tags.label }}
- {{ mark_form.tags }}
-
-
-
-
- {{ mark_form.visibility.label }}:
- {{ mark_form.visibility }}
-
-
- {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
-
-
-
-
-
-
-
-
-
-
-
-
{% trans '确定要删除你的标记吗?' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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" %}
-
-
-
-
-
-
-
- {% include "partial/mark_list.html" with mark_list=marks current_item=movie %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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 }}
-
-
-
-
-
-
- {{ form.content }}
-
- {{ form.media }}
- {% csrf_token %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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 '根据豆瓣内容填写下方表单' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 %}
-
- {% 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 '关联单曲' %}
-
-
-
- {% for song in album.album_songs.all %}
-
- {% endfor %}
-
-
-
-
-
- {% 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 '标记这部作品' %}
-
- {% trans '想听' %}
- {% trans '在听' %}
- {% trans '听过' %}
-
-
- {% endif %}
-
-
-
-
- {% if review %}
-
- {% else %}
-
-
-
- {% endif %}
-
-
- {% if collection_list %}
-
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %}
-
- {{ c.title }}
-
- {% endfor %}
-
- {% trans '添加到收藏单' %}
-
-
-
-
- {% endif %}
-
- {% if album.get_embed_link %}
-
- {% endif %}
-
-
-
-
-
- {% include "partial/_footer.html" %}
-
-
-
-
-
-
- {% if not mark %}
-
-
-
-
- {% else %}
-
{% trans '我的标记' %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ mark_form.media }}
- {% csrf_token %}
- {{ mark_form.id }}
- {{ mark_form.album }}
- {% if mark.rating %}
- {% endif %}
-
-
- {{ mark_form.rating }}
-
- {{ mark_form.status }}
-
-
-
-
- {{ mark_form.text }}
-
-
- {{ mark_form.tags.label }}
- {{ mark_form.tags }}
-
-
-
-
- {{ mark_form.visibility.label }}:
- {{ mark_form.visibility }}
-
-
- {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
-
-
-
-
-
-
-
-
-
-
-
-
{% trans '确定要删除你的标记吗?' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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" %}
-
-
-
-
-
-
-
- {% include "partial/mark_list.html" with mark_list=marks current_item=album %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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 }}
-
-
-
-
-
-
- {{ form.content }}
-
- {{ form.media }}
- {% csrf_token %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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' %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
-
-
-
-
- {% endif %}
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
{% 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 }}
-
-
-
-
-
-
- {{ 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" %}
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
{% 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.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 }}
-
-
-
-
-
-
- {{ 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 '确认删除这个作品吗?相关评论和标记将一并删除。' %}
-
-
-
-
-
-
-
- {% if album.rating %}
- {% trans '评分:' %}
-
-
{{ album.rating }}
- {% else %}
-
{% trans '评分:暂无评分' %}
- {% endif %}
-
- {% if album.last_editor %}
-
- {% endif %}
-
-
{% trans '上次编辑时间:' %}{{ album.edited_time }}
-
- {% if album.album_marks.all %}
-
- {% endif %}
- {% if album.album_reviews.all %}
-
- {% endif %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 '确认删除这个作品吗?相关评论和标记将一并删除。' %}
-
-
-
-
-
-
-
- {% if song.rating %}
- {% trans '评分:' %}
-
-
{{ song.rating }}
- {% else %}
-
{% trans '评分:暂无评分' %}
- {% endif %}
-
- {% if song.last_editor %}
-
- {% endif %}
-
-
{% trans '上次编辑时间:' %}{{ song.edited_time }}
-
- {% if song.song_marks.all %}
-
- {% endif %}
- {% if song.song_reviews.all %}
-
- {% endif %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 %}
-
-
- {% trans '返回' %}
-
-
-
-
-
-
- {% 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 '根据豆瓣内容填写下方表单' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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 '根据豆瓣内容填写下方表单' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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" %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% 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.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 %}
-
- {% 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 '标记这部作品' %}
-
- {% trans '想听' %}
- {% trans '在听' %}
- {% trans '听过' %}
-
-
- {% endif %}
-
-
-
-
- {% if review %}
-
- {% else %}
-
-
-
- {% endif %}
-
-
- {% if collection_list %}
-
-
-
{% trans '相关收藏单' %}
-
- {% for c in collection_list %}
-
- {{ c.title }}
-
- {% endfor %}
-
- {% trans '添加到收藏单' %}
-
-
-
-
- {% endif %}
-
- {% if song.source_site == "spotify" %}
-
- {% endif %}
-
-
-
-
-
- {% include "partial/_footer.html" %}
-
-
-
-
-
-
- {% if not mark %}
-
-
-
-
- {% else %}
-
{% trans '我的标记' %}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ mark_form.media }}
- {% csrf_token %}
- {{ mark_form.id }}
- {{ mark_form.song }}
- {% if mark.rating %}
- {% endif %}
-
-
- {{ mark_form.rating }}
-
- {{ mark_form.status }}
-
-
-
-
- {{ mark_form.text }}
-
-
- {{ mark_form.tags.label }}
- {{ mark_form.tags }}
-
-
-
-
- {{ mark_form.visibility.label }}:
- {{ mark_form.visibility }}
-
-
- {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
-
-
-
-
-
-
-
-
-
-
-
-
{% trans '确定要删除你的标记吗?' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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" %}
-
-
-
-
-
-
-
- {% include "partial/mark_list.html" with mark_list=marks current_item=song %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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.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 }}
-
-
-
-
-
-
- {{ form.content }}
-
- {{ form.media }}
- {% csrf_token %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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.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" %}
-
-
-
-
-
-
-
-
-
- {% for review in reviews %}
-
-
-
- {{ review.owner.username }}
- {% if review.visibility > 0 %}
-
- {% endif %}
- {{ review.edited_time }}
-
-
- {{ review.title }}
-
-
- {% empty %}
- {% trans '无结果' %}
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% 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.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()