+ {% endcomment %}
+
+
+
+
+
+
\ 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
new file mode 100644
index 00000000..c12d6276
--- /dev/null
+++ b/music/templates/music/create_update_album_review.html
@@ -0,0 +1,131 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+ {% 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" %}
+
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html
new file mode 100644
index 00000000..a694c891
--- /dev/null
+++ b/music/templates/music/create_update_song.html
@@ -0,0 +1,97 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
\ 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
new file mode 100644
index 00000000..52ee274d
--- /dev/null
+++ b/music/templates/music/create_update_song_review.html
@@ -0,0 +1,135 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - ' %}{{ 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 %}
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/music/templates/music/delete_album.html b/music/templates/music/delete_album.html
new file mode 100644
index 00000000..36593eba
--- /dev/null
+++ b/music/templates/music/delete_album.html
@@ -0,0 +1,104 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - 删除音乐' %}
+
+
+
+
+
+
+
+
+
+
+ {% 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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html
new file mode 100644
index 00000000..7deaaa49
--- /dev/null
+++ b/music/templates/music/delete_album_review.html
@@ -0,0 +1,107 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - 删除评论' %}
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
{% trans '确认删除这篇评论吗?' %}
+
+
+
+
+
+
+
+ {{ review.title }}
+
+ {% if review.is_private %}
+
+ {% endif %}
+
+
+
+
+
{{ review.owner.username }}
+
+ {% if mark %}
+
+ {% if mark.rating %}
+
+ {% endif %}
+
+ {% endif %}
+
+
{{ review.edited_time }}
+
+
+
+
+
+
+
+ {{ form.content }}
+
+ {{ form.media }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/music/templates/music/delete_song.html b/music/templates/music/delete_song.html
new file mode 100644
index 00000000..ace5bc50
--- /dev/null
+++ b/music/templates/music/delete_song.html
@@ -0,0 +1,104 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - 删除音乐' %}
+
+
+
+
+
+
+
+
+
+
+ {% 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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html
new file mode 100644
index 00000000..9e515b02
--- /dev/null
+++ b/music/templates/music/delete_song_review.html
@@ -0,0 +1,107 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - 删除评论' %}
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
{% trans '确认删除这篇评论吗?' %}
+
+
+
+
+
+
+
+ {{ review.title }}
+
+ {% if review.is_private %}
+
+ {% endif %}
+
+
+
+
+
{{ review.owner.username }}
+
+ {% if mark %}
+
+ {% if mark.rating %}
+
+ {% endif %}
+
+ {% endif %}
+
+
{{ review.edited_time }}
+
+
+
+
+
+
+
+ {{ form.content }}
+
+ {{ form.media }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html
new file mode 100644
index 00000000..01d48925
--- /dev/null
+++ b/music/templates/music/scrape_album.html
@@ -0,0 +1,109 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - 从豆瓣获取数据' %}
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
+
+
+ {% trans '根据豆瓣内容填写下方表单' %}
+
+
+
+
+
+
+
+
+
+
+
+ {% trans '复制详情页链接' %}
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+
+
+
+
diff --git a/music/templates/music/scrape_song.html b/music/templates/music/scrape_song.html
new file mode 100644
index 00000000..e2cd4730
--- /dev/null
+++ b/music/templates/music/scrape_song.html
@@ -0,0 +1,109 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
{% trans 'NiceDB - 从豆瓣获取数据' %}
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
+
+
+ {% trans '根据豆瓣内容填写下方表单' %}
+
+
+
+
+
+
+
+
+
+
+
+ {% trans '复制详情页链接' %}
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+
+
+
+
diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html
new file mode 100644
index 00000000..841514d7
--- /dev/null
+++ b/music/templates/music/song_detail.html
@@ -0,0 +1,400 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load strip_scheme %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans 'NiceDB - 音乐详情' %} | {{ song.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+ {% if song.rating %}
+
+ {{ song.rating }}
+ {% 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}}
+
+ {% endfor %}
+ {% endif %}
+
+
+ {% if song.last_editor %}
+
+ {% 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 %}
+ {% if mark_list %}
+
+ {% for others_mark in mark_list %}
+ -
+ {{ others_mark.owner.username }}
+ {{ others_mark.get_status_display }}
+ {% if others_mark.rating %}
+
+ {% endif %}
+ {% if others_mark.is_private %}
+
+ {% endif %}
+ {{ others_mark.edited_time }}
+ {% if others_mark.text %}
+
{{ others_mark.text }}
+ {% endif %}
+
+ {% endfor %}
+
+ {% else %}
+
{% trans '暂无标记' %}
+ {% endif %}
+
+
+
{% 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.is_private %}
+
+ {% endif %}
+
+ {% trans '修改' %}
+
+
+
+
+
{{ mark.edited_time }}
+
+ {% if mark.text %}
+
{{ mark.text }}
+ {% endif %}
+
+
+ {% for tag in mark_tags %}
+ {{ tag }}
+ {% endfor %}
+
+
+
+ {% else %}
+
+
{% trans '标记这部作品' %}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+ {% if review %}
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+
+
+
+ {% if not mark %}
+
+
+
+
+ {% else %}
+
{% trans '我的标记' %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans '确定要删除你的标记吗?' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html
new file mode 100644
index 00000000..07dbae3b
--- /dev/null
+++ b/music/templates/music/song_mark_list.html
@@ -0,0 +1,175 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+
+
+
+
+
+
+
{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的标记' %}
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
+
+
+
+
+ {% for mark in marks %}
+
+ -
+ {{ mark.owner.username }}
+ {{ mark.get_status_display }}
+ {% if mark.rating %}
+
+ {% endif %}
+ {% if mark.is_private %}
+
+ {% endif %}
+ {{ mark.edited_time }}
+ {% if mark.text %}
+
{{ mark.text }}
+ {% endif %}
+
+
+ {% 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" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html
new file mode 100644
index 00000000..14c43fc3
--- /dev/null
+++ b/music/templates/music/song_review_detail.html
@@ -0,0 +1,152 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+
+
+
+
+
+
+
+
+
+
+
+
{% trans 'NiceDB - 评论详情' %}
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
+
+
+ {{ review.title }}
+
+ {% if review.is_private %}
+
+ {% endif %}
+
+
+
+
{{ review.owner.username }}
+
+ {% if mark %}
+
+ {% if mark.rating %}
+
+ {% endif %}
+
+ {% endif %}
+
+
{{ review.edited_time }}
+
+
+
+
+
+
+ {{ form.content }}
+
+ {{ form.media }}
+
+
+
+
+
+
+
+
+

+
+
+
+
+
{% 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" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html
new file mode 100644
index 00000000..b204767e
--- /dev/null
+++ b/music/templates/music/song_review_list.html
@@ -0,0 +1,158 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+
+
+
+
+
+
+
{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的评论' %}
+
+
+
+
+
+
+
+
+
+
+ {% 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 %}
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% comment %}
+
{% oauth_token %}
+
{% mastodon request.user.mastodon_site %}
+
+
{{ user.mastodon_id }}
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/music/tests.py b/music/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/music/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/music/urls.py b/music/urls.py
new file mode 100644
index 00000000..4acab46d
--- /dev/null
+++ b/music/urls.py
@@ -0,0 +1,40 @@
+from django.urls import path
+from .views import *
+
+
+app_name = 'music'
+urlpatterns = [
+ path('song/create/', create_song, name='create_song'),
+ path('song/
/', 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//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'),
+ path('song//review/list/',
+ retrieve_song_review_list, name='retrieve_song_review_list'),
+# path('song/scrape/', scrape_song, name='scrape_song'),
+ path('song/click_to_scrape/', click_to_scrape_song, name='click_to_scrape_song'),
+
+ path('album/create/', create_album, name='create_album'),
+ path('album//', retrieve_album, name='retrieve_album'),
+ path('album/update//', update_album, name='update_album'),
+ path('album/delete//', delete_album, name='delete_album'),
+ path('album/mark/', create_update_album_mark, name='create_update_album_mark'),
+ path('album//mark/list/',
+ 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
new file mode 100644
index 00000000..993dae35
--- /dev/null
+++ b/music/views.py
@@ -0,0 +1,1182 @@
+# from boofilsic.settings import MASTODON_TAGS
+from .forms import *
+from .models import *
+from common.models import SourceSiteEnum
+from common.views import PAGE_LINK_NUMBER, jump_or_scrape
+from common.utils import PageLinksGenerator
+from mastodon.utils import rating_to_emoji
+from mastodon.api import check_visibility, post_toot, TootVisibilityEnum
+from mastodon import mastodon_request_included
+from django.core.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
+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
+
+
+
+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,
+ '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,
+ '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,
+ '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, request.session['oauth_token'])
+ review_list = SongReview.get_available(
+ song, request.user, request.session['oauth_token'])
+ 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]
+
+ # 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,
+ }
+ )
+ 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 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()
+ # update
+ form = SongMarkForm(request.POST, instance=mark)
+ else:
+ # create
+ form = SongMarkForm(request.POST)
+
+ if form.is_valid():
+ if form.instance.status == MarkStatusEnum.WISH.value:
+ 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 form.cleaned_data['is_private']:
+ visibility = TootVisibilityEnum.PRIVATE
+ else:
+ visibility = TootVisibilityEnum.UNLISTED
+ url = "https://" + request.get_host() + reverse("music:retrieve_song",
+ args=[song.id])
+ words = MusicMarkStatusTranslator(form.cleaned_data['status']) +\
+ f"《{song.title}》" + \
+ rating_to_emoji(form.cleaned_data['rating'])
+
+ # tags = MASTODON_TAGS % {'category': '书', 'type': '标记'}
+ tags = ''
+ content = words + '\n' + url + '\n' + \
+ form.cleaned_data['text'] + '\n' + tags
+ response = post_toot(request.user.mastodon_site, content, visibility,
+ request.session['oauth_token'])
+ if response.status_code != 200:
+ mastodon_logger.error(
+ f"CODE:{response.status_code} {response.text}")
+ return HttpResponseServerError("publishing mastodon status failed")
+ else:
+ return HttpResponseBadRequest("invalid form data")
+
+ return redirect(reverse("music:retrieve_song", args=[form.instance.song.id]))
+ else:
+ return HttpResponseBadRequest("invalid method")
+
+
+@mastodon_request_included
+@login_required
+def retrieve_song_mark_list(request, song_id):
+ if request.method == 'GET':
+ song = get_object_or_404(Song, pk=song_id)
+ queryset = SongMark.get_available(
+ song, request.user, request.session['oauth_token'])
+ 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 form.cleaned_data['is_private']:
+ visibility = TootVisibilityEnum.PRIVATE
+ else:
+ visibility = TootVisibilityEnum.UNLISTED
+ url = "https://" + request.get_host() + reverse("music:retrieve_song_review",
+ args=[form.instance.id])
+ words = "发布了关于" + f"《{form.instance.song.title}》" + "的评论"
+ # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'}
+ tags = ''
+ content = words + '\n' + url + \
+ '\n' + form.cleaned_data['title'] + '\n' + tags
+ response = post_toot(request.user.mastodon_site, content, visibility,
+ request.session['oauth_token'])
+ if response.status_code != 200:
+ mastodon_logger.error(
+ f"CODE:{response.status_code} {response.text}")
+ return HttpResponseServerError("publishing mastodon status failed")
+ 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 form.cleaned_data['is_private']:
+ visibility = TootVisibilityEnum.PRIVATE
+ else:
+ visibility = TootVisibilityEnum.UNLISTED
+ url = "https://" + request.get_host() + reverse("music:retrieve_song_review",
+ args=[form.instance.id])
+ words = "发布了关于" + f"《{form.instance.song.title}》" + "的评论"
+ # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'}
+ tags = ''
+ content = words + '\n' + url + \
+ '\n' + form.cleaned_data['title'] + '\n' + tags
+ response = post_toot(request.user.mastodon_site, content, visibility,
+ request.session['oauth_token'])
+ if response.status_code != 200:
+ mastodon_logger.error(
+ f"CODE:{response.status_code} {response.text}")
+ return HttpResponseServerError("publishing mastodon status failed")
+ 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
+@login_required
+def retrieve_song_review(request, id):
+ if request.method == 'GET':
+ review = get_object_or_404(SongReview, pk=id)
+ if not check_visibility(review, request.session['oauth_token'], 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, request.session['oauth_token'])
+ 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 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,
+ '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,
+ '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,
+ '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, request.session['oauth_token'])
+ review_list = AlbumReview.get_available(
+ album, request.user, request.session['oauth_token'])
+ 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]
+
+ # 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,
+ }
+ )
+ 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 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()
+ # update
+ form = AlbumMarkForm(request.POST, instance=mark)
+ else:
+ # create
+ form = AlbumMarkForm(request.POST)
+
+ if form.is_valid():
+ if form.instance.status == MarkStatusEnum.WISH.value:
+ 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 form.cleaned_data['is_private']:
+ visibility = TootVisibilityEnum.PRIVATE
+ else:
+ visibility = TootVisibilityEnum.UNLISTED
+ url = "https://" + request.get_host() + reverse("music:retrieve_album",
+ args=[album.id])
+ words = MusicMarkStatusTranslator(form.cleaned_data['status']) +\
+ f"《{album.title}》" + \
+ rating_to_emoji(form.cleaned_data['rating'])
+
+ # tags = MASTODON_TAGS % {'category': '书', 'type': '标记'}
+ tags = ''
+ content = words + '\n' + url + '\n' + \
+ form.cleaned_data['text'] + '\n' + tags
+ response = post_toot(request.user.mastodon_site, content, visibility,
+ request.session['oauth_token'])
+ if response.status_code != 200:
+ mastodon_logger.error(
+ f"CODE:{response.status_code} {response.text}")
+ return HttpResponseServerError("publishing mastodon status failed")
+ else:
+ return HttpResponseBadRequest("invalid form data")
+
+ return redirect(reverse("music:retrieve_album", args=[form.instance.album.id]))
+ else:
+ return HttpResponseBadRequest("invalid method")
+
+
+@mastodon_request_included
+@login_required
+def retrieve_album_mark_list(request, album_id):
+ if request.method == 'GET':
+ album = get_object_or_404(Album, pk=album_id)
+ queryset = AlbumMark.get_available(
+ album, request.user, request.session['oauth_token'])
+ 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 form.cleaned_data['is_private']:
+ visibility = TootVisibilityEnum.PRIVATE
+ else:
+ visibility = TootVisibilityEnum.UNLISTED
+ url = "https://" + request.get_host() + reverse("music:retrieve_album_review",
+ args=[form.instance.id])
+ words = "发布了关于" + f"《{form.instance.album.title}》" + "的评论"
+ # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'}
+ tags = ''
+ content = words + '\n' + url + \
+ '\n' + form.cleaned_data['title'] + '\n' + tags
+ response = post_toot(request.user.mastodon_site, content, visibility,
+ request.session['oauth_token'])
+ if response.status_code != 200:
+ mastodon_logger.error(
+ f"CODE:{response.status_code} {response.text}")
+ return HttpResponseServerError("publishing mastodon status failed")
+ 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 form.cleaned_data['is_private']:
+ visibility = TootVisibilityEnum.PRIVATE
+ else:
+ visibility = TootVisibilityEnum.UNLISTED
+ url = "https://" + request.get_host() + reverse("music:retrieve_album_review",
+ args=[form.instance.id])
+ words = "发布了关于" + f"《{form.instance.album.title}》" + "的评论"
+ # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'}
+ tags = ''
+ content = words + '\n' + url + \
+ '\n' + form.cleaned_data['title'] + '\n' + tags
+ response = post_toot(request.user.mastodon_site, content, visibility,
+ request.session['oauth_token'])
+ if response.status_code != 200:
+ mastodon_logger.error(
+ f"CODE:{response.status_code} {response.text}")
+ return HttpResponseServerError("publishing mastodon status failed")
+ 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
+@login_required
+def retrieve_album_review(request, id):
+ if request.method == 'GET':
+ review = get_object_or_404(AlbumReview, pk=id)
+ if not check_visibility(review, request.session['oauth_token'], 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, request.session['oauth_token'])
+ 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()
diff --git a/users/templates/users/book_list.html b/users/templates/users/book_list.html
index 3790556d..5694ed22 100644
--- a/users/templates/users/book_list.html
+++ b/users/templates/users/book_list.html
@@ -118,8 +118,7 @@
data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;">
{% endif %}
{% if mark.is_private %}
-
{% endif %}
{% if mark.is_private %}
-
+
diff --git a/users/templates/users/music_list.html b/users/templates/users/music_list.html
new file mode 100644
index 00000000..8ed992ff
--- /dev/null
+++ b/users/templates/users/music_list.html
@@ -0,0 +1,289 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+
+
+
+
+
+
+ {% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_navbar.html" %}
+
+
+
+
+
+
+
+
+
+ {{ user.username }}{{ list_title }}
+
+
+
+
+ {% for mark in marks %}
+
+ {% with mark.music as music %}
+
+ -
+
+ {% if music.category_name|lower == 'album' %}
+
+
+
+ {% elif music.category_name|lower == 'song' %}
+
+
+
+ {% endif %}
+
+
+
+
+ {% if music.artist %}{% trans '艺术家' %}
+ {% for artist in music.artist %}
+ {{ artist }}
+ {% if not forloop.last %} {% endif %}
+ {% endfor %}
+ {% endif %}
+
+ {% if music.genre %}/ {% trans '流派' %}
+ {{ music.genre }}
+ {% endif %}
+
+ {% if music.release_date %}/ {% trans '发行日期' %}
+ {{ music.release_date }}
+ {% endif %}
+
+ {% if music.brief %}
+
+ {{ music.brief }}
+
+ {% elif music.category_name|lower == 'album' %}
+
+ {% trans '曲目:' %}{{ music.track_list }}
+
+ {% else %}
+
+
+ {% trans '所属专辑:' %}{{ music.album }}
+
+ {% endif %}
+
+ {% for tag_dict in music.tag_list %}
+ {% for k, v in tag_dict.items %}
+ {% if k == 'content' %}
+
+ {{ v }}
+
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+
+
+
+ -
+
+ {% if mark.rating %}
+
+ {% endif %}
+ {% if mark.is_private %}
+
+
+
+ {% endif %}
+ {% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}
+ {% if mark.text %}
+
{{ mark.text }}
+ {% endif %}
+
+
+
+
+
+
+
+ {% endwith %}
+
+ {% empty %}
+ {% trans '无结果' %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include "partial/_footer.html" %}
+
+
+
+ {% oauth_token %}
+ {% mastodon request.user.mastodon_site %}
+
+ {% if user == request.user %}
+ {{ user.mastodon_id }}
+ {% else %}
+ {{ user.target_site_id }}
+ {% endif %}
+ {% url 'users:home' 0 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/users/templates/users/register.html b/users/templates/users/register.html
index d0e6056b..9b750a66 100644
--- a/users/templates/users/register.html
+++ b/users/templates/users/register.html
@@ -19,7 +19,7 @@
-
欢迎来到NiceDB书影音(其实现在只有书汗电影)!
+
欢迎来到NiceDB书影音!
NiceDB书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
这里仍是一片处女地,丰富的内容需要大家共同创造。
diff --git a/users/urls.py b/users/urls.py
index cf79b2b3..4b8adb7d 100644
--- a/users/urls.py
+++ b/users/urls.py
@@ -13,6 +13,7 @@ urlpatterns = [
path('/following/', following, name='following'),
path('/book//', book_list, name='book_list'),
path('/movie//', movie_list, name='movie_list'),
+ path('/music//', music_list, name='music_list'),
path('/', home, name='home'),
path('/followers/', followers, name='followers'),
path('/following/', following, name='following'),
diff --git a/users/views.py b/users/views.py
index 96e183ab..b9918405 100644
--- a/users/views.py
+++ b/users/views.py
@@ -17,8 +17,10 @@ from common.models import MarkStatusEnum
from common.utils import PageLinksGenerator
from books.models import *
from movies.models import *
+from music.models import *
from books.forms import BookMarkStatusTranslator
from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
from mastodon.models import MastodonApplication
@@ -108,6 +110,14 @@ def register(request):
elif request.method == 'POST':
token = request.session['new_user_token']
user_data = get_user_data(request.COOKIES['mastodon_domain'], token)
+ if user_data is None:
+ return render(
+ request,
+ 'common/error.html',
+ {
+ 'msg': _("长毛象访问失败😫")
+ }
+ )
new_user = User(
username=user_data['username'],
mastodon_id=user_data['id'],
@@ -372,7 +382,7 @@ def book_list(request, id, status):
mark.book.tag_list = mark.book.get_tags_manager().values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
- list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书"))
+ list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的书"))
return render(
request,
'users/book_list.html',
@@ -441,7 +451,7 @@ def movie_list(request, id, status):
mark.movie.tag_list = mark.movie.get_tags_manager().values('content').annotate(
tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
- list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集"))
+ list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的电影和剧集"))
return render(
request,
'users/movie_list.html',
@@ -455,6 +465,86 @@ def movie_list(request, id, status):
return HttpResponseBadRequest()
+@mastodon_request_included
+@login_required
+def music_list(request, id, status):
+ if request.method == 'GET':
+ if not status.upper() in MarkStatusEnum.names:
+ return HttpResponseBadRequest()
+
+ if isinstance(id, str):
+ try:
+ username = id.split('@')[0]
+ site = id.split('@')[1]
+ except IndexError as e:
+ return HttpResponseBadRequest("Invalid user id")
+ query_kwargs = {'username': username, 'mastodon_site': site}
+ elif isinstance(id, int):
+ query_kwargs = {'pk': id}
+ try:
+ user = User.objects.get(**query_kwargs)
+ except ObjectDoesNotExist:
+ msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!")
+ sec_msg = _("目前只开放本站用户注册")
+ return render(
+ request,
+ 'common/error.html',
+ {
+ 'msg': msg,
+ 'secondary_msg': sec_msg,
+ }
+ )
+ if not user == request.user:
+ # mastodon request
+ relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
+ if relation['blocked_by']:
+ msg = _("你没有访问TA主页的权限😥")
+ return render(
+ request,
+ 'common/error.html',
+ {
+ 'msg': msg,
+ }
+ )
+ queryset = list(AlbumMark.get_available_by_user(user, relation['following']).filter(
+ status=MarkStatusEnum[status.upper()])) \
+ + list(SongMark.get_available_by_user(user, relation['following']).filter(
+ status=MarkStatusEnum[status.upper()]))
+
+ user.target_site_id = get_cross_site_id(
+ user, request.user.mastodon_site, request.session['oauth_token'])
+ else:
+ queryset = list(AlbumMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) \
+ + list(SongMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]))
+ queryset = sorted(queryset, key=lambda e: e.edited_time, reverse=True)
+ paginator = Paginator(queryset, ITEMS_PER_PAGE)
+ page_number = request.GET.get('page', default=1)
+ marks = paginator.get_page(page_number)
+ for mark in marks:
+ if mark.__class__ == AlbumMark:
+ mark.music = mark.album
+ mark.music.tag_list = mark.album.get_tags_manager().values('content').annotate(
+ tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
+ elif mark.__class__ == SongMark:
+ mark.music = mark.song
+ mark.music.tag_list = mark.song.get_tags_manager().values('content').annotate(
+ tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
+
+ marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
+ list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的音乐"))
+ return render(
+ request,
+ 'users/music_list.html',
+ {
+ 'marks': marks,
+ 'user': user,
+ 'list_title' : list_title,
+ }
+ )
+ else:
+ return HttpResponseBadRequest()
+
+
@login_required
def report(request):
if request.method == 'GET':