From 6c3e377bbe8e707a269dc0c797a47379d29fe2ac Mon Sep 17 00:00:00 2001
From: doubaniux <56670055+doubaniux@users.noreply.github.com>
Date: Wed, 27 Jul 2022 21:58:25 +0200
Subject: [PATCH 1/6] fix tag search result pagination (#109) (#114)

* fix imdb link parsing for DoubanMovie

* delete douban image download mixin

* add readme

* remove usage of eval

* fix tag search result pagination

Co-authored-by: Your Name <you@example.com>
Co-authored-by: doubaniux <56670055+doubaniux@users.noreply.github.com>
Co-authored-by: doubaniux <goodsir@vivaldi.net>

Co-authored-by: alphatownsman <90480431+alphatownsman@users.noreply.github.com>
Co-authored-by: Your Name <you@example.com>
---
 README.md                                  |  2 +-
 common/templates/common/search_result.html | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index ed8b2d05..2cfde8ea 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@ Currently looking for someone to help with:
 - Explaining the structure of code
 - Refactoring (this is something big)
 
-## Deployment
 This project is still in its early stage, so you are not encouraged to deploy it on your own. If you do want to give it a try, please check the [fork of *alphatownsman*](https://github.com/alphatownsman/boofilsic), which is more friendly.
 
 ## Sponsor
 If you like this project, please consider sponsoring us on [Patreon](https://patreon.com/tertius).
+
diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html
index baec6b0d..cc420eed 100644
--- a/common/templates/common/search_result.html
+++ b/common/templates/common/search_result.html
@@ -419,23 +419,23 @@
                             <div class="pagination" >
                                 
                                 {% if items.pagination.has_prev %}
-                                    <a href="?page=1&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                    <a href="?page={{ items.previous_page_number }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                    <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                    <a href="?page={{ items.previous_page_number }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
                                 {% endif %}
                                     
                                 {% for page in items.pagination.page_range %}
                                     
                                     {% if page == items.pagination.current_page %}
-                                    <a href="?page={{ page }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
                                     {% else %}
-                                    <a href="?page={{ page }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}" class="pagination__page-link">{{ page }}</a>
+                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__page-link">{{ page }}</a>
                                     {% endif %}
                                         
                                 {% endfor %}
                                     
                                 {% if items.pagination.has_next %}
-                                    <a href="?page={{ items.next_page_number }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                    <a href="?page={{ items.pagination.last_page }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}" class="pagination__nav-link">&raquo;</a>
+                                    <a href="?page={{ items.next_page_number }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                    <a href="?page={{ items.pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link">&raquo;</a>
                                 {% endif %}           
                               
                             </div>            

From 14b003a44a0bc97e91630ab36c61f7ab63bda0bb Mon Sep 17 00:00:00 2001
From: Henri Dickson <90480431+alphatownsman@users.noreply.github.com>
Date: Wed, 9 Nov 2022 13:56:50 -0500
Subject: [PATCH 2/6] add all NeoDB features to NiceDB (#115)

* fix scraping failure with wepb image (merge upstream/fix-webp-scrape)

* add filetype to requirements

* add proxycrawl.com as fallback for douban scraper

* load 3p js/css from cdn

* add fix-cover task

* fix book/album cover tasks

* scrapestack

* bandcamp scrape and preview ;
manage.py scrape <url> ;
make ^C work when DEBUG

* use scrapestack when fix cover

* add user agent to improve compatibility

* search BandCamp for music albums

* add missing MovieGenre

* fix search 500 when song has no parent album

* adjust timeout

* individual scrapers

* fix tmdb parser

* export marks via rq; pref to send public toot; move import to data page

* fix spotify import

* fix edge cases

* export: fix dupe tags

* use rq to manage doufen import

* add django command to manage rq jobs

* fix export edge case

* tune rq admin

* fix detail page 502 step 1: async pull mastodon follow/block/mute list

* fix detail page 502 step 2: calculate relationship by local cached data

* manual sync mastodon follow info

* domain_blocks parsing fix

* marks by who i follows

* adjust label

* use username in urls

* add page to list a user\'s review

* review widget on user home page

* fix preview 500

* fix typo

* minor fix

* fix google books parsing

* allow mark/review visible to oneself

* fix auto sync masto for new user

* fix search 500

* add command to restart a sync task

* reset visibility

* delete user data

* fix tag search result pagination

* not upgrade to django 4 yet

* basic doc

* wip: collection

* wip

* wip

* collection use htmx

* show in-collection section for entities

* fix typo

* add su for easier debug

* fix some 500s

* fix login using alternative domain

* hide data from disabled user

* add item to list from detail page

* my tags

* collection: inline comment edit

* show number of ratings

* fix collection delete

* more detail in collection view

* use item template in search result

* fix 500

* write index to meilisearch

* fix search

* reindex in batch

* fix 500

* show search result from meilisearch

* more search commands

* index less fields

* index new items only

* search highlights

* fix 500

* auto set search category

* classic search if no meili server

* fix index stats error

* support typesense backend

* workaround typesense bug

* make external search async

* fix 500, typo

* fix cover scripts

* fix minor issue in douban parser

* supports m.douban.com and customized bandcamp domain

* move account

* reword with gender-friendly and instance-neutral language

* Friendica does not have vapid_key in api response

* enable anonymous search

* tweak book result template

* API v0

API v0

* fix meilisearch reindex

* fix search by url error

* login via twitter.com

* login via pixelfed

* minor fix

* no refresh on inactive users

* support refresh access token

* get rid of /users/number-id/

* refresh twitter handler automatically

* paste image when review

* support PixelFed (very long token)

* fix django-markdownx version

* ignore single quote for meilisearch for now

* update logo

* show book review/mark from same isbn

* show movie review/mark from same imdb

* fix login with older mastodon servers

* import Goodreads book list and profile

* add timestamp to Goodreads import

* support new google books api

* import goodreads list

* minor goodreads fix

* click corner action icon to add to wishlist

* clean up duplicated code

* fix anonymous search

* fix 500

* minor fix search 500

* show rating only if votes > 5

* Entity.refresh_rating()

* preference to append text when sharing; clean up duplicated code

* fix missing data for user tagged view

* fix page link for tag view

* fix 500 when language field longer than 10

* fix 500 when sharing mark for song

* fix error when reimport goodread profile

* fix minor typo

* fix a rare 500

* error log dump less

* fix tags in marks export

* fix missing param in pagination

* import douban review

* clarify text

* fix missing sheet in review import

* review: show in progress

* scrape douban: ignore unknown genre

* minor fix

* improve review import by guess entity urls

* clear guide text for review import

* improve review import form text

* workaround some 500

* fix mark import error

* fix img in review import

* load external results earlier

* ignore search server errors

* simplify user register flow to avoid inconsistent state

* Add a learn more link on login page

* Update login.html

* show mark created timestamp as mark time

* no 500 for api error

* redirect for expired tokens

* ensure preference object created.

* mark collections

* tag list

* fix tag display

* fix sorting etc

* fix 500

* fix potential export 500; save shared links

* fix share to twittwe

* fix review url

* fix 500

* fix 500

* add timeline, etc

* missing status change in timeline

* missing id in timeline

* timeline view by default

* workaround bug in markdownx...

* fix typo

* option to create new collection when add from detail page

* add missing announcement and tags in timeline home

* add missing announcement

* add missing announcement

* opensearch

* show fediverse shared link

* public review no longer requires login

* fix markdownx bug

* fix 500

* use cloudflare cdn

* validate jquery load and domain input

* fix 500

* tips for goodreads import

* collaborative collection

* show timeline and profile link on nav bar

* minor tweak

* share collection

* fix Goodreads search

* show wish mark in timeline

* resync failed urls with local proxy

* resync failed urls with local proxy: check proxy first

* scraper minor fix

* resync failed urls

* fix fields limit

* fix douban parsing error

* resync

* scraper minor fix

* scraper minor fix

* scraper minor fix

* local proxy

* local proxy

* sync default config from neodb

* configurable site name

* fix 500

* fix 500 for anonymous user

* add sentry

* add git version in log

* add git version in log

* no longer rely on cdnjs.cloudflare.com

* move jq/cash to _common_libs template partial

* fix rare js error

* fix 500

* avoid double submission error

* import tag in lower case

* catch some js network errors

* catch some js network errors

* support more goodread urls

* fix unaired tv in tmdb

* support more google book urls

* fix related series

* more goodreads urls

* robust googlebooks search

* robust search

* Update settings.py

* Update scraper.py

* Update requirements.txt

* make nicedb work

* doc update

* simplify permission check

* update doc

* update doc for bug report link

* skip spotify tracks

* fix 500

* improve search api

* blind fix import compatibility

* show years for movie in timeline

* show years for movie in timeline; thinner font

* export reviews

* revert user home to use jquery https://github.com/fabiospampinato/cash/issues/246

* IGDB

* use IGDB for Steam

* use TMDB for IMDb

* steam: igdb then fallback to steam

* keep change history

* keep change history: add django settings

* Steam: keep localized title/brief while merging IGDB

* basic Docker support

* rescrape

* Create codeql-analysis.yml

* Create SECURITY.md

* Create pysa.yml

Co-authored-by: doubaniux <goodsir@vivaldi.net>
Co-authored-by: Your Name <you@example.com>
Co-authored-by: Their Name <they@example.com>
Co-authored-by: Mt. Front <mfcndw@gmail.com>
---
 .github/workflows/codeql-analysis.yml         |   74 +
 .github/workflows/pysa.yml                    |   50 +
 .gitignore                                    |    5 +-
 Dockerfile                                    |   23 +
 README.md                                     |   11 +-
 SECURITY.md                                   |    5 +
 boofilsic/context_processors.py               |    5 +
 boofilsic/settings.py                         |  123 +-
 boofilsic/urls.py                             |    6 +
 books/admin.py                                |    3 +-
 books/apps.py                                 |    5 +
 books/forms.py                                |   24 +-
 books/management/commands/fix-book-cover.py   |  200 +++
 books/models.py                               |  162 ++-
 books/templates/books/create_update.html      |   28 +-
 .../templates/books/create_update_review.html |   12 +-
 books/templates/books/delete.html             |   12 +-
 books/templates/books/delete_review.html      |   14 +-
 books/templates/books/detail.html             |  132 +-
 books/templates/books/mark_list.html          |   43 +-
 books/templates/books/review_detail.html      |   22 +-
 books/templates/books/review_list.html        |   18 +-
 books/templates/books/scrape.html             |    4 +-
 books/urls.py                                 |    6 +-
 books/views.py                                |  137 +-
 collection/__init__.py                        |    0
 collection/admin.py                           |    3 +
 collection/apps.py                            |    6 +
 collection/forms.py                           |   45 +
 collection/models.py                          |  126 ++
 collection/templates/add_to_list.html         |   45 +
 collection/templates/create_update.html       |   71 +
 collection/templates/delete.html              |  117 ++
 collection/templates/detail.html              |  147 ++
 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                           |  442 ++++++
 common/forms.py                               |   50 +-
 common/importers/douban.py                    |  270 ++++
 common/importers/goodreads.py                 |  202 +++
 common/index.py                               |   12 +
 common/management/commands/delete_job.py      |   19 +
 common/management/commands/index_stats.py     |   40 +
 common/management/commands/init_index.py      |   18 +
 common/management/commands/list_jobs.py       |   24 +
 common/management/commands/reindex.py         |   40 +
 common/management/commands/restart_sync.py    |   28 +
 common/management/commands/scrape.py          |   25 +
 common/models.py                              |  220 ++-
 common/scraper.py                             | 1259 ++---------------
 common/scrapers/bandcamp.py                   |   71 +
 common/scrapers/bangumi.py                    |  199 +++
 common/scrapers/douban.py                     |  714 ++++++++++
 common/scrapers/goodreads.py                  |  157 ++
 common/scrapers/google.py                     |  102 ++
 common/scrapers/igdb.py                       |   88 ++
 common/scrapers/imdb.py                       |  116 ++
 common/scrapers/spotify.py                    |  287 ++++
 common/scrapers/steam.py                      |   92 ++
 common/scrapers/tmdb.py                       |  150 ++
 common/search/meilisearch.py                  |  183 +++
 common/search/typesense.py                    |  215 +++
 common/searcher.py                            |  209 +++
 common/static/css/boofilsic.css               |  552 +++-----
 common/static/css/boofilsic.min.css           |    2 +-
 common/static/img/fediverse.svg               |    5 +
 common/static/img/logo.svg                    |  141 +-
 common/static/img/logo_square.jpg             |  Bin 42490 -> 32105 bytes
 common/static/img/logo_square.svg             |  194 ++-
 common/static/js/create_update_review.js      |    4 +-
 common/static/js/detail.js                    |   16 +-
 common/static/js/home.js                      |  189 ++-
 common/static/js/mastodon.js                  |   72 +-
 common/static/js/rating-star-readonly.js      |    8 +-
 common/static/js/scrape.js                    |    2 +-
 common/static/js/sort_layout.js               |    6 +-
 common/static/lib/css/milligram.css           |  605 --------
 common/static/lib/css/multiple-select.min.css |   10 -
 common/static/lib/css/neo.css                 |  166 +++
 common/static/lib/js/hyperscript-0.9.5.min.js |    2 +
 common/static/lib/js/multiple-select.min.js   |   10 -
 common/static/opensearch.xml                  |    8 +
 common/static/sass/_AsideSection.sass         |    2 +-
 common/static/sass/_Label.sass                |   38 +-
 common/static/sass/_Modal.sass                |    4 +-
 common/static/sass/_Vendor.sass               |    5 +-
 common/templates/common/error.html            |    5 +-
 .../common/external_search_result.html        |   48 +
 common/templates/common/search_result.html    |  418 +-----
 common/templates/partial/_announcement.html   |   61 +
 common/templates/partial/_common_libs.html    |   23 +
 common/templates/partial/_footer.html         |    7 +-
 common/templates/partial/_navbar.html         |   39 +-
 common/templates/partial/_sidebar.html        |  186 +++
 common/templates/partial/list_item.html       |    9 +
 common/templates/partial/list_item_book.html  |  159 +++
 common/templates/partial/list_item_game.html  |  139 ++
 common/templates/partial/list_item_movie.html |  164 +++
 common/templates/partial/list_item_music.html |  171 +++
 common/templates/partial/mark_list.html       |   37 +
 common/templatetags/highlight.py              |   14 +-
 common/templatetags/neo.py                    |   48 +
 common/templatetags/oauth_token.py            |    2 +-
 common/templatetags/thumb.py                  |    5 +-
 common/urls.py                                |    2 +
 common/utils.py                               |    2 +
 common/views.py                               |  124 +-
 doc/GUIDE.md                                  |  106 ++
 docker-compose.yml                            |   32 +
 docker/entrypoint.sh                          |   13 +
 docker/start.sh                               |   36 +
 games/admin.py                                |    3 +-
 games/apps.py                                 |    5 +
 games/forms.py                                |   25 +-
 games/models.py                               |   64 +-
 games/templates/games/create_update.html      |   29 +-
 .../templates/games/create_update_review.html |   12 +-
 games/templates/games/delete.html             |   12 +-
 games/templates/games/delete_review.html      |   14 +-
 games/templates/games/detail.html             |  101 +-
 games/templates/games/mark_list.html          |   42 +-
 games/templates/games/review_detail.html      |   22 +-
 games/templates/games/review_list.html        |   14 +-
 games/templates/games/scrape.html             |    4 +-
 games/urls.py                                 |    7 +-
 games/views.py                                |  137 +-
 management/models.py                          |    2 +-
 .../templates/management/create_update.html   |    4 +-
 management/templates/management/delete.html   |    4 +-
 management/templates/management/detail.html   |    4 +-
 management/templates/management/list.html     |    4 +-
 mastodon/admin.py                             |    2 +-
 mastodon/api.py                               |  373 ++++-
 mastodon/auth.py                              |   82 +-
 mastodon/decorators.py                        |    2 +-
 mastodon/management/commands/wrong_sites.py   |   21 +
 mastodon/models.py                            |   10 +-
 mastodon/utils.py                             |    9 +-
 movies/admin.py                               |    3 +-
 movies/apps.py                                |    5 +
 movies/forms.py                               |   22 +-
 .../management/commands/fix-movie-poster.py   |  203 +++
 movies/models.py                              |  108 +-
 movies/templates/movies/create_update.html    |   31 +-
 .../movies/create_update_review.html          |   16 +-
 movies/templates/movies/delete.html           |   12 +-
 movies/templates/movies/delete_review.html    |   14 +-
 movies/templates/movies/detail.html           |  112 +-
 movies/templates/movies/mark_list.html        |   44 +-
 movies/templates/movies/review_detail.html    |   24 +-
 movies/templates/movies/review_list.html      |   19 +-
 movies/templates/movies/scrape.html           |    4 +-
 movies/urls.py                                |    6 +-
 movies/views.py                               |  137 +-
 music/admin.py                                |    5 +-
 music/apps.py                                 |    6 +
 music/forms.py                                |   33 +-
 music/management/commands/fix-album-cover.py  |  199 +++
 music/models.py                               |  117 +-
 music/templates/music/album_detail.html       |   89 +-
 music/templates/music/album_mark_list.html    |   45 +-
 .../templates/music/album_review_detail.html  |   24 +-
 music/templates/music/album_review_list.html  |   16 +-
 .../templates/music/create_update_album.html  |   29 +-
 .../music/create_update_album_review.html     |   14 +-
 music/templates/music/create_update_song.html |   10 +-
 .../music/create_update_song_review.html      |   14 +-
 music/templates/music/delete_album.html       |   12 +-
 .../templates/music/delete_album_review.html  |   14 +-
 music/templates/music/delete_song.html        |   12 +-
 music/templates/music/delete_song_review.html |   14 +-
 music/templates/music/scrape_album.html       |    4 +-
 music/templates/music/scrape_song.html        |    4 +-
 music/templates/music/song_detail.html        |   89 +-
 music/templates/music/song_mark_list.html     |   52 +-
 music/templates/music/song_review_detail.html |   24 +-
 music/templates/music/song_review_list.html   |   16 +-
 music/urls.py                                 |   13 +-
 music/views.py                                |  250 ++--
 requirements.txt                              |   26 +
 sync/apps.py                                  |    5 +-
 sync/jobs.py                                  |  158 +--
 sync/management/commands/resync.py            |   91 ++
 sync/models.py                                |    8 +-
 sync/views.py                                 |    9 +-
 timeline/__init__.py                          |    0
 timeline/admin.py                             |    3 +
 timeline/apps.py                              |   15 +
 .../management/commands/regen_activity.py     |   20 +
 timeline/models.py                            |   63 +
 timeline/templates/timeline.html              |   83 ++
 timeline/templates/timeline_data.html         |  124 ++
 timeline/tests.py                             |    3 +
 timeline/urls.py                              |    9 +
 timeline/views.py                             |   71 +
 users/account.py                              |  255 ++++
 users/data.py                                 |  142 ++
 .../management/commands/backfill_mastodon.py  |   21 +
 users/management/commands/disable_user.py     |   19 +
 .../management/commands/refresh_following.py  |   19 +
 users/management/commands/refresh_mastodon.py |   25 +
 users/models.py                               |  145 +-
 users/static/js/followers_list.js             |   19 +-
 users/static/js/following_list.js             |   12 +-
 users/static/lib/js/js.cookie.min.js          |    2 -
 users/tasks.py                                |  146 ++
 users/templates/users/book_list.html          |  282 ----
 users/templates/users/data.html               |  337 +++++
 users/templates/users/game_list.html          |  272 ----
 users/templates/users/home.html               |  668 ++++-----
 users/templates/users/home_anonymous.html     |   17 +
 users/templates/users/item_list.html          |   94 ++
 users/templates/users/login.html              |  108 +-
 users/templates/users/manage_report.html      |   14 +-
 users/templates/users/movie_list.html         |  285 ----
 users/templates/users/music_list.html         |  290 ----
 users/templates/users/preferences.html        |   93 ++
 users/templates/users/register.html           |   21 +-
 users/templates/users/relation_list.html      |   18 +-
 users/templates/users/report.html             |   10 +-
 users/templates/users/tags.html               |  110 ++
 users/urls.py                                 |   32 +-
 users/views.py                                |  693 ++++-----
 228 files changed, 12218 insertions(+), 6514 deletions(-)
 create mode 100644 .github/workflows/codeql-analysis.yml
 create mode 100644 .github/workflows/pysa.yml
 create mode 100644 Dockerfile
 create mode 100644 SECURITY.md
 create mode 100644 boofilsic/context_processors.py
 create mode 100644 books/management/commands/fix-book-cover.py
 create mode 100644 collection/__init__.py
 create mode 100644 collection/admin.py
 create mode 100644 collection/apps.py
 create mode 100644 collection/forms.py
 create mode 100644 collection/models.py
 create mode 100644 collection/templates/add_to_list.html
 create mode 100644 collection/templates/create_update.html
 create mode 100644 collection/templates/delete.html
 create mode 100644 collection/templates/detail.html
 create mode 100644 collection/templates/edit_item_comment.html
 create mode 100644 collection/templates/entity_list.html
 create mode 100644 collection/templates/list.html
 create mode 100644 collection/templates/share_collection.html
 create mode 100644 collection/templates/show_item_comment.html
 create mode 100644 collection/tests.py
 create mode 100644 collection/urls.py
 create mode 100644 collection/views.py
 create mode 100644 common/importers/douban.py
 create mode 100644 common/importers/goodreads.py
 create mode 100644 common/index.py
 create mode 100644 common/management/commands/delete_job.py
 create mode 100644 common/management/commands/index_stats.py
 create mode 100644 common/management/commands/init_index.py
 create mode 100644 common/management/commands/list_jobs.py
 create mode 100644 common/management/commands/reindex.py
 create mode 100644 common/management/commands/restart_sync.py
 create mode 100644 common/management/commands/scrape.py
 create mode 100644 common/scrapers/bandcamp.py
 create mode 100644 common/scrapers/bangumi.py
 create mode 100644 common/scrapers/douban.py
 create mode 100644 common/scrapers/goodreads.py
 create mode 100644 common/scrapers/google.py
 create mode 100644 common/scrapers/igdb.py
 create mode 100644 common/scrapers/imdb.py
 create mode 100644 common/scrapers/spotify.py
 create mode 100644 common/scrapers/steam.py
 create mode 100644 common/scrapers/tmdb.py
 create mode 100644 common/search/meilisearch.py
 create mode 100644 common/search/typesense.py
 create mode 100644 common/searcher.py
 create mode 100644 common/static/img/fediverse.svg
 delete mode 100644 common/static/lib/css/milligram.css
 delete mode 100644 common/static/lib/css/multiple-select.min.css
 create mode 100644 common/static/lib/css/neo.css
 create mode 100644 common/static/lib/js/hyperscript-0.9.5.min.js
 delete mode 100644 common/static/lib/js/multiple-select.min.js
 create mode 100644 common/static/opensearch.xml
 create mode 100644 common/templates/common/external_search_result.html
 create mode 100644 common/templates/partial/_announcement.html
 create mode 100644 common/templates/partial/_common_libs.html
 create mode 100644 common/templates/partial/_sidebar.html
 create mode 100644 common/templates/partial/list_item.html
 create mode 100644 common/templates/partial/list_item_book.html
 create mode 100644 common/templates/partial/list_item_game.html
 create mode 100644 common/templates/partial/list_item_movie.html
 create mode 100644 common/templates/partial/list_item_music.html
 create mode 100644 common/templates/partial/mark_list.html
 create mode 100644 common/templatetags/neo.py
 create mode 100644 doc/GUIDE.md
 create mode 100644 docker-compose.yml
 create mode 100755 docker/entrypoint.sh
 create mode 100755 docker/start.sh
 create mode 100644 mastodon/management/commands/wrong_sites.py
 create mode 100644 movies/management/commands/fix-movie-poster.py
 create mode 100644 music/management/commands/fix-album-cover.py
 create mode 100644 requirements.txt
 create mode 100644 sync/management/commands/resync.py
 create mode 100644 timeline/__init__.py
 create mode 100644 timeline/admin.py
 create mode 100644 timeline/apps.py
 create mode 100644 timeline/management/commands/regen_activity.py
 create mode 100644 timeline/models.py
 create mode 100644 timeline/templates/timeline.html
 create mode 100644 timeline/templates/timeline_data.html
 create mode 100644 timeline/tests.py
 create mode 100644 timeline/urls.py
 create mode 100644 timeline/views.py
 create mode 100644 users/account.py
 create mode 100644 users/data.py
 create mode 100644 users/management/commands/backfill_mastodon.py
 create mode 100644 users/management/commands/disable_user.py
 create mode 100644 users/management/commands/refresh_following.py
 create mode 100644 users/management/commands/refresh_mastodon.py
 delete mode 100644 users/static/lib/js/js.cookie.min.js
 create mode 100644 users/tasks.py
 delete mode 100644 users/templates/users/book_list.html
 create mode 100644 users/templates/users/data.html
 delete mode 100644 users/templates/users/game_list.html
 create mode 100644 users/templates/users/home_anonymous.html
 create mode 100644 users/templates/users/item_list.html
 delete mode 100644 users/templates/users/movie_list.html
 delete mode 100644 users/templates/users/music_list.html
 create mode 100644 users/templates/users/preferences.html
 create mode 100644 users/templates/users/tags.html

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..514df728
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,74 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "neo" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "neo" ]
+  schedule:
+    - cron: '35 0 * * 0'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'javascript', 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        
+        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+        
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+    #   If the Autobuild fails above, remove it and uncomment the following three lines. 
+    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+    # - run: |
+    #   echo "Run, Build Application using script"
+    #   ./location_of_script_within_repo/buildscript.sh
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
+      with:
+        category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/pysa.yml b/.github/workflows/pysa.yml
new file mode 100644
index 00000000..e4e20af3
--- /dev/null
+++ b/.github/workflows/pysa.yml
@@ -0,0 +1,50 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+# This workflow integrates Python Static Analyzer (Pysa) with
+# GitHub's Code Scanning feature.
+#
+# Python Static Analyzer (Pysa) is a security-focused static
+# analysis tool that tracks flows of data from where they
+# originate to where they terminate in a dangerous location.
+#
+# See https://pyre-check.org/docs/pysa-basics/
+
+name: Pysa
+
+on:
+  workflow_dispatch:
+  push:
+    branches: [ "neo" ]
+  pull_request:
+    branches: [ "neo" ]
+  schedule:
+    - cron: '45 12 * * 4'
+
+permissions:
+    contents: read
+
+jobs:
+  pysa:
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+
+      - name: Run Pysa
+        uses: facebook/pysa-action@f46a63777e59268613bd6e2ff4e29f144ca9e88b
+        with:
+          # To customize these inputs:
+          # See https://github.com/facebook/pysa-action#inputs
+          repo-directory: './'
+          requirements-path: 'requirements.txt'
+          infer-types: true
+          include-default-sapp-filters: true
diff --git a/.gitignore b/.gitignore
index 086f9d04..d1edae82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,7 @@ migrations/
 
 # debug log file
 /log
-log
\ No newline at end of file
+log
+
+# conf folder for neodb
+/neodb
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..cc9bf6e8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+# syntax=docker/dockerfile:1
+FROM python:3.8-slim
+ENV PYTHONDONTWRITEBYTECODE=1
+ENV PYTHONUNBUFFERED=1
+RUN apt-get update \  
+  && apt-get install -y --no-install-recommends build-essential libpq-dev git \  
+  && rm -rf /var/lib/apt/lists/*
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt \  
+    && rm -rf /tmp/requirements.txt \  
+    && useradd -U app_user \  
+    && install -d -m 0755 -o app_user -g app_user /app/static
+
+ENV DJANGO_SETTINGS_MODULE=neodb.dev
+WORKDIR /app
+USER app_user:app_user
+COPY --chown=app_user:app_user . .
+RUN chmod +x docker/*.sh
+
+# Section 6- Docker Run Checks and Configurations 
+ENTRYPOINT [ "docker/entrypoint.sh" ]
+
+CMD [ "docker/start.sh", "server" ]
\ No newline at end of file
diff --git a/README.md b/README.md
index 2cfde8ea..d9f46dbc 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,13 @@ An application allows you to mark any books, movies and more things you love.
 
 Depends on Mastodon.
 
+## Install
+Please see [doc/GUIDE.md](doc/GUIDE.md)
+
+## Bug Report
+ - to file a bug for NiceDB, please create an issue [here](https://github.com/doubaniux/boofilsic/issues/new)
+ - to file a bug or request new features for NeoDB, please contact NeoDB on [Fediverse](https://mastodon.social/@neodb) or [Twitter](https://twitter.com/NeoDBsocial)
+
 ## Contribution
 The project is based on Django. If you are familiar with this technique and willing to read through the terrible code😝, your contribution would be the most welcome!
 
@@ -11,8 +18,6 @@ Currently looking for someone to help with:
 - Explaining the structure of code
 - Refactoring (this is something big)
 
-This project is still in its early stage, so you are not encouraged to deploy it on your own. If you do want to give it a try, please check the [fork of *alphatownsman*](https://github.com/alphatownsman/boofilsic), which is more friendly.
-
 ## Sponsor
-If you like this project, please consider sponsoring us on [Patreon](https://patreon.com/tertius).
+If you like this project, please consider sponsoring NiceDB on [Patreon](https://patreon.com/tertius).
 
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..cafb3b8b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Please DM [us on Fediverse](https://mastodon.social/@neodb) or send email to `dev`@`neodb.social` to report a vulnerability. Please do not post publicly or create pr/issues directly. Thank you.
diff --git a/boofilsic/context_processors.py b/boofilsic/context_processors.py
new file mode 100644
index 00000000..6fd333b3
--- /dev/null
+++ b/boofilsic/context_processors.py
@@ -0,0 +1,5 @@
+from django.conf import settings
+
+
+def site_info(request):
+    return settings.SITE_INFO
diff --git a/boofilsic/settings.py b/boofilsic/settings.py
index 86d4abf2..6393f4a0 100644
--- a/boofilsic/settings.py
+++ b/boofilsic/settings.py
@@ -12,10 +12,13 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
 
 import os
 import psycopg2.extensions
+from git import Repo
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
+# https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
 
 # Quick-start development settings - unsuitable for production
 # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
@@ -38,6 +41,8 @@ INTERNAL_IPS = [
 
 INSTALLED_APPS = [
     'django.contrib.admin',
+    'hijack',
+    'hijack.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
@@ -45,6 +50,9 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'django.contrib.humanize',
     'django.contrib.postgres',
+    'django_sass',
+    'django_rq',
+    'simple_history',
     'markdownx',
     'management.apps.ManagementConfig',
     'mastodon.apps.MastodonConfig',
@@ -54,7 +62,12 @@ INSTALLED_APPS = [
     'movies.apps.MoviesConfig',
     'music.apps.MusicConfig',
     'games.apps.GamesConfig',
+    'sync.apps.SyncConfig',
+    'collection.apps.CollectionConfig',
+    'timeline.apps.TimelineConfig',
     'easy_thumbnails',
+    'user_messages',
+    'django_slack',
 ]
 
 MIDDLEWARE = [
@@ -65,6 +78,8 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'hijack.middleware.HijackUserMiddleware',
+    'simple_history.middleware.HistoryRequestMiddleware',
 ]
 
 ROOT_URLCONF = 'boofilsic.urls'
@@ -79,7 +94,9 @@ TEMPLATES = [
                 'django.template.context_processors.debug',
                 'django.template.context_processors.request',
                 'django.contrib.auth.context_processors.auth',
-                'django.contrib.messages.context_processors.messages',
+                # 'django.contrib.messages.context_processors.messages',
+                "user_messages.context_processors.messages",
+                'boofilsic.context_processors.site_info',
             ],
         },
     },
@@ -95,10 +112,10 @@ if DEBUG:
     DATABASES = {
         'default': {
             'ENGINE': 'django.db.backends.postgresql',
-            'NAME': 'test',
-            'USER': 'donotban',
-            'PASSWORD': 'donotbansilvousplait',
-            'HOST': '172.18.116.29',
+            'NAME': os.environ.get('DB_NAME', 'test'),
+            'USER': os.environ.get('DB_USER', 'donotban'),
+            'PASSWORD': os.environ.get('DB_PASSWORD', 'donotbansilvousplait'),
+            'HOST': os.environ.get('DB_HOST', '172.18.116.29'),
             'OPTIONS': {
                 'client_encoding': 'UTF8',
                 # 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
@@ -184,13 +201,29 @@ STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesSto
 
 AUTH_USER_MODEL = 'users.User'
 
+SILENCED_SYSTEM_CHECKS = [
+    "auth.W004",  # User.username is non-unique
+    "admin.E404"  # Required by django-user-messages
+]
+
 MEDIA_URL = '/media/'
 MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
 
+PROJECT_ROOT = os.path.abspath(os.path.dirname(__name__))
+SITE_INFO = {
+    'site_name': 'NiceDB',
+    'support_link': 'https://github.com/doubaniux/boofilsic/issues',
+    'version_hash': None,
+    'settings_module': os.getenv('DJANGO_SETTINGS_MODULE'),
+    'sentry_dsn': None,
+}
+
 # Mastodon configs
-CLIENT_NAME = 'NiceDB'
-APP_WEBSITE = 'https://nicedb.org'
-REDIRECT_URIS = "https://nicedb.org/users/OAuth2_login/\nhttps://www.nicedb.org/users/OAuth2_login/"
+CLIENT_NAME = os.environ.get('APP_NAME', 'NiceDB')
+SITE_INFO['site_name'] = os.environ.get('APP_NAME', 'NiceDB')
+APP_WEBSITE = os.environ.get('APP_URL', 'https://nicedb.org')
+REDIRECT_URIS = APP_WEBSITE + "/users/OAuth2_login/"
+
 
 # Path to save report related images, ends with slash
 REPORT_MEDIA_PATH_ROOT = 'report/'
@@ -205,10 +238,23 @@ ALBUM_MEDIA_PATH_ROOT = 'album/'
 DEFAULT_ALBUM_IMAGE = os.path.join(ALBUM_MEDIA_PATH_ROOT, 'default.svg')
 GAME_MEDIA_PATH_ROOT = 'game/'
 DEFAULT_GAME_IMAGE = os.path.join(GAME_MEDIA_PATH_ROOT, 'default.svg')
+COLLECTION_MEDIA_PATH_ROOT = 'collection/'
+DEFAULT_COLLECTION_IMAGE = os.path.join(COLLECTION_MEDIA_PATH_ROOT, 'default.svg')
+SYNC_FILE_PATH_ROOT = 'sync/'
+EXPORT_FILE_PATH_ROOT = 'export/'
+
+# Allow user to login via any Mastodon/Pleroma sites
+MASTODON_ALLOW_ANY_SITE = False
 
 # Timeout of requests to Mastodon, in seconds
 MASTODON_TIMEOUT = 30
 
+MASTODON_CLIENT_SCOPE = 'read write follow'
+#use the following if it's a new site
+#MASTODON_CLIENT_SCOPE = 'read:accounts read:follows read:search read:blocks read:mutes write:statuses write:media'
+
+MASTODON_LEGACY_CLIENT_SCOPE = 'read write follow'
+
 # Tags for toots posted from this site
 MASTODON_TAGS = '#NiceDB #NiceDB%(category)s #NiceDB%(category)s%(type)s'
 
@@ -217,7 +263,7 @@ STAR_SOLID = ':star_solid:'
 STAR_HALF = ':star_half:'
 STAR_EMPTY = ':star_empty:'
 
-# Default password for each user. since assword is not used any way,
+# Default password for each user. since password is not used any way,
 # any string that is not empty is ok
 DEFAULT_PASSWORD = 'ab7nsm8didusbaqPgq'
 
@@ -231,8 +277,12 @@ ADMIN_URL = 'tertqX7256n7ej8nbv5cwvsegdse6w7ne5rHd'
 LUMINATI_USERNAME = 'lum-customer-hl_nw4tbv78-zone-static'
 LUMINATI_PASSWORD = 'nsb7te9bw0ney'
 
+SCRAPING_TIMEOUT = 90
+
 # ScraperAPI api key
 SCRAPERAPI_KEY = 'wnb3794v675b8w475h0e8hr7tyge'
+PROXYCRAWL_KEY = None
+SCRAPESTACK_KEY = None
 
 # Spotify credentials
 SPOTIFY_CREDENTIAL = "NzYzNkYTE6MGQ0ODY0NTY2Y2b3n645sdfgAyY2I1ljYjg3Nzc0MjIwODQ0ZWE="
@@ -240,6 +290,17 @@ SPOTIFY_CREDENTIAL = "NzYzNkYTE6MGQ0ODY0NTY2Y2b3n645sdfgAyY2I1ljYjg3Nzc0MjIwODQ0
 # IMDb API service https://imdb-api.com/
 IMDB_API_KEY = "k23fwewff23"
 
+# The Movie Database (TMDB) API Keys
+TMDB_API3_KEY = "deadbeef"
+TMDB_API4_KEY = "deadbeef.deadbeef.deadbeef"
+
+# Google Books API Key
+GOOGLE_API_KEY = 'deadbeef-deadbeef-deadbeef'
+
+# IGDB
+IGDB_CLIENT_ID = 'deadbeef'
+IGDB_ACCESS_TOKEN = 'deadbeef'
+
 # Thumbnail setting
 # It is possible to optimize the image size even more: https://easy-thumbnails.readthedocs.io/en/latest/ref/optimize/
 THUMBNAIL_ALIASES = {
@@ -257,3 +318,47 @@ if DEBUG:
 
 # https://django-debug-toolbar.readthedocs.io/en/latest/
 # maybe benchmarking before deployment
+
+REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1')
+
+RQ_QUEUES = {
+    'mastodon': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'DEFAULT_TIMEOUT': -1,
+    },
+    'export': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'DEFAULT_TIMEOUT': -1,
+    },
+    'doufen': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'DEFAULT_TIMEOUT': -1,
+    }
+}
+
+RQ_SHOW_ADMIN_LINK = True
+
+SEARCH_INDEX_NEW_ONLY = False
+
+SEARCH_BACKEND = None
+
+# SEARCH_BACKEND = 'MEILISEARCH'
+# MEILISEARCH_SERVER = 'http://127.0.0.1:7700'
+# MEILISEARCH_KEY = 'deadbeef'
+
+# SEARCH_BACKEND = 'TYPESENSE'
+# TYPESENSE_CONNECTION = {
+#     'api_key': 'deadbeef',
+#     'nodes': [{
+#         'host': 'localhost',
+#         'port': '8108',
+#         'protocol': 'http'
+#     }],
+#     'connection_timeout_seconds': 2
+# }
diff --git a/boofilsic/urls.py b/boofilsic/urls.py
index dd52087a..38d74a5a 100644
--- a/boofilsic/urls.py
+++ b/boofilsic/urls.py
@@ -27,10 +27,16 @@ urlpatterns = [
     path('movies/', include('movies.urls')),
     path('music/', include('music.urls')),
     path('games/', include('games.urls')),
+    path('collections/', include('collection.urls')),
+    path('timeline/', include('timeline.urls')),
     path('sync/', include('sync.urls')),
     path('announcement/', include('management.urls')),
+    path('hijack/', include('hijack.urls')),
     path('', include('common.urls')),
+]
 
+urlpatterns += [
+    path(settings.ADMIN_URL + '-rq/', include('django_rq.urls'))
 ]
 
 if settings.DEBUG:
diff --git a/books/admin.py b/books/admin.py
index 942dccb4..75df663b 100644
--- a/books/admin.py
+++ b/books/admin.py
@@ -1,7 +1,8 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Book)
+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
index f716137a..b03e2d23 100644
--- a/books/apps.py
+++ b/books/apps.py
@@ -3,3 +3,8 @@ 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
index da7ecee6..27abda07 100644
--- a/books/forms.py
+++ b/books/forms.py
@@ -1,17 +1,12 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
-from .models import Book, BookMark, BookReview
+from .models import Book, BookMark, BookReview, BookMarkStatusTranslation
 from common.models import MarkStatusEnum
 from common.forms import *
 
 
 def BookMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在读"),
-        MarkStatusEnum.WISH.value: _("想读"),
-        MarkStatusEnum.COLLECT.value: _("读过")
-    }
-    return trans_dict[status]        
+    return BookMarkStatusTranslation[status]
 
 
 class BookForm(forms.ModelForm):
@@ -96,11 +91,8 @@ class BookMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
-        ]
-        labels = {
-            'rating': _("评分"),
-        }        
+            'visibility',
+        ]       
         widgets = {
             'book': forms.TextInput(attrs={"hidden": ""}),
         }      
@@ -115,14 +107,8 @@ class BookReviewForm(ReviewForm):
             'book',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'book': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'book': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/books/management/commands/fix-book-cover.py b/books/management/commands/fix-book-cover.py
new file mode 100644
index 00000000..ae9227b5
--- /dev/null
+++ b/books/management/commands/fix-book-cover.py
@@ -0,0 +1,200 @@
+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('不存在[^<]+</title>', 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 = '<html />'
+        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
index 4f898709..8b23e9a6 100644
--- a/books/models.py
+++ b/books/models.py
@@ -1,98 +1,184 @@
-import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
 from common.utils import GenerateDateUUIDMediaFilePath
-from boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE
-from django.utils import timezone
+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, BOOK_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.BOOK_MEDIA_PATH_ROOT)
 
 
 class Book(Entity):
     # widely recognized name, usually in Chinese
-    title = models.CharField(_("title"), max_length=200)
-    subtitle = models.CharField(_("subtitle"), blank=True, default='', max_length=200)
+    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=200)
+    orig_title = models.CharField(
+        _("original title"), blank=True, default='', max_length=500)
 
     author = postgres.ArrayField(
-        models.CharField(_("author"), blank=True, default='', max_length=100),
+        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=100),
+        models.CharField(_("translator"), blank=True,
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
     )
-    language = models.CharField(_("language"), blank=True, default='', max_length=10)
-    pub_house = models.CharField(_("publishing house"), blank=True, default='', max_length=200)
+    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=50)
+    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)
+    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=DEFAULT_BOOK_IMAGE, 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:
-        # more info: https://docs.djangoproject.com/en/2.2/ref/models/options/
-        # set managed=False if the model represents an existing table or
-        # a database view that has been created by some other means.
-        # check the link above for further info
-        # managed = True
-        # db_table = 'book'
         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'),
+            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 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)
+    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")
+            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)
+    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")
-        ]    
+            models.UniqueConstraint(
+                fields=['owner', 'book'], name="unique_book_review")
+        ]
+
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + 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)
+    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")
+            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
index fdd7ec61..de4b8ca7 100644
--- a/books/templates/books/create_update.html
+++ b/books/templates/books/create_update.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -22,8 +22,24 @@
         
             <section id="content" class="container">
                 <div class="grid">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'books:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'books:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                        {% comment %} <a href="{% url 'books:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -38,12 +54,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/create_update_review.html b/books/templates/books/create_update_review.html
index 9acae132..04cc0c3b 100644
--- a/books/templates/books/create_update_review.html
+++ b/books/templates/books/create_update_review.html
@@ -12,8 +12,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -80,7 +80,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -100,12 +100,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/delete.html b/books/templates/books/delete.html
index 5adcb246..82d2fd7a 100644
--- a/books/templates/books/delete.html
+++ b/books/templates/books/delete.html
@@ -11,8 +11,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除图书' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除图书' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if book.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' book.last_editor.id %}">
+                                    <a href="{% url 'users:home' book.last_editor.mastodon_username %}">
                                         <span>{{ book.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/delete_review.html b/books/templates/books/delete_review.html
index 8c0a7d18..7a0fad5c 100644
--- a/books/templates/books/delete_review.html
+++ b/books/templates/books/delete_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
                                             viewBox="0 0 20 20">
@@ -47,7 +47,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -90,12 +90,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/books/templates/books/detail.html b/books/templates/books/detail.html
index fd8ee1e7..2452bcf1 100644
--- a/books/templates/books/detail.html
+++ b/books/templates/books/detail.html
@@ -1,9 +1,12 @@
 {% load static %}
 {% load i18n %}
+{% load l10n %}
+{% load humanize %}
 {% load admin_url %}
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load strip_scheme %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -11,11 +14,11 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB书 - {{ book.title }}">
+    <meta property="og:title" content="{{ site_name }}书 - {{ book.title }}">
     <meta property="og:type" content="book">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ book.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
+    <meta property="og:site_name" content="{{ site_name }}">
     <meta property="og:description" content="{{ book.brief }}">
     {% if book.author %}
     <meta property="og:book:author" content="{% for author in book.author %}{{ author }}{% if not forloop.last %},{% endif %}{% endfor %}">
@@ -23,13 +26,13 @@
     {% if book.isbn %}
     <meta property="og:book:isbn" content="{{ book.isbn }}">
     {% endif %}
+
+    <title>{{ site_name }} - {% trans '书籍详情' %} | {{ book.title }}</title>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
     
-    <title>{% trans 'NiceDB - 书籍详情' %} | {{ book.title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
 </head>
 
 <body>
@@ -57,11 +60,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if book.rating %}
+                                            {% if book.rating and book.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ book.rating }} </span>
+                                            <small>({{ book.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if book.isbn %}{% trans 'ISBN:' %}{{ book.isbn }}{% endif %}</div>
@@ -96,7 +100,7 @@
                                         
                                     
                                         {% if book.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.id %}">{{ book.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.mastodon_username %}">{{ book.last_editor | default:"" }}</a></div>
                                         {% endif %}
                                             
                                         <div>
@@ -148,46 +152,27 @@
                                 
                             <div class="entity-marks">
                                 <h5 class="entity-marks__title">{% trans '这本书的标记' %}</h5>
-                                {% if mark_list_more %}
-                                <a href="{% url 'books:retrieve_mark_list' book.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
-                                {% endif %}
+                                <a href="{% url 'books:retrieve_mark_list' book.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
+                                <a href="{% url 'books:retrieve_mark_list' book.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=book %}
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这本书的评论' %}</h5>
                                 {% if review_list_more %}
-                                <a href="{% url 'books:retrieve_review_list' book.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'books:retrieve_review_list' book.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
+                                    {% if others_review.book != book %}
+                                    <span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'books:retrieve' others_review.book.id %}">{{ others_review.book.get_source_site_display }}</a></span>
+                                    {% endif %}
                                     <span class="entity-reviews__review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
                                     <span>{{ others_review.get_plain_content | truncate:100 }}</span>
                                 </li>
@@ -202,7 +187,6 @@
 
                     <div class="grid__aside" id="aside">
                         <div class="aside-section-wrapper">
-                            
                             {% if mark %}
                             <div class="mark-panel">
 
@@ -212,7 +196,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -224,7 +208,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -245,9 +229,8 @@
                                     <button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在读' %}</button>
                                     <button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '读过' %}</button>
                                 </div>
-                            </div>                            
+                            </div>
                             {% endif %}
-                                
                         </div>
                         
                         <div class="aside-section-wrapper">
@@ -255,7 +238,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -284,7 +267,53 @@
 
                             {% endif %}
                         </div>
-                                
+
+                        {% if book.get_related_books.count > 0 %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关书目' %}</div>
+                                <div >
+                                    {% for b in book.get_related_books %}
+                                    <p>
+                                        <a href="{% url 'books:retrieve' b.id %}">{{ b.title }}</a>
+                                        <small>({{ b.pub_house }} {{ b.pub_year }})</small>
+                                        <span class="source-label source-label__{{ b.source_site }}">{{ b.get_source_site_display }}</span>
+                                    </p>
+                                    {% endfor %}
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if book.isbn %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '借阅或购买' %}</div>
+                                <div class="action-panel__button-group">
+                                    <a class="action-panel__button" target="_blank" href="https://www.worldcat.org/isbn/{{ book.isbn }}">{% trans 'WorldCat' %}</a>
+                                    <a class="action-panel__button" target="_blank" href="https://openlibrary.org/search?isbn={{ book.isbn }}">{% trans 'Open Library' %}</a>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'book' book.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    {% endif %}
                     </div>
                 </div>
             </section>
@@ -296,7 +325,6 @@
     <div id="modals">
         <div class="mark-modal modal">
             <div class="mark-modal__head">
-                
                 {% if not mark %}
                 <style>
                     .mark-modal__title::after {
@@ -313,12 +341,12 @@
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                             <polygon
                             points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
-                        </polygon>
-                    </svg>
+                            </polygon>
+                        </svg>
+                    </span>
                 </span>
-            </span>
-        </div>
-        <div class="mark-modal__body">
+            </div>
+            <div class="mark-modal__body">
                 <form action="{% url 'books:create_update_mark' %}" method="post">
                     {{ mark_form.media }}
                     {% csrf_token %}
@@ -344,8 +372,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:
+                            {{ mark_form.visibility }}</span>
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/books/templates/books/mark_list.html b/books/templates/books/mark_list.html
index 46c5acf1..fe96c4ad 100644
--- a/books/templates/books/mark_list.html
+++ b/books/templates/books/mark_list.html
@@ -12,8 +12,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ book.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ book.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -33,38 +33,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>{% trans ' 的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-        
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-   
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                    </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-                                        
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=book %}
                             </div>
                             <div class="pagination">
                             
@@ -132,12 +101,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
   
diff --git a/books/templates/books/review_detail.html b/books/templates/books/review_detail.html
index 76a53aaf..b4046532 100644
--- a/books/templates/books/review_detail.html
+++ b/books/templates/books/review_detail.html
@@ -11,17 +11,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB书评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}书评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ book.cover|thumb:'normal' }}">
+    <title>{{ site_name }}{% trans '书评' %} - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -37,7 +38,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -46,7 +47,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -71,6 +72,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -112,16 +114,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/books/templates/books/review_list.html b/books/templates/books/review_list.html
index d7fad68b..a7682b0b 100644
--- a/books/templates/books/review_list.html
+++ b/books/templates/books/review_list.html
@@ -12,8 +12,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ book.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ book.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -39,12 +39,14 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
-                                        
+                                            {% if review.book != book %}
+                                            <span class="entity-reviews__review-time source-label"><a href="{% url 'books:retrieve' review.book.id %}" class="entity-reviews__review-time">{{ review.book.get_source_site_display }}</a></span>
+                                            {% endif %}
         
                                         <span href="{% url 'books:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'books:retrieve_review' review.id %}">{{ review.title }}</a></span>
                                             
@@ -119,12 +121,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/scrape.html b/books/templates/books/scrape.html
index 6ad5a8a1..4f0adcea 100644
--- a/books/templates/books/scrape.html
+++ b/books/templates/books/scrape.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/books/urls.py b/books/urls.py
index 7ca4d24e..518ea096 100644
--- a/books/urls.py
+++ b/books/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -8,8 +8,10 @@ urlpatterns = [
     path('<int:id>/', retrieve, name='retrieve'),
     path('update/<int:id>/', update, name='update'),
     path('delete/<int:id>/', delete, name='delete'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('mark/', create_update_mark, name='create_update_mark'),
-    path('<int:book_id>/mark/list/', retrieve_mark_list, name='retrieve_mark_list'),
+    path('wish/<int:id>/', wish, name='wish'),
+    re_path('(?P<book_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
     path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
     path('<int:book_id>/review/create/', create_review, name='create_review'),
     path('review/update/<int:id>/', update_review, name='update_review'),
diff --git a/books/views.py b/books/views.py
index a06c31cf..10d31b77 100644
--- a/books/views.py
+++ b/books/views.py
@@ -2,22 +2,24 @@ 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
+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.api import check_visibility, post_toot, TootVisibilityEnum
-from mastodon.utils import rating_to_emoji
+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
+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 boofilsic.settings import MASTODON_TAGS
+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__)
@@ -88,6 +90,18 @@ def create(request):
         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':
@@ -98,6 +112,7 @@ def update(request, id):
             'books/create_update.html',
             {
                 'form': form,
+                'is_update': True,
                 'title': _('修改书籍'),
                 'submit_url': reverse("books:update", args=[book.id]),
                 # provided for frontend js
@@ -126,6 +141,7 @@ def update(request, id):
                 'books/create_update.html',
                 {
                     'form': form,
+                    'is_update': True,
                     'title': _('修改书籍'),
                     'submit_url': reverse("books:update", args=[book.id]),
                     # provided for frontend js
@@ -166,6 +182,7 @@ def retrieve(request, id):
         else:
             mark_form = BookMarkForm(initial={
                 'book': book,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -184,10 +201,8 @@ def retrieve(request, id):
             mark_list_more = None
             review_list_more = None
         else:
-            mark_list = BookMark.get_available(
-                book, request.user, request.session['oauth_token'])
-            review_list = BookReview.get_available(
-                book, request.user, request.session['oauth_token'])
+            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:
@@ -195,6 +210,7 @@ def retrieve(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(book=book)))
 
         # def strip_html_tags(text):
         #     import re
@@ -219,6 +235,7 @@ def retrieve(request, id):
                 'review_list_more': review_list_more,
                 'book_tag_list': book_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -263,12 +280,19 @@ def create_update_mark(request):
         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:
@@ -276,13 +300,13 @@ def create_update_mark(request):
             form = BookMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -304,27 +328,10 @@ def create_update_mark(request):
                 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("books:retrieve",
-                                                                args=[book.id])
-                words = BookMarkStatusTranslator(form.cleaned_data['status']) +\
-                    f"《{book.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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("books:retrieve", args=[form.instance.book.id]))
     else:
@@ -333,11 +340,30 @@ def create_update_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_mark_list(request, book_id):
+def wish(request, id):
+    if request.method == 'POST':
+        book = get_object_or_404(Book, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            '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(
-            book, request.user, request.session['oauth_token'])
+        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)
@@ -398,23 +424,8 @@ def create_review(request, book_id):
             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("books:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.book.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -450,22 +461,8 @@ def update_review(request, id):
             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("books:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.book.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -500,11 +497,10 @@ def delete_review(request, id):
 
 
 @mastodon_request_included
-@login_required
 def retrieve_review(request, id):
     if request.method == 'GET':
         review = get_object_or_404(BookReview, pk=id)
-        if not check_visibility(review, request.session['oauth_token'], request.user):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -539,8 +535,7 @@ def retrieve_review(request, id):
 def retrieve_review_list(request, book_id):
     if request.method == 'GET':
         book = get_object_or_404(Book, pk=book_id)
-        queryset = BookReview.get_available(
-            book, request.user, request.session['oauth_token'])
+        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)
diff --git a/collection/__init__.py b/collection/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/collection/admin.py b/collection/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/collection/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/collection/apps.py b/collection/apps.py
new file mode 100644
index 00000000..7edc77d1
--- /dev/null
+++ b/collection/apps.py
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 00000000..ed84f5a1
--- /dev/null
+++ b/collection/forms.py
@@ -0,0 +1,45 @@
+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
new file mode 100644
index 00000000..d079ec60
--- /dev/null
+++ b/collection/models.py
@@ -0,0 +1,126 @@
+from django.db import models
+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 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 settings.APP_WEBSITE + 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
new file mode 100644
index 00000000..8ce1cae8
--- /dev/null
+++ b/collection/templates/add_to_list.html
@@ -0,0 +1,45 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
+    <div class="modal-underlay" _="on click trigger closeModal"></div>
+    <div class="modal-content">
+        <div class="add-to-list-modal__head">
+            <span class="add-to-list-modal__title">{% trans '添加到收藏单' %}</span>
+            <span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
+                <span class="icon-cross">
+                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                        <polygon
+                        points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
+                        </polygon>
+                    </svg>
+                </span>
+            </span>
+        </div>
+        <div class="add-to-list-modal__body">
+            <form action="/collections/add_to_list/{{ type }}/{{ id }}/" method="post">
+                {% csrf_token %}
+                <select name="collection_id">
+                    {% for collection in collections %}
+                    <option value="{{ collection.id }}">{{ collection.title }}{% if collection.visibility > 0 %}🔒{% endif %}</option>
+                    {% endfor %}
+                    <option value="0">新建收藏单</option>
+                </select>
+                <div>
+                    <textarea type="text" name="comment" placeholder="条目备注"></textarea>
+                </div>
+                <div class="add-to-list-modal__confirm-button">
+                    <input type="submit" class="button float-right" value="{% trans '提交' %}">
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
diff --git a/collection/templates/create_update.html b/collection/templates/create_update.html
new file mode 100644
index 00000000..43e82dcb
--- /dev/null
+++ b/collection/templates/create_update.html
@@ -0,0 +1,71 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <style type="text/css">
+        #id_collaborative li, #id_visibility li {display: inline-block !important;}
+    </style>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        {% include "partial/_navbar.html" %}
+        <div id="content-wrapper">
+            <section id="content" class="container">
+                <div class="grid">
+                    <div class="single-section-wrapper" id="main">
+                        <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
+                            {% csrf_token %}
+                            {{ form }}
+                            <input class="button" type="submit" value="{% trans '提交' %}">
+                        </form>
+                        {{ form.media }}
+                    </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+
+    <script>
+        // mark required
+        $("#content *[required]").each(function () {
+            $(this).prev().prepend("*");
+        });
+
+        // when source site is this site, hide url input box and populate it with fake url
+        // the backend would update this field
+        if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
+            $("input[name='source_url']").hide();
+            $("label[for='id_source_url']").hide();
+            $("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
+        }
+        $("select[name='source_site']").change(function () {
+            let value = $(this).val();
+            if (value == "{{ this_site_enum_value }}") {
+                $("input[name='source_url']").hide();
+                $("label[for='id_source_url']").hide();
+                $("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
+            } else {
+                $("input[name='source_url']").show();
+                $("label[for='id_source_url']").show();
+                $("input[name='source_url']").val("");
+            }
+        });
+
+    </script>
+</body>
+
+
+</html>
\ No newline at end of file
diff --git a/collection/templates/delete.html b/collection/templates/delete.html
new file mode 100644
index 00000000..ee6d440d
--- /dev/null
+++ b/collection/templates/delete.html
@@ -0,0 +1,117 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
+    <meta property="og:description" content="{{ collection.description }}">
+    <meta property="og:type" content="article">
+    <meta property="og:article:author" content="{{ collection.owner.username }}">
+    <meta property="og:url" content="{{ request.build_absolute_uri }}">
+    <meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
+    <title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+        
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="review-head">
+                                <h5 class="review-head__title">
+                                    确认删除收藏单「{{ collection.title }}」吗?
+                                </h5>
+                                {% if collection.visibility > 0 %}
+                                <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                            <path
+                                                d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                                            </svg></span>
+                                {% endif %}
+                                <div class="review-head__body">
+                                    <div class="review-head__info">
+                                      
+                                            <a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
+                                            
+                                                
+                                            <span class="review-head__time">{{ collection.edited_time }}</span>
+                                            
+                                    </div>
+                                    <div class="review-head__actions">
+                                    </div>
+                                </div>
+                                <div id="rawContent">
+                                    {{ form.description }}
+                                </div>
+                                {{ form.media }}
+                                <div class="dividing-line"></div>
+                                <div class="clearfix">
+                                    <form action="{% url 'collection:delete' collection.id %}" method="post" class="float-right">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '确认' %}">
+                                    </form>
+                                    <button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
+                                </div>
+                                <!-- <div class="dividing-line"></div> -->
+                                <!-- <div class="entity-card__img-wrapper" style="text-align: center;">
+                                    <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                </div> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="grid__aside" id="aside">
+                        <div class="aside-section-wrapper">
+                            <div class="entity-card">
+                                <div class="entity-card__img-wrapper">
+                                    <a href="{% url 'collection:retrieve' collection.id %}">
+                                        <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                    </a>
+                                </div>
+                                <div class="entity-card__info-wrapper">
+                                    <h5 class="entity-card__title">
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            {{ collection.title }}
+                                        </a>
+                                    </h5>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+    <script>
+        $(".markdownx textarea").hide();
+    </script>
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+</body>
+
+
+</html>
diff --git a/collection/templates/detail.html b/collection/templates/detail.html
new file mode 100644
index 00000000..d78513b9
--- /dev/null
+++ b/collection/templates/detail.html
@@ -0,0 +1,147 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
+    <meta property="og:description" content="{{ collection.description }}">
+    <meta property="og:type" content="article">
+    <meta property="og:article:author" content="{{ collection.owner.username }}">
+    <meta property="og:url" content="{{ request.build_absolute_uri }}">
+    <meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
+
+    <title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+        
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="review-head">
+                                <h5 class="review-head__title">
+                                    {{ collection.title }}
+                                </h5>
+                                {% if collection.visibility > 0 %}
+                                <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                            <path
+                                                d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                                            </svg></span>
+                                {% endif %}
+                                <div class="review-head__body">
+                                    <div class="review-head__info">
+                                      
+                                            <a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
+                                            
+                                                
+                                            <span class="review-head__time">{{ collection.edited_time }}</span>
+                                            
+                                    </div>
+                                    <div class="review-head__actions">
+                                            {% if request.user == collection.owner %}
+                                            <a class="review-head__action-link" href="{% url 'collection:update' collection.id %}">{% trans '编辑' %}</a>
+                                            <a class="review-head__action-link" href="{% url 'collection:delete' collection.id %}">{% trans '删除' %}</a>
+                                            {% elif editable %}
+                                            <span class="review-head__time">可协作整理</span>
+                                            {% endif %}
+                                    </div>
+                                </div>
+                                <!-- <div class="dividing-line"></div> -->
+                                <!-- <div class="entity-card__img-wrapper" style="text-align: center;">
+                                    <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                </div> -->
+                                <div id="rawContent">
+                                    {{ form.description }}
+                                </div>
+                                {{ form.media }}
+                            </div>
+                            <div class="entity-list" hx-get="{% url 'collection:retrieve_entity_list' collection.id %}" hx-trigger="load">
+                            </div>
+                        </div>
+                    </div>
+                    <div class="grid__aside" id="aside">
+                        <div class="aside-section-wrapper">
+                            <div class="entity-card">
+                                <div class="entity-card__img-wrapper">
+                                    <a href="{% url 'collection:retrieve' collection.id %}">
+                                        <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                    </a>
+                                </div>
+                                <div class="entity-card__info-wrapper">
+                                    <h5 class="entity-card__title">
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            {{ collection.title }}
+                                        </a>
+                                    </h5>
+                                </div>
+                            </div>
+                        </div>
+
+                        {% if request.user != collection.owner %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__button-group action-panel__button-group--center">
+                                    {% if following %}
+                                    <form action="{% url 'collection:unfollow' collection.id %}" method="post">
+                                        {% csrf_token %}
+                                        <button class="action-panel__button">{% trans '取消关注' %}</button>
+                                    </form>
+                                    {% else %}
+                                    <form action="{% url 'collection:follow' collection.id %}" method="post">
+                                        {% csrf_token %}
+                                        <button class="action-panel__button">{% trans '关注' %}</button>
+                                    </form>
+                                    {% endif %}                                         
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__button-group action-panel__button-group--center">
+                                    <form>
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:share' collection.id %}" hx-target="body" hx-swap="beforeend">{% trans '分享到联邦网络' %}</button>
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+    <script>
+        $(".markdownx textarea").hide();
+    </script>
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+</body>
+
+
+</html>
diff --git a/collection/templates/edit_item_comment.html b/collection/templates/edit_item_comment.html
new file mode 100644
index 00000000..674985e3
--- /dev/null
+++ b/collection/templates/edit_item_comment.html
@@ -0,0 +1,5 @@
+<form hx-post="{% url 'collection:update_item_comment' collection.id collectionitem.id %}">
+		<input name="comment" value="{{ collectionitem.comment }}">
+		<input type="submit" style="width:unset;" value="修改">
+		<button style="width:unset;" hx-get="{% url 'collection:show_item_comment' collection.id collectionitem.id %}">取消</button>
+</form>
\ No newline at end of file
diff --git a/collection/templates/entity_list.html b/collection/templates/entity_list.html
new file mode 100644
index 00000000..4cdc9ee2
--- /dev/null
+++ b/collection/templates/entity_list.html
@@ -0,0 +1,21 @@
+{% load thumb %}
+{% load i18n %}
+{% load l10n %}
+<ul class="entity-list__entities">
+    {% for collectionitem in collection.collectionitem_list %}
+    {% if collectionitem.item is not None %}
+    {% include "partial/list_item.html" with item=collectionitem.item %}
+    {% endif %}
+    {% empty %}
+    {% endfor %}
+    {% if editable %}
+    <li>
+        <form hx-target=".entity-list" hx-post="{% url 'collection:append_item' form.instance.id %}" method="POST">
+            {% csrf_token %}
+            <input type="url" name="url" placeholder="https://neodb.social/movies/1/" style="min-width:24rem" required>
+            <input type="text" name="comment" placeholder="{% trans '备注' %}" style="min-width:24rem">
+            <input class="button" type="submit" value="{% trans '添加' %}" >
+        </form>
+    </li>
+    {% endif %}
+</ul>
diff --git a/collection/templates/list.html b/collection/templates/list.html
new file mode 100644
index 00000000..0c9c75c3
--- /dev/null
+++ b/collection/templates/list.html
@@ -0,0 +1,99 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="entity-reviews">
+                                <h5 class="entity-reviews__title entity-reviews__title--stand-alone">
+                                    {{ title }}
+                                </h5>
+                                <ul class="entity-reviews__review-list">
+
+                                    {% for collection in collections %}
+
+                                    <li class="entity-reviews__review entity-reviews__review--wider">
+                                        <img src="{{ collection.cover|thumb:'normal' }}" style="width:40px; float:right"class="entity-card__img">
+                                        <span class="entity-reviews__review-title"><a href="{% url 'collection:retrieve' collection.id %}">{{ collection.title }}</a></span>
+                                        <span class="entity-reviews__review-time">{{ collection.edited_time }}</span>
+                                        {% if collection.visibility > 0 %}
+                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
+                                        {% endif %}
+                                    </li>
+                                    {% empty %}
+                                    <div>{% trans '无结果' %}</div>
+                                    {% endfor %}
+
+                                </ul>
+                            </div>
+                            <div class="pagination">
+
+                                {% if collections.pagination.has_prev %}
+                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                <a href="?page={{ collections.previous_page_number }}"
+                                class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                {% endif %}
+
+                                {% for page in collections.pagination.page_range %}
+
+                                {% if page == collections.pagination.current_page %}
+                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                {% else %}
+                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
+                                {% endif %}
+
+                                {% endfor %}
+
+                                {% if collections.pagination.has_next %}
+                                <a href="?page={{ collections.next_page_number }}"
+                                class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                <a href="?page={{ collections.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
+                                {% endif %}
+
+                            </div>
+                        </div>
+                    </div>
+
+
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+
+    <script>
+
+    </script>
+</body>
+
+
+</html>
diff --git a/collection/templates/share_collection.html b/collection/templates/share_collection.html
new file mode 100644
index 00000000..dc8e3beb
--- /dev/null
+++ b/collection/templates/share_collection.html
@@ -0,0 +1,56 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
+    <div class="modal-underlay" _="on click trigger closeModal"></div>
+    <div class="modal-content">
+        <div class="add-to-list-modal__head">
+            <span class="add-to-list-modal__title">{% trans '分享收藏单' %}</span>
+            <span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
+                <span class="icon-cross">
+                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                        <polygon
+                        points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
+                        </polygon>
+                    </svg>
+                </span>
+            </span>
+        </div>
+        <div class="add-to-list-modal__body">
+            <form action="/collections/share/{{ id }}/" method="post">
+                {% csrf_token %}
+                <div>
+                <label for="id_visibility_0">分享可见性(不同于收藏单本身的权限):</label>
+                <ul id="id_visibility">
+                    <li><label for="id_visibility_0"><input type="radio" name="visibility" value="0" required="" id="id_visibility_0" {% if visibility == 0 %}checked{% endif %}>
+                 公开</label>
+
+                </li>
+                    <li><label for="id_visibility_1"><input type="radio" name="visibility" value="1" required="" id="id_visibility_1" {% if visibility == 1 %}checked{% endif %}>
+                 仅关注者</label>
+
+                </li>
+                    <li><label for="id_visibility_2"><input type="radio" name="visibility" value="2" required="" id="id_visibility_2" {% if visibility == 2 %}checked{% endif %}>
+                 仅自己</label>
+
+                </li>
+                </ul>
+                </div>
+                <div>
+                    <textarea type="text" name="comment" placeholder="分享附言"></textarea>
+                </div>
+                <div class="add-to-list-modal__confirm-button">
+                    <input type="submit" class="button float-right" value="{% trans '提交' %}">
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
diff --git a/collection/templates/show_item_comment.html b/collection/templates/show_item_comment.html
new file mode 100644
index 00000000..f9add2bd
--- /dev/null
+++ b/collection/templates/show_item_comment.html
@@ -0,0 +1,4 @@
+{{ collectionitem.comment }} 
+{% if editable %}
+<a class="action-icon" hx-get="{% url 'collection:update_item_comment' collection.id collectionitem.id %}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19,20H5a1,1,0,0,0,0,2H19a1,1,0,0,0,0-2Z"/><path d="M5,18h.09l4.17-.38a2,2,0,0,0,1.21-.57l9-9a1.92,1.92,0,0,0-.07-2.71h0L16.66,2.6A2,2,0,0,0,14,2.53l-9,9a2,2,0,0,0-.57,1.21L4,16.91a1,1,0,0,0,.29.8A1,1,0,0,0,5,18ZM15.27,4,18,6.73,16,8.68,13.32,6Zm-8.9,8.91L12,7.32l2.7,2.7-5.6,5.6-3,.28Z"/></g></svg></a>
+{% endif %}
\ No newline at end of file
diff --git a/collection/tests.py b/collection/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/collection/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/collection/urls.py b/collection/urls.py
new file mode 100644
index 00000000..f3c62663
--- /dev/null
+++ b/collection/urls.py
@@ -0,0 +1,27 @@
+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('<int:id>/', retrieve, name='retrieve'),
+    path('<int:id>/entity_list', retrieve_entity_list, name='retrieve_entity_list'),
+    path('update/<int:id>/', update, name='update'),
+    path('delete/<int:id>/', delete, name='delete'),
+    path('follow/<int:id>/', follow, name='follow'),
+    path('unfollow/<int:id>/', unfollow, name='unfollow'),
+    path('<int:id>/append_item/', append_item, name='append_item'),
+    path('<int:id>/delete_item/<int:item_id>', delete_item, name='delete_item'),
+    path('<int:id>/move_up_item/<int:item_id>', move_up_item, name='move_up_item'),
+    path('<int:id>/move_down_item/<int:item_id>', move_down_item, name='move_down_item'),
+    path('<int:id>/update_item_comment/<int:item_id>', update_item_comment, name='update_item_comment'),
+    path('<int:id>/show_item_comment/<int:item_id>', show_item_comment, name='show_item_comment'),
+    path('with/<str:type>/<int:id>/', list_with, name='list_with'),
+    path('add_to_list/<str:type>/<int:id>/', add_to_list, name='add_to_list'),
+    path('share/<int:id>/', share, name='share'),
+    path('follow2/<int:id>/', wish, name='wish'),
+
+    # TODO: tag
+]
diff --git a/collection/views.py b/collection/views.py
new file mode 100644
index 00000000..659cb586
--- /dev/null
+++ b/collection/views.py
@@ -0,0 +1,442 @@
+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 post_toot, TootVisibilityEnum, 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)
+
+        if request.user.is_authenticated:
+            following = True if CollectionMark.objects.filter(owner=request.user, collection=collection).first() is not None else False
+            followers = []
+        else:
+            following = False
+            followers = []
+
+        return render(
+            request,
+            'detail.html',
+            {
+                'collection': collection,
+                'form': form,
+                'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
+                'followers': followers,
+                'following': following,
+            }
+        )
+    else:
+        logger.warning('non-GET method at /collections/<id>')
+        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)
+
+    followers = []
+    if request.user.is_authenticated:
+        followers = []
+
+    return render(
+        request,
+        'entity_list.html',
+        {
+            'collection': collection,
+            'form': form,
+            'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
+            'followers': followers,
+
+        }
+    )
+
+
+@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/common/forms.py b/common/forms.py
index 5372ce12..849b31c4 100644
--- a/common/forms.py
+++ b/common/forms.py
@@ -1,4 +1,5 @@
 from django import forms
+from markdownx.fields import MarkdownxFormField
 import django.contrib.postgres.forms as postgres
 from django.utils import formats
 from django.core.exceptions import ValidationError
@@ -45,7 +46,7 @@ class HstoreInput(forms.Widget):
         js = ('js/key_value_input.js',)
 
 
-class JSONField(postgres.JSONField):
+class JSONField(forms.fields.JSONField):
     widget = KeyValueInput
     def to_python(self, value):
         if not value:
@@ -88,7 +89,7 @@ class RatingValidator:
                 _('%(value)s is not an integer'),
                 params={'value': value},
             )
-        if not str(value) in [str(i) for i in range(1, 11)]:
+        if not str(value) in [str(i) for i in range(0, 11)]:
             raise ValidationError(
                 _('%(value)s is not an integer in range 1-10'),
                 params={'value': value},
@@ -154,9 +155,9 @@ class MultiSelect(forms.SelectMultiple):
 
     class Media:
         css = {
-            'all': ('lib/css/multiple-select.min.css',)
+            'all': ('https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.css',)
         }
-        js = ('lib/js/multiple-select.min.js',)
+        js = ('https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.js',)
 
 
 class HstoreField(forms.CharField):
@@ -223,22 +224,25 @@ class DurationField(forms.TimeField):
 #############################
 # Form
 #############################
+VISIBILITY_CHOICES = [
+    (0, _("公开")),
+    (1, _("仅关注者")),
+    (2, _("仅自己")),
+]
 
-class MarkForm(forms.ModelForm):
-    IS_PRIVATE_CHOICES = [
-        (True, _("仅关注者")),
-        (False, _("公开")),
-    ]
-    
+
+class MarkForm(forms.ModelForm):    
     id = forms.IntegerField(required=False, widget=forms.HiddenInput())
     share_to_mastodon = forms.BooleanField(
-        label=_("分享到长毛象"), initial=True, required=False)
+        label=_("分享到联邦网络"), initial=True, required=False)
     rating = forms.IntegerField(
-        validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
-    is_private = RadioBooleanField(
+        label=_("评分"), validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
+    visibility = forms.TypedChoiceField(
         label=_("可见性"),
-        initial=True,
-        choices=IS_PRIVATE_CHOICES
+        initial=0,
+        coerce=int,
+        choices=VISIBILITY_CHOICES,
+        widget=forms.RadioSelect
     )
     tags = TagField(
         required=False,
@@ -259,15 +263,15 @@ class MarkForm(forms.ModelForm):
 
 
 class ReviewForm(forms.ModelForm):
-    IS_PRIVATE_CHOICES = [
-        (True, _("仅关注者")),
-        (False, _("公开")),
-    ]
+    title = forms.CharField(label=_("标题"))
+    content = MarkdownxFormField(label=_("正文 (Markdown)"))
     share_to_mastodon = forms.BooleanField(
-        label=_("分享到长毛象"), initial=True, required=False)
+        label=_("分享到联邦网络"), initial=True, required=False)
     id = forms.IntegerField(required=False, widget=forms.HiddenInput())
-    is_private = RadioBooleanField(
+    visibility = forms.TypedChoiceField(
         label=_("可见性"),
-        initial=True,
-        choices=IS_PRIVATE_CHOICES
+        initial=0,
+        coerce=int,
+        choices=VISIBILITY_CHOICES,
+        widget=forms.RadioSelect
     )
diff --git a/common/importers/douban.py b/common/importers/douban.py
new file mode 100644
index 00000000..321cea8f
--- /dev/null
+++ b/common/importers/douban.py
@@ -0,0 +1,270 @@
+import openpyxl
+import requests
+import re
+from lxml import html
+from markdownify import markdownify as md
+from datetime import datetime
+from common.scraper import get_scraper_by_url
+import logging
+import pytz
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from user_messages import api as msg
+import django_rq
+from common.utils import GenerateDateUUIDMediaFilePath
+import os
+from books.models import BookReview, Book, BookMark, BookTag
+from movies.models import MovieReview, Movie, MovieMark, MovieTag
+from music.models import AlbumReview, Album, AlbumMark, AlbumTag
+from games.models import GameReview, Game, GameMark, GameTag
+from common.scraper import DoubanAlbumScraper, DoubanBookScraper, DoubanGameScraper, DoubanMovieScraper
+from PIL import Image
+from io import BytesIO
+import filetype
+from common.models import MarkStatusEnum
+
+
+logger = logging.getLogger(__name__)
+tz_sh = pytz.timezone('Asia/Shanghai')
+
+
+def fetch_remote_image(url):
+    try:
+        print(f'fetching remote image {url}')
+        raw_img = None
+        ext = None
+        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}'
+        else:
+            dl_url = url
+        img_response = requests.get(dl_url, timeout=settings.SCRAPING_TIMEOUT)
+        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
+        f = GenerateDateUUIDMediaFilePath(None, "x." + ext, settings.MARKDOWNX_MEDIA_PATH)
+        file = settings.MEDIA_ROOT + f
+        local_url = settings.MEDIA_URL + f
+        os.makedirs(os.path.dirname(file), exist_ok=True)
+        img.save(file)
+        # print(f'remote image saved as {local_url}')
+        return local_url
+    except Exception:
+        print(f'unable to fetch remote image {url}')
+        return url
+
+
+class DoubanImporter:
+    total = 0
+    processed = 0
+    skipped = 0
+    imported = 0
+    failed = []
+    user = None
+    visibility = 0
+    file = None
+
+    def __init__(self, user, visibility):
+        self.user = user
+        self.visibility = visibility
+
+    def update_user_import_status(self, status):
+        self.user.preference.import_status['douban_pending'] = status
+        self.user.preference.import_status['douban_file'] = self.file
+        self.user.preference.import_status['douban_visibility'] = self.visibility
+        self.user.preference.import_status['douban_total'] = self.total
+        self.user.preference.import_status['douban_processed'] = self.processed
+        self.user.preference.import_status['douban_skipped'] = self.skipped
+        self.user.preference.import_status['douban_imported'] = self.imported
+        self.user.preference.import_status['douban_failed'] = self.failed
+        self.user.preference.save(update_fields=['import_status'])
+
+    def import_from_file(self, uploaded_file):
+        try:
+            wb = openpyxl.open(uploaded_file, read_only=True, data_only=True, keep_links=False)
+            wb.close()
+            file = settings.MEDIA_ROOT + GenerateDateUUIDMediaFilePath(None, "x.xlsx", settings.SYNC_FILE_PATH_ROOT)
+            os.makedirs(os.path.dirname(file), exist_ok=True)
+            with open(file, 'wb') as destination:
+                for chunk in uploaded_file.chunks():
+                    destination.write(chunk)
+            self.file = file
+            self.update_user_import_status(2)
+            jid = f'Douban_{self.user.id}_{os.path.basename(self.file)}'
+            django_rq.get_queue('doufen').enqueue(self.import_from_file_task, job_id=jid)
+        except Exception:
+            return False
+        # self.import_from_file_task(file, user, visibility)
+        return True
+
+    mark_sheet_config = {
+        '想读': [MarkStatusEnum.WISH, DoubanBookScraper, Book, BookMark, BookTag],
+        '在读': [MarkStatusEnum.DO, DoubanBookScraper, Book, BookMark, BookTag],
+        '读过': [MarkStatusEnum.COLLECT, DoubanBookScraper, Book, BookMark, BookTag],
+        '想看': [MarkStatusEnum.WISH, DoubanMovieScraper, Movie, MovieMark, MovieTag],
+        '在看': [MarkStatusEnum.DO, DoubanMovieScraper, Movie, MovieMark, MovieTag],
+        '想看': [MarkStatusEnum.COLLECT, DoubanMovieScraper, Movie, MovieMark, MovieTag],
+        '想听': [MarkStatusEnum.WISH, DoubanAlbumScraper, Album, AlbumMark, AlbumTag],
+        '在听': [MarkStatusEnum.DO, DoubanAlbumScraper, Album, AlbumMark, AlbumTag],
+        '听过': [MarkStatusEnum.COLLECT, DoubanAlbumScraper, Album, AlbumMark, AlbumTag],
+        '想玩': [MarkStatusEnum.WISH, DoubanGameScraper, Game, GameMark, GameTag],
+        '在玩': [MarkStatusEnum.DO, DoubanGameScraper, Game, GameMark, GameTag],
+        '玩过': [MarkStatusEnum.COLLECT, DoubanGameScraper, Game, GameMark, GameTag],
+    }
+    review_sheet_config = {
+        '书评': [DoubanBookScraper, Book, BookReview],
+        '影评': [DoubanMovieScraper, Movie, MovieReview],
+        '乐评': [DoubanAlbumScraper, Album, AlbumReview],
+        '游戏评论&攻略': [DoubanGameScraper, Game, GameReview],
+    }
+    mark_data = {}
+    review_data = {}
+    entity_lookup = {}
+
+    def load_sheets(self):
+        f = open(self.file, 'rb')
+        wb = openpyxl.load_workbook(f, read_only=True, data_only=True, keep_links=False)
+        for data, config in [(self.mark_data, self.mark_sheet_config), (self.review_data, self.review_sheet_config)]:
+            for name in config:
+                data[name] = []
+                if name in wb:
+                    print(f'{self.user} parsing {name}')
+                    for row in wb[name].iter_rows(min_row=2, values_only=True):
+                        cells = [cell for cell in row]
+                        if len(cells) > 6:
+                            data[name].append(cells)
+        for sheet in self.mark_data.values():
+            for cells in sheet:
+                # entity_lookup["title|rating"] = [(url, time), ...]
+                k = f'{cells[0]}|{cells[5]}'
+                v = (cells[3], cells[4])
+                if k in self.entity_lookup:
+                    self.entity_lookup[k].append(v)
+                else:
+                    self.entity_lookup[k] = [v]
+        self.total = sum(map(lambda a: len(a), self.review_data.values()))
+
+    def guess_entity_url(self, title, rating, timestamp):
+        k = f'{title}|{rating}'
+        if k not in self.entity_lookup:
+            return None
+        v = self.entity_lookup[k]
+        if len(v) > 1:
+            v.sort(key=lambda c: abs(timestamp - (datetime.strptime(c[1], "%Y-%m-%d %H:%M:%S") if type(c[1])==str else c[1]).replace(tzinfo=tz_sh)))
+        return v[0][0]
+        # for sheet in self.mark_data.values():
+        #     for cells in sheet:
+        #         if cells[0] == title and cells[5] == rating:
+        #             return cells[3]
+
+    def import_from_file_task(self):
+        print(f'{self.user} import start')
+        msg.info(self.user, f'开始导入豆瓣评论')
+        self.update_user_import_status(1)
+        self.load_sheets()
+        print(f'{self.user} sheet loaded, {self.total} lines total')
+        self.update_user_import_status(1)
+        for name, param in self.review_sheet_config.items():
+            self.import_review_sheet(self.review_data[name], param[0], param[1], param[2])
+        self.update_user_import_status(0)
+        msg.success(self.user, f'豆瓣评论导入完成,共处理{self.total}篇,已存在{self.skipped}篇,新增{self.imported}篇。')
+        if len(self.failed):
+            msg.error(self.user, f'豆瓣评论导入时未能处理以下网址:\n{" , ".join(self.failed)}')
+
+    def import_review_sheet(self, worksheet, scraper, entity_class, review_class):
+        prefix = f'{self.user} |'
+        if worksheet is None:  # or worksheet.max_row < 2:
+            print(f'{prefix} {review_class.__name__} empty sheet')
+            return
+        for cells in worksheet:
+            if len(cells) < 6:
+                continue
+            title = cells[0]
+            entity_title = re.sub('^《', '', re.sub('》$', '', cells[1]))
+            review_url = cells[2]
+            time = cells[3]
+            rating = cells[4]
+            content = cells[6]
+            self.processed += 1
+            if time:
+                if type(time) == str:
+                    time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S")
+                time = time.replace(tzinfo=tz_sh)
+            else:
+                time = None
+            if not content:
+                content = ""
+            if not title:
+                title = ""
+            r = self.import_review(entity_title, rating, title, review_url, content, time, scraper, entity_class, review_class)
+            if r == 1:
+                self.imported += 1
+            elif r == 2:
+                self.skipped += 1
+            else:
+                self.failed.append(review_url)
+            self.update_user_import_status(1)
+
+    def import_review(self, entity_title, rating, title, review_url, content, time, scraper, entity_class, review_class):
+        # return 1: done / 2: skipped / None: failed
+        prefix = f'{self.user} |'
+        url = self.guess_entity_url(entity_title, rating, time)
+        if url is None:
+            print(f'{prefix} fetching {review_url}')
+            try:
+                if settings.SCRAPESTACK_KEY is not None:
+                    _review_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={review_url}'
+                else:
+                    _review_url = review_url
+                r = requests.get(_review_url, timeout=settings.SCRAPING_TIMEOUT)
+                if r.status_code != 200:
+                    print(f'{prefix} fetching error {review_url} {r.status_code}')
+                    return
+                h = html.fromstring(r.content.decode('utf-8'))
+                for u in h.xpath("//header[@class='main-hd']/a/@href"):
+                    if '.douban.com/subject/' in u:
+                        url = u
+                if not url:
+                    print(f'{prefix} fetching error {review_url} unable to locate entity url')
+                    return
+            except Exception:
+                print(f'{prefix} fetching exception {review_url}')
+                return
+        try:
+            entity = entity_class.objects.get(source_url=url)
+            print(f'{prefix} matched {url}')
+        except ObjectDoesNotExist:
+            try:
+                print(f'{prefix} scraping {url}')
+                scraper.scrape(url)
+                form = scraper.save(request_user=self.user)
+                entity = form.instance
+            except Exception as e:
+                print(f"{prefix} scrape failed: {url} {e}")
+                logger.error(f"{prefix} scrape failed: {url}", exc_info=e)
+                return
+        params = {
+            'owner': self.user,
+            entity_class.__name__.lower(): entity
+        }
+        if review_class.objects.filter(**params).exists():
+            return 2
+        content = re.sub(r'<span style="font-weight: bold;">([^<]+)</span>', r'<b>\1</b>', content)
+        content = re.sub(r'(<img [^>]+>)', r'\1<br>', content)
+        content = re.sub(r'<div class="image-caption">([^<]+)</div>', r'<br><i>\1</i><br>', content)
+        content = md(content)
+        content = re.sub(r'(?<=!\[\]\()([^)]+)(?=\))', lambda x: fetch_remote_image(x[1]), content)
+        params = {
+            'owner': self.user,
+            'created_time': time,
+            'edited_time': time,
+            'title': title,
+            'content': content,
+            'visibility': self.visibility,
+            entity_class.__name__.lower(): entity,
+        }
+        review_class.objects.create(**params)
+        return 1
diff --git a/common/importers/goodreads.py b/common/importers/goodreads.py
new file mode 100644
index 00000000..4e2fec58
--- /dev/null
+++ b/common/importers/goodreads.py
@@ -0,0 +1,202 @@
+import re
+import requests
+from lxml import html
+from datetime import datetime
+# from common.scrapers.goodreads import GoodreadsScraper
+from common.scraper import get_scraper_by_url
+from books.models import Book, BookMark
+from collection.models import Collection
+from common.models import MarkStatusEnum
+from django.conf import settings
+from user_messages import api as msg
+import django_rq
+from django.utils.timezone import make_aware
+
+
+re_list = r'^https://www.goodreads.com/list/show/\d+'
+re_shelf = r'^https://www.goodreads.com/review/list/\d+[^?]*\?shelf=[^&]+'
+re_profile = r'^https://www.goodreads.com/user/show/(\d+)'
+gr_rating = {
+    'did not like it': 2,
+    'it was ok': 4,
+    'liked it': 6,
+    'really liked it': 8,
+    'it was amazing': 10
+}
+
+
+class GoodreadsImporter:
+    @classmethod
+    def import_from_url(self, raw_url, user):
+        match_list = re.match(re_list, raw_url)
+        match_shelf = re.match(re_shelf, raw_url)
+        match_profile = re.match(re_profile, raw_url)
+        if match_profile or match_shelf or match_list:
+            django_rq.get_queue('doufen').enqueue(self.import_from_url_task, raw_url, user)
+            return True
+        else:
+            return False
+
+    @classmethod
+    def import_from_url_task(cls, url, user):
+        match_list = re.match(re_list, url)
+        match_shelf = re.match(re_shelf, url)
+        match_profile = re.match(re_profile, url)
+        total = 0
+        if match_list or match_shelf:
+            shelf = cls.parse_shelf(match_shelf[0], user) if match_shelf else cls.parse_list(match_list[0], user)
+            if shelf['title'] and shelf['books']:
+                collection = Collection.objects.create(title=shelf['title'],
+                                                       description=shelf['description'] + '\n\nImported from [Goodreads](' + url + ')',
+                                                       owner=user)
+                for book in shelf['books']:
+                    collection.append_item(book['book'], book['review'])
+                    total += 1
+                collection.save()
+            msg.success(user, f'成功从Goodreads导入包含{total}本书的收藏单{shelf["title"]}。')
+        elif match_profile:
+            uid = match_profile[1]
+            shelves = {
+                MarkStatusEnum.WISH: f'https://www.goodreads.com/review/list/{uid}?shelf=to-read',
+                MarkStatusEnum.DO: f'https://www.goodreads.com/review/list/{uid}?shelf=currently-reading',
+                MarkStatusEnum.COLLECT: f'https://www.goodreads.com/review/list/{uid}?shelf=read',
+            }
+            for status in shelves:
+                shelf_url = shelves.get(status)
+                shelf = cls.parse_shelf(shelf_url, user)
+                for book in shelf['books']:
+                    params = {
+                        'owner': user,
+                        'rating': book['rating'],
+                        'text': book['review'],
+                        'status': status,
+                        'visibility': 0,
+                        'book': book['book'],
+                    }
+                    if book['last_updated']:
+                        params['created_time'] = book['last_updated']
+                        params['edited_time'] = book['last_updated']
+                    try:
+                        mark = BookMark.objects.create(**params)
+                        mark.book.update_rating(None, mark.rating)
+                    except Exception:
+                        print(f'Skip mark for {book["book"]}')
+                        pass
+                    total += 1
+            msg.success(user, f'成功从Goodreads用户主页导入{total}个标记。')
+
+    @classmethod
+    def parse_shelf(cls, url, user):  # return {'title': 'abc', books: [{'book': obj, 'rating': 10, 'review': 'txt'}, ...]}
+        title = None
+        books = []
+        url_shelf = url + '&view=table'
+        while url_shelf:
+            print(f'Shelf loading {url_shelf}')
+            r = requests.get(url_shelf, timeout=settings.SCRAPING_TIMEOUT)
+            if r.status_code != 200:
+                print(f'Shelf loading error {url_shelf}')
+                break
+            url_shelf = None
+            content = html.fromstring(r.content.decode('utf-8'))
+            title_elem = content.xpath("//span[@class='h1Shelf']/text()")
+            if not title_elem:
+                print(f'Shelf parsing error {url_shelf}')
+                break
+            title = title_elem[0].strip()
+            print("Shelf title: " + title)
+            for cell in content.xpath("//tbody[@id='booksBody']/tr"):
+                url_book = 'https://www.goodreads.com' + \
+                    cell.xpath(
+                        ".//td[@class='field title']//a/@href")[0].strip()
+                # has_review = cell.xpath(
+                #     ".//td[@class='field actions']//a/text()")[0].strip() == 'view (with text)'
+                rating_elem = cell.xpath(
+                    ".//td[@class='field rating']//span/@title")
+                rating = gr_rating.get(
+                    rating_elem[0].strip()) if rating_elem else None
+                url_review = 'https://www.goodreads.com' + \
+                    cell.xpath(
+                        ".//td[@class='field actions']//a/@href")[0].strip()
+                review = ''
+                last_updated = None
+                try:
+                    r2 = requests.get(
+                        url_review, timeout=settings.SCRAPING_TIMEOUT)
+                    if r2.status_code == 200:
+                        c2 = html.fromstring(r2.content.decode('utf-8'))
+                        review_elem = c2.xpath(
+                            "//div[@itemprop='reviewBody']/text()")
+                        review = '\n'.join(
+                            p.strip() for p in review_elem) if review_elem else ''
+                        date_elem = c2.xpath(
+                            "//div[@class='readingTimeline__text']/text()")
+                        for d in date_elem:
+                            date_matched = re.search(r'(\w+)\s+(\d+),\s+(\d+)', d)
+                            if date_matched:
+                                last_updated = make_aware(datetime.strptime(date_matched[1] + ' ' + date_matched[2] + ' ' + date_matched[3], '%B %d %Y'))
+                    else:
+                        print(f"Error loading review{url_review}, ignored")
+                    scraper = get_scraper_by_url(url_book)
+                    url_book = scraper.get_effective_url(url_book)
+                    book = Book.objects.filter(source_url=url_book).first()
+                    if not book:
+                        print("add new book " + url_book)
+                        scraper.scrape(url_book)
+                        form = scraper.save(request_user=user)
+                        book = form.instance
+                    books.append({
+                        'url': url_book,
+                        'book': book,
+                        'rating': rating,
+                        'review': review,
+                        'last_updated': last_updated
+                    })
+                except Exception:
+                    print("Error adding " + url_book)
+                    pass  # likely just download error
+            next_elem = content.xpath("//a[@class='next_page']/@href")
+            url_shelf = ('https://www.goodreads.com' + next_elem[0].strip()) if next_elem else None
+        return {'title': title, 'description': '', 'books': books}
+
+    @classmethod
+    def parse_list(cls, url, user):  # return {'title': 'abc', books: [{'book': obj, 'rating': 10, 'review': 'txt'}, ...]}
+        title = None
+        books = []
+        url_shelf = url
+        while url_shelf:
+            print(f'List loading {url_shelf}')
+            r = requests.get(url_shelf, timeout=settings.SCRAPING_TIMEOUT)
+            if r.status_code != 200:
+                print(f'List loading error {url_shelf}')
+                break
+            url_shelf = None
+            content = html.fromstring(r.content.decode('utf-8'))
+            title_elem = content.xpath('//h1[@class="gr-h1 gr-h1--serif"]/text()')
+            if not title_elem:
+                print(f'List parsing error {url_shelf}')
+                break
+            title = title_elem[0].strip()
+            description = content.xpath('//div[@class="mediumText"]/text()')[0].strip()
+            print("List title: " + title)
+            for link in content.xpath('//a[@class="bookTitle"]/@href'):
+                url_book = 'https://www.goodreads.com' + link
+                try:
+                    scraper = get_scraper_by_url(url_book)
+                    url_book = scraper.get_effective_url(url_book)
+                    book = Book.objects.filter(source_url=url_book).first()
+                    if not book:
+                        print("add new book " + url_book)
+                        scraper.scrape(url_book)
+                        form = scraper.save(request_user=user)
+                        book = form.instance
+                    books.append({
+                        'url': url_book,
+                        'book': book,
+                        'review': '',
+                    })
+                except Exception:
+                    print("Error adding " + url_book)
+                    pass  # likely just download error
+            next_elem = content.xpath("//a[@class='next_page']/@href")
+            url_shelf = ('https://www.goodreads.com' + next_elem[0].strip()) if next_elem else None
+        return {'title': title, 'description': description, 'books': books}
diff --git a/common/index.py b/common/index.py
new file mode 100644
index 00000000..42227b60
--- /dev/null
+++ b/common/index.py
@@ -0,0 +1,12 @@
+from django.conf import settings
+
+
+if settings.SEARCH_BACKEND == 'MEILISEARCH':
+    from .search.meilisearch import Indexer
+elif settings.SEARCH_BACKEND == 'TYPESENSE':
+    from .search.typesense import Indexer
+else:
+    class Indexer:
+        @classmethod
+        def update_model_indexable(self, cls):
+            pass
diff --git a/common/management/commands/delete_job.py b/common/management/commands/delete_job.py
new file mode 100644
index 00000000..1c1d6c86
--- /dev/null
+++ b/common/management/commands/delete_job.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+import pprint
+from redis import Redis
+from rq.job import Job
+from rq import Queue
+
+
+class Command(BaseCommand):
+    help = 'Delete a job'
+
+    def add_arguments(self, parser):
+        parser.add_argument('job_id', type=str, help='Job ID')
+
+    def handle(self, *args, **options):
+        redis = Redis()
+        job_id = str(options['job_id'])
+        job = Job.fetch(job_id, connection=redis)
+        job.delete()
+        self.stdout.write(self.style.SUCCESS(f'Deleted {job}'))
diff --git a/common/management/commands/index_stats.py b/common/management/commands/index_stats.py
new file mode 100644
index 00000000..28a9f07e
--- /dev/null
+++ b/common/management/commands/index_stats.py
@@ -0,0 +1,40 @@
+from django.core.management.base import BaseCommand
+from common.index import Indexer
+from django.conf import settings
+from movies.models import Movie
+from books.models import Book
+from games.models import Game
+from music.models import Album, Song
+from django.core.paginator import Paginator
+from tqdm import tqdm
+from time import sleep
+from datetime import timedelta
+from django.utils import timezone
+
+
+class Command(BaseCommand):
+    help = 'Check search index'
+
+    def handle(self, *args, **options):
+        print(f'Connecting to search server')
+        stats = Indexer.get_stats()
+        print(stats)
+        st = Indexer.instance().get_all_update_status() 
+        cnt = {"enqueued": [0, 0], "processing": [0, 0], "processed": [0, 0], "failed": [0, 0]}
+        lastEnq = {"enqueuedAt": ""}
+        lastProc = {"enqueuedAt": ""}
+        for s in st:
+            n = s["type"].get("number")
+            cnt[s["status"]][0] += 1
+            cnt[s["status"]][1] += n if n else 0
+            if s["status"] == "processing":
+                print(s)
+            elif s["status"] == "enqueued":
+                if s["enqueuedAt"] > lastEnq["enqueuedAt"]:
+                    lastEnq = s
+            elif s["status"] == "processed":
+                if s["enqueuedAt"] > lastProc["enqueuedAt"]:
+                    lastProc = s
+        print(lastEnq)
+        print(lastProc)
+        print(cnt)
diff --git a/common/management/commands/init_index.py b/common/management/commands/init_index.py
new file mode 100644
index 00000000..797d32e4
--- /dev/null
+++ b/common/management/commands/init_index.py
@@ -0,0 +1,18 @@
+from django.core.management.base import BaseCommand
+from common.index import Indexer
+from django.conf import settings
+
+
+class Command(BaseCommand):
+    help = 'Initialize the search index'
+
+    def handle(self, *args, **options):
+        print(f'Connecting to search server')
+        Indexer.init()
+        self.stdout.write(self.style.SUCCESS('Index created.'))
+        # try:
+        #     Indexer.init()
+        #     self.stdout.write(self.style.SUCCESS('Index created.'))
+        # except Exception:
+        #     Indexer.update_settings()
+        #     self.stdout.write(self.style.SUCCESS('Index settings updated.'))
diff --git a/common/management/commands/list_jobs.py b/common/management/commands/list_jobs.py
new file mode 100644
index 00000000..e843f527
--- /dev/null
+++ b/common/management/commands/list_jobs.py
@@ -0,0 +1,24 @@
+from django.core.management.base import BaseCommand
+import pprint
+from redis import Redis
+from rq.job import Job
+from rq import Queue
+
+
+class Command(BaseCommand):
+    help = 'Show jobs in queue'
+
+    def add_arguments(self, parser):
+        parser.add_argument('queue', type=str, help='Queue')
+
+    def handle(self, *args, **options):
+        redis = Redis()
+        queue = Queue(str(options['queue']), connection=redis)
+        for registry in [queue.started_job_registry, queue.deferred_job_registry, queue.finished_job_registry, queue.failed_job_registry, queue.scheduled_job_registry]:
+            self.stdout.write(self.style.SUCCESS(f'Registry {registry}'))
+            for job_id in registry.get_job_ids():
+                try:
+                    job = Job.fetch(job_id, connection=redis)
+                    pprint.pp(job)
+                except Exception as e:
+                    print(f'Error fetching {job_id}')
diff --git a/common/management/commands/reindex.py b/common/management/commands/reindex.py
new file mode 100644
index 00000000..5dcc766f
--- /dev/null
+++ b/common/management/commands/reindex.py
@@ -0,0 +1,40 @@
+from django.core.management.base import BaseCommand
+from common.index import Indexer
+from django.conf import settings
+from movies.models import Movie
+from books.models import Book
+from games.models import Game
+from music.models import Album, Song
+from django.core.paginator import Paginator
+from tqdm import tqdm
+from time import sleep
+from datetime import timedelta
+from django.utils import timezone
+
+
+BATCH_SIZE = 1000
+
+
+class Command(BaseCommand):
+    help = 'Regenerate the search index'
+
+    # def add_arguments(self, parser):
+    #     parser.add_argument('hours', type=int, help='Re-index items modified in last N hours, 0 to reindex all')
+
+    def handle(self, *args, **options):
+        # h = int(options['hours'])
+        print(f'Connecting to search server')
+        if Indexer.busy():
+            print('Please wait for previous updates')
+        # Indexer.update_settings()
+        # self.stdout.write(self.style.SUCCESS('Index settings updated.'))
+        for c in [Book, Song, Album, Game, Movie]:
+            print(f'Re-indexing {c}')
+            qs = c.objects.all()  # if h == 0 else c.objects.filter(edited_time__gt=timezone.now() - timedelta(hours=h))
+            pg = Paginator(qs.order_by('id'), BATCH_SIZE)
+            for p in tqdm(pg.page_range):
+                items = list(map(lambda o: Indexer.obj_to_dict(o), pg.get_page(p).object_list))
+                if items:
+                    Indexer.replace_batch(items)
+                    while Indexer.busy():
+                        sleep(0.5)
diff --git a/common/management/commands/restart_sync.py b/common/management/commands/restart_sync.py
new file mode 100644
index 00000000..93903c57
--- /dev/null
+++ b/common/management/commands/restart_sync.py
@@ -0,0 +1,28 @@
+from django.core.management.base import BaseCommand
+from redis import Redis
+from rq.job import Job
+from sync.models import SyncTask
+from sync.jobs import import_doufen_task
+from django.utils import timezone
+import django_rq
+
+
+class Command(BaseCommand):
+    help = 'Restart a sync task'
+
+    def add_arguments(self, parser):
+        parser.add_argument('synctask_id', type=int, help='Sync Task ID')
+
+    def handle(self, *args, **options):
+        task = SyncTask.objects.get(id=options['synctask_id'])
+        task.finished_items = 0
+        task.failed_urls = []
+        task.success_items = 0
+        task.total_items = 0
+        task.is_finished = False
+        task.is_failed = False
+        task.break_point = ''
+        task.started_time = timezone.now()
+        task.save()
+        django_rq.get_queue('doufen').enqueue(import_doufen_task, task, job_id=f'SyncTask_{task.id}')
+        self.stdout.write(self.style.SUCCESS(f'Queued {task}'))
diff --git a/common/management/commands/scrape.py b/common/management/commands/scrape.py
new file mode 100644
index 00000000..d48898e6
--- /dev/null
+++ b/common/management/commands/scrape.py
@@ -0,0 +1,25 @@
+from django.core.management.base import BaseCommand
+from common.scraper import get_scraper_by_url, get_normalized_url
+import pprint
+
+
+class Command(BaseCommand):
+    help = 'Scrape an item from URL (but not save it)'
+
+    def add_arguments(self, parser):
+        parser.add_argument('url', type=str, help='URL to scrape')
+
+    def handle(self, *args, **options):
+        url = str(options['url'])
+        url = get_normalized_url(url)
+        scraper = get_scraper_by_url(url)
+
+        if scraper is None:
+            self.stdout.write(self.style.ERROR(f'Unable to match a scraper for {url}'))
+            return
+
+        effective_url = scraper.get_effective_url(url)
+        self.stdout.write(f'Fetching {effective_url} via {scraper.__name__}')
+        data, img = scraper.scrape(effective_url)
+        self.stdout.write(self.style.SUCCESS(f'Done.'))
+        pprint.pp(data)
diff --git a/common/models.py b/common/models.py
index c6bf6a70..5d1b55b5 100644
--- a/common/models.py
+++ b/common/models.py
@@ -1,29 +1,34 @@
 import re
 from decimal import *
 from markdown import markdown
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 from django.db import models, IntegrityError
 from django.core.serializers.json import DjangoJSONEncoder
-from django.db.models import Q
+from django.db.models import Q, Count, Sum
 from markdownx.models import MarkdownxField
 from users.models import User
-from mastodon.api import get_relationships, get_cross_site_id
-from boofilsic.settings import CLIENT_NAME
 from django.utils import timezone
+from django.conf import settings
 
 
 RE_HTML_TAG = re.compile(r"<[^>]*>")
+MAX_TOP_TAGS = 5
 
 
 # abstract base classes
 ###################################
 class SourceSiteEnum(models.TextChoices):
-    IN_SITE = "in-site", CLIENT_NAME
-    DOUBAN = "douban",  _("豆瓣")
+    IN_SITE = "in-site", settings.CLIENT_NAME
+    DOUBAN = "douban", _("豆瓣")
     SPOTIFY = "spotify", _("Spotify")
     IMDB = "imdb", _("IMDb")
     STEAM = "steam", _("STEAM")
     BANGUMI = 'bangumi', _("bangumi")
+    GOODREADS = "goodreads", _("goodreads")
+    TMDB = "tmdb", _("The Movie Database")
+    GOOGLEBOOKS = "googlebooks", _("Google Books")
+    BANDCAMP = "bandcamp", _("BandCamp")
+    IGDB = "igdb", _("IGDB")
 
 
 class Entity(models.Model):
@@ -52,10 +57,25 @@ class Entity(models.Model):
                 rating__lte=10), name='%(class)s_rating_upperbound'),
         ]
 
-
     def get_absolute_url(self):
         raise NotImplementedError("Subclass should implement this method")
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + self.get_absolute_url()
+
+    def get_json(self):
+        return {
+            'title': self.title,
+            'brief': self.brief,
+            'rating': self.rating,
+            'url': self.url,
+            'cover_url': settings.APP_WEBSITE + self.cover.url,
+            'top_tags': self.tags[:5],
+            'category_name': self.verbose_category_name,
+            'other_info': self.other_info,
+        }
+
     def save(self, *args, **kwargs):
         """ update rating and strip source url scheme & querystring before save to db """
         if self.rating_number and self.rating_total_score:
@@ -108,6 +128,15 @@ class Entity(models.Model):
         self.calculate_rating(old_rating, new_rating)
         self.save()
 
+    def refresh_rating(self):  # TODO: replace update_rating()
+        a = self.marks.filter(rating__gt=0).aggregate(Sum('rating'), Count('rating'))
+        if self.rating_total_score != a['rating__sum'] or self.rating_number != a['rating__count']:
+            self.rating_total_score = a['rating__sum']
+            self.rating_number = a['rating__count']
+            self.rating = a['rating__sum'] / a['rating__count'] if a['rating__count'] > 0 else None
+            self.save()
+        return self.rating
+
     def get_tags_manager(self):
         """
         Since relation between tag and entity is foreign key, and related name has to be unique,
@@ -115,9 +144,13 @@ class Entity(models.Model):
         """
         raise NotImplementedError("Subclass should implement this method.")
 
+    @property
+    def top_tags(self):
+        return self.get_tags_manager().values('content').annotate(tag_frequency=Count('content')).order_by('-tag_frequency')[:MAX_TOP_TAGS]
+
     def get_marks_manager(self):
         """
-        Normally this won't be used. 
+        Normally this won't be used.
         There is no ocassion where visitor can simply view all the marks.
         """
         raise NotImplementedError("Subclass should implement this method.")
@@ -129,6 +162,19 @@ class Entity(models.Model):
         """
         raise NotImplementedError("Subclass should implement this method.")
 
+    @property
+    def all_tag_list(self):
+        return self.get_tags_manager().values('content').annotate(frequency=Count('content')).order_by('-frequency')
+
+    @property
+    def tags(self):
+        return list(map(lambda t: t['content'], self.all_tag_list))
+
+    @property
+    def marks(self):
+        params = {self.__class__.__name__.lower() + '_id': self.id}
+        return self.mark_class.objects.filter(**params)
+
     @classmethod
     def get_category_mapping_dict(cls):
         category_mapping_dict = {}
@@ -144,75 +190,64 @@ class Entity(models.Model):
     def verbose_category_name(self):
         raise NotImplementedError("Subclass should implement this.")
 
+    @property
+    def mark_class(self):
+        raise NotImplementedError("Subclass should implement this.")
+
+    @property
+    def tag_class(self):
+        raise NotImplementedError("Subclass should implement this.")
+
 
 class UserOwnedEntity(models.Model):
-    is_private = models.BooleanField()
-    owner = models.ForeignKey(
-        User, on_delete=models.CASCADE, related_name='user_%(class)ss')
+    is_private = models.BooleanField(default=False, null=True)  # first set allow null, then migration, finally (in a few days) remove for good
+    visibility = models.PositiveSmallIntegerField(default=0)  # 0: Public / 1: Follower only / 2: Self only
+    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_%(class)ss')
     created_time = models.DateTimeField(default=timezone.now)
     edited_time = models.DateTimeField(default=timezone.now)
 
     class Meta:
         abstract = True
 
+    def is_visible_to(self, viewer):
+        if not viewer.is_authenticated:
+            return self.visibility == 0
+        owner = self.owner
+        if owner == viewer:
+            return True
+        if not owner.is_active:
+            return False
+        if self.visibility == 2:
+            return False
+        if viewer.is_blocking(owner) or owner.is_blocking(viewer) or viewer.is_muting(owner):
+            return False
+        if self.visibility == 1:
+            return viewer.is_following(owner)
+        else:
+            return True
+
+    def is_editable_by(self, viewer):
+        return True if viewer.is_staff or viewer.is_superuser or viewer == self.owner else False
+
     @classmethod
-    def get_available(cls, entity, request_user, token):
-        # TODO add amount limit for once query
-        """ 
-        Returns all avaliable user-owned entities related to given entity. 
-        This method handles mute/block relationships and private/public visibilities.
-        """
-        # the foreign key field that points to entity
-        # has to be named as the lower case name of that entity
+    def get_available(cls, entity, request_user, following_only=False):
+        # e.g. SongMark.get_available(song, request.user)
         query_kwargs = {entity.__class__.__name__.lower(): entity}
-        user_owned_entities = cls.objects.filter(
-            **query_kwargs).order_by("-edited_time")
-
-        # every user should only be abled to have one user owned entity for each entity
-        # this is guaranteed by models
-        id_list = []
-
-        # none_index tracks those failed cross site id query
-        none_index = []
-
-        for (i, entity) in enumerate(user_owned_entities):
-            if entity.owner.mastodon_site == request_user.mastodon_site:
-                id_list.append(entity.owner.mastodon_id)
-            else:
-                # TODO there could be many requests therefore make the pulling asynchronized
-                cross_site_id = get_cross_site_id(
-                    entity.owner, request_user.mastodon_site, token)
-                if not cross_site_id is None:
-                    id_list.append(cross_site_id)
-                else:
-                    none_index.append(i)
-                    # populate those query-failed None postions
-                    # to ensure the consistency of the orders of 
-                    # the three(id_list, user_owned_entities, relationships)
-                    id_list.append(request_user.mastodon_id)
-
-        # Mastodon request
-        relationships = get_relationships(
-            request_user.mastodon_site, id_list, token)
-        mute_block_blocked_index = []
-        following_index = []
-        for i, r in enumerate(relationships):
-            # the order of relationships is corresponding to the id_list,
-            # and the order of id_list is the same as user_owned_entiies
-            if r['blocking'] or r['blocked_by'] or r['muting']:
-                mute_block_blocked_index.append(i)
-            if r['following']:
-                following_index.append(i)
-        available_entities = [
-            e for i, e in enumerate(user_owned_entities)
-                if ((e.is_private == True and i in following_index) or e.is_private == False or e.owner == request_user)
-                    and not i in mute_block_blocked_index and not i in none_index
-        ]
-        return available_entities
+        all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time")  # get all marks for song
+        visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
+        return visible_entities
 
     @classmethod
-    def get_available_by_user(cls, owner, is_following):
-        """ 
+    def get_available_for_identicals(cls, entity, request_user, following_only=False):
+        # e.g. SongMark.get_available(song, request.user)
+        query_kwargs = {entity.__class__.__name__.lower() + '__in': entity.get_identicals()}
+        all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time")  # get all marks for song
+        visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
+        return visible_entities
+
+    @classmethod
+    def get_available_by_user(cls, owner, is_following):  # FIXME
+        """
         Returns all avaliable owner's entities.
         Mute/Block relation is not handled in this method.
 
@@ -220,10 +255,17 @@ class UserOwnedEntity(models.Model):
         :param is_following: if the current user is following the owner
         """
         user_owned_entities = cls.objects.filter(owner=owner)
-        if not is_following:
-            user_owned_entities = user_owned_entities.exclude(is_private=True)
+        if is_following:
+            user_owned_entities = user_owned_entities.exclude(visibility=2)
+        else:
+            user_owned_entities = user_owned_entities.filter(visibility=0)
         return user_owned_entities
 
+    @property
+    def item(self):
+        attr = re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', self.__class__.__name__)[0].lower()
+        return getattr(self, attr)
+
 
 # commonly used entity classes
 ###################################
@@ -236,10 +278,20 @@ class MarkStatusEnum(models.TextChoices):
 class Mark(UserOwnedEntity):
     status = models.CharField(choices=MarkStatusEnum.choices, max_length=20)
     rating = models.PositiveSmallIntegerField(blank=True, null=True)
-    text = models.CharField(max_length=500, blank=True, default='')
+    text = models.CharField(max_length=5000, blank=True, default='')
+    shared_link = models.CharField(max_length=5000, blank=True, default='')
 
     def __str__(self):
-        return f"({self.id}) {self.owner} {self.status.upper()}"
+        return f"Mark({self.id} {self.owner} {self.status.upper()})"
+
+    @property
+    def translated_status(self):
+        raise NotImplementedError("Subclass should implement this.")
+
+    @property
+    def tags(self):
+        tags = self.item.tag_class.objects.filter(mark_id=self.id)
+        return tags
 
     class Meta:
         abstract = True
@@ -249,7 +301,7 @@ class Mark(UserOwnedEntity):
             models.CheckConstraint(check=models.Q(
                 rating__lte=10), name='mark_rating_upperbound'),
         ]
-    
+
     # TODO update entity rating when save
     # TODO update tags
 
@@ -257,6 +309,7 @@ class Mark(UserOwnedEntity):
 class Review(UserOwnedEntity):
     title = models.CharField(max_length=120)
     content = MarkdownxField()
+    shared_link = models.CharField(max_length=5000, blank=True, default='')
 
     def __str__(self):
         return self.title
@@ -271,6 +324,10 @@ class Review(UserOwnedEntity):
     class Meta:
         abstract = True
 
+    @property
+    def translated_status(self):
+        return '评论了'
+
 
 class Tag(models.Model):
     content = models.CharField(max_length=50)
@@ -278,5 +335,28 @@ class Tag(models.Model):
     def __str__(self):
         return self.content
 
+    @property
+    def edited_time(self):
+        return self.mark.edited_time
+
+    @property
+    def created_time(self):
+        return self.mark.created_time
+
+    @property
+    def text(self):
+        return self.mark.text
+
+    @classmethod
+    def find_by_user(cls, tag, owner, viewer):
+        qs = cls.objects.filter(content=tag, mark__owner=owner)
+        if owner != viewer:
+            qs = qs.filter(mark__visibility__lte=owner.get_max_visibility(viewer))
+        return qs
+
+    @classmethod
+    def all_by_user(cls, owner):
+        return cls.objects.filter(mark__owner=owner).values('content').annotate(total=Count('content')).order_by('-total')
+
     class Meta:
         abstract = True
diff --git a/common/scraper.py b/common/scraper.py
index 33bef7e4..defe6f38 100644
--- a/common/scraper.py
+++ b/common/scraper.py
@@ -7,23 +7,17 @@ import dateparser
 import datetime
 import time
 import filetype
+import dns.resolver
+import urllib.parse
 from lxml import html
 from threading import Thread
-from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG, IMDB_API_KEY, SCRAPERAPI_KEY
-from boofilsic.settings import SPOTIFY_CREDENTIAL
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.files.uploadedfile import SimpleUploadedFile
 from common.models import SourceSiteEnum
-from movies.models import Movie, MovieGenreEnum
-from movies.forms import MovieForm
-from books.models import Book
-from books.forms import BookForm
-from music.models import Album, Song
-from music.forms import AlbumForm, SongForm
-from games.models import Game
-from games.forms import GameForm
+from django.conf import settings
+from django.core.exceptions import ValidationError
 
 
 RE_NUMBERS = re.compile(r"\d+\d*")
@@ -43,12 +37,11 @@ DEFAULT_REQUEST_HEADERS = {
     'Cache-Control': 'no-cache',
 }
 
-# in seconds
-TIMEOUT = 60
 
 # luminati account credentials
 PORT = 22225
 
+
 logger = logging.getLogger(__name__)
 
 
@@ -56,6 +49,12 @@ logger = logging.getLogger(__name__)
 scraper_registry = {}
 
 
+def get_normalized_url(raw_url):
+    url = re.sub(r'//m.douban.com/(\w+)/', r'//\1.douban.com/', raw_url)
+    url = re.sub(r'//www.google.com/books/edition/_/([A-Za-z0-9_\-]+)[\?]*', r'//books.google.com/books?id=\1&', url)
+    return url
+
+
 def log_url(func):
     """
     Catch exceptions and log then pass the exceptions.
@@ -67,20 +66,23 @@ def log_url(func):
             return func(*args, **kwargs)
         except Exception as e:
             # log the url and trace stack
-            logger.error(f"Scrape Failed URL: {args[1]}")
-            logger.error("Expections during scraping:", exc_info=e)
+            logger.error(f"Scrape Failed URL: {args[1]}\n{e}")
+            if settings.DEBUG:
+                logger.error("Expections during scraping:", exc_info=e)
             raise e
 
     return wrapper
 
+
 def parse_date(raw_str):
     return dateparser.parse(
-        raw_str, 
+        raw_str,
         settings={
-        "RELATIVE_BASE": datetime.datetime(1900, 1, 1)
+            "RELATIVE_BASE": datetime.datetime(1900, 1, 1)
         }
     )
 
+
 class AbstractScraper:
     """
     Scrape entities. The entities means those defined in the models.py file,
@@ -119,7 +121,7 @@ class AbstractScraper:
 
         # decorate the scrape method
         cls.scrape = classmethod(log_url(cls.scrape))
-        
+
         # register scraper
         if isinstance(cls.host, list):
             for host in cls.host:
@@ -141,26 +143,30 @@ class AbstractScraper:
         """
         The return value should be identical with that saved in DB as `source_url`
         """
-        url = cls.regex.findall(raw_url)
+        url = cls.regex.findall(raw_url.replace('http:', 'https:'))  # force all http to be https
         if not url:
-            raise ValueError("not valid url")
+            raise ValueError(f"not valid url: {raw_url}")
         return url[0]
 
     @classmethod
     def download_page(cls, url, headers):
         url = cls.get_effective_url(url)
 
-        session_id = random.random()
-        proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
-                     (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
-        proxies = {
-            'http': proxy_url,
-            'https': proxy_url,
-        }
-        # if DEBUG:
-        #     proxies = None
+        if settings.LUMINATI_USERNAME is None:
+            proxies = None
+            if settings.SCRAPESTACK_KEY is not None:
+                url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
+        else:
+            session_id = random.random()
+            proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
+                         (settings.LUMINATI_USERNAME, session_id, settings.LUMINATI_PASSWORD, PORT))
+            proxies = {
+                'http': proxy_url,
+                'https': proxy_url,
+            }
+
         r = requests.get(url, proxies=proxies,
-                         headers=headers, timeout=TIMEOUT)
+                         headers=headers, timeout=settings.SCRAPING_TIMEOUT)
 
         if r.status_code != 200:
             raise RuntimeError(f"download page failed, status code {r.status_code}")
@@ -169,19 +175,19 @@ class AbstractScraper:
         return html.fromstring(r.content.decode('utf-8'))
 
     @classmethod
-    def download_image(cls, url):
+    def download_image(cls, url, item_url=None):
         if url is None:
-            return
+            return None, None
         raw_img = None
         session_id = random.random()
         proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
-                     (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
+                     (settings.LUMINATI_USERNAME, session_id, settings.LUMINATI_PASSWORD, PORT))
         proxies = {
             'http': proxy_url,
             'https': proxy_url,
         }
-        # if DEBUG:
-        #     proxies = None
+        if settings.LUMINATI_USERNAME is None:
+            proxies = None
         if url:
             img_response = requests.get(
                 url,
@@ -194,7 +200,7 @@ class AbstractScraper:
                     'dnt': '1',
                 },
                 proxies=proxies,
-                timeout=TIMEOUT,
+                timeout=settings.SCRAPING_TIMEOUT,
             )
             if img_response.status_code == 200:
                 raw_img = img_response.content
@@ -205,13 +211,14 @@ class AbstractScraper:
         return raw_img, ext
 
     @classmethod
-    def save(cls, request_user):
+    def save(cls, request_user, instance=None):
         entity_cover = {
             'cover': SimpleUploadedFile('temp.' + cls.img_ext, cls.raw_img)
-        }
-        form = cls.form_class(cls.raw_data, entity_cover)
+        } if cls.img_ext is not None else None
+        form = cls.form_class(data=cls.raw_data, files=entity_cover, instance=instance)
         if form.is_valid():
             form.instance.last_editor = request_user
+            form.instance._change_reason = 'scrape'
             form.save()
             cls.instance = form.instance
         else:
@@ -220,1145 +227,37 @@ class AbstractScraper:
         return form
 
 
-class DoubanScrapperMixin:
-    @classmethod
-    def download_page(cls, url, headers):
-        url = cls.get_effective_url(url)
-
-        scraper_api_endpoint = f'http://api.scraperapi.com?api_key={SCRAPERAPI_KEY}&url={url}'
-
-        r = requests.get(scraper_api_endpoint, timeout=TIMEOUT)
-
-        if r.status_code != 200:
-            raise RuntimeError(f"download page failed, status code {r.status_code}")
-        # with open('temp.html', 'w', encoding='utf-8') as fp:
-        #     fp.write(r.content.decode('utf-8'))
-        return html.fromstring(r.content.decode('utf-8'))
-
-
-class DoubanBookScraper(DoubanScrapperMixin, 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)
-
-        # parsing starts here
-        try:
-            title = content.xpath("/html/body//h1/span/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no book info")
-
-        subtitle_elem = content.xpath(
-            "//div[@id='info']//span[text()='副标题:']/following::text()")
-        subtitle = subtitle_elem[0].strip() if subtitle_elem else None
-
-        orig_title_elem = content.xpath(
-            "//div[@id='info']//span[text()='原作名:']/following::text()")
-        orig_title = orig_title_elem[0].strip() if orig_title_elem else None
-
-        language_elem = content.xpath(
-            "//div[@id='info']//span[text()='语言:']/following::text()")
-        language = language_elem[0].strip() if language_elem else None
-
-        pub_house_elem = content.xpath(
-            "//div[@id='info']//span[text()='出版社:']/following::text()")
-        pub_house = pub_house_elem[0].strip() if pub_house_elem else None
-
-        pub_date_elem = content.xpath(
-            "//div[@id='info']//span[text()='出版年:']/following::text()")
-        pub_date = pub_date_elem[0].strip() if pub_date_elem else ''
-        year_month_day = RE_NUMBERS.findall(pub_date)
-        if len(year_month_day) in (2, 3):
-            pub_year = int(year_month_day[0])
-            pub_month = int(year_month_day[1])
-        elif len(year_month_day) == 1:
-            pub_year = int(year_month_day[0])
-            pub_month = None
-        else:
-            pub_year = None
-            pub_month = None
-        if pub_year and pub_month and pub_year < pub_month:
-            pub_year, pub_month = pub_month, pub_year
-        pub_year = None if pub_year is not None and not pub_year in range(
-            0, 3000) else pub_year
-        pub_month = None if pub_month is not None and not pub_month in range(
-            1, 12) else pub_month
-
-        binding_elem = content.xpath(
-            "//div[@id='info']//span[text()='装帧:']/following::text()")
-        binding = binding_elem[0].strip() if binding_elem else None
-
-        price_elem = content.xpath(
-            "//div[@id='info']//span[text()='定价:']/following::text()")
-        price = price_elem[0].strip() if price_elem else None
-
-        pages_elem = content.xpath(
-            "//div[@id='info']//span[text()='页数:']/following::text()")
-        pages = pages_elem[0].strip() if pages_elem else None
-        if pages is not None:
-            pages = int(RE_NUMBERS.findall(pages)[
-                        0]) if RE_NUMBERS.findall(pages) else None
-
-        isbn_elem = content.xpath(
-            "//div[@id='info']//span[text()='ISBN:']/following::text()")
-        isbn = isbn_elem[0].strip() if isbn_elem else None
-
-        brief_elem = content.xpath(
-            "//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()")
-        brief = '\n'.join(p.strip()
-                          for p in brief_elem) if brief_elem else None
-
-        contents = None
-        try:
-            contents_elem = content.xpath(
-                "//h2/span[text()='目录']/../following-sibling::div[1]")[0]
-            # if next the id of next sibling contains `dir`, that would be the full contents
-            if "dir" in contents_elem.getnext().xpath("@id")[0]:
-                contents_elem = contents_elem.getnext()
-                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
-                    "text()")[:-2]) if contents_elem else None
-            else:
-                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
-                    "text()")) if contents_elem else None
-        except:
-            pass
-
-        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)
-
-        # there are two html formats for authors and translators
-        authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
-            preceding-sibling::a[preceding-sibling::span[text()='作者:']]/text()""")
-        if not authors_elem:
-            authors_elem = content.xpath(
-                """//div[@id='info']//span[text()=' 作者']/following-sibling::a/text()""")
-        if authors_elem:
-            authors = []
-            for author in authors_elem:
-                authors.append(RE_WHITESPACES.sub(' ', author.strip()))
-        else:
-            authors = None
-
-        translators_elem = content.xpath("""//div[@id='info']//span[text()='译者:']/following-sibling::br[1]/
-            preceding-sibling::a[preceding-sibling::span[text()='译者:']]/text()""")
-        if not translators_elem:
-            translators_elem = content.xpath(
-                """//div[@id='info']//span[text()=' 译者']/following-sibling::a/text()""")
-        if translators_elem:
-            translators = []
-            for translator in translators_elem:
-                translators.append(RE_WHITESPACES.sub(' ', translator.strip()))
-        else:
-            translators = None
-
-        other = {}
-        cncode_elem = content.xpath(
-            "//div[@id='info']//span[text()='统一书号:']/following::text()")
-        if cncode_elem:
-            other['统一书号'] = cncode_elem[0].strip()
-        series_elem = content.xpath(
-            "//div[@id='info']//span[text()='丛书:']/following-sibling::a[1]/text()")
-        if series_elem:
-            other['丛书'] = series_elem[0].strip()
-        imprint_elem = content.xpath(
-            "//div[@id='info']//span[text()='出品方:']/following-sibling::a[1]/text()")
-        if imprint_elem:
-            other['出品方'] = imprint_elem[0].strip()
-
-        data = {
-            'title': title,
-            'subtitle': subtitle,
-            'orig_title': orig_title,
-            'author': authors,
-            'translator': translators,
-            'language': language,
-            'pub_house': pub_house,
-            'pub_year': pub_year,
-            'pub_month': pub_month,
-            'binding': binding,
-            'price': price,
-            'pages': pages,
-            'isbn': isbn,
-            'brief': brief,
-            'contents': contents,
-            'other_info': other,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-class DoubanMovieScraper(DoubanScrapperMixin, 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)
-
-        # parsing starts here
-        try:
-            raw_title = content.xpath(
-                "//span[@property='v:itemreviewed']/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no movie info")
-
-        orig_title = content.xpath(
-            "//img[@rel='v:image']/@alt")[0].strip()
-        title = raw_title.split(orig_title)[0].strip()
-        # if has no chinese title
-        if title == '':
-            title = orig_title
-
-        if title == orig_title:
-            orig_title = None
-
-        # there are two html formats for authors and translators
-        other_title_elem = content.xpath(
-            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
-        other_title = other_title_elem[0].strip().split(
-            ' / ') if other_title_elem else None
-
-        imdb_elem = content.xpath(
-            "//div[@id='info']//span[text()='IMDb链接:']/following-sibling::a[1]/text()")
-        if not imdb_elem:
-            imdb_elem = content.xpath(
-                "//div[@id='info']//span[text()='IMDb:']/following-sibling::text()[1]")
-        imdb_code = imdb_elem[0].strip() if imdb_elem else None
-
-        director_elem = content.xpath(
-            "//div[@id='info']//span[text()='导演']/following-sibling::span[1]/a/text()")
-        director = director_elem if director_elem else None
-
-        playwright_elem = content.xpath(
-            "//div[@id='info']//span[text()='编剧']/following-sibling::span[1]/a/text()")
-        playwright = playwright_elem if playwright_elem else None
-
-        actor_elem = content.xpath(
-            "//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()")
-        actor = actor_elem if actor_elem else None
-
-        # construct genre translator
-        genre_translator = {}
-        attrs = [attr for attr in dir(MovieGenreEnum) if not '__' in attr]
-        for attr in attrs:
-            genre_translator[getattr(MovieGenreEnum, attr).label] = getattr(
-                MovieGenreEnum, attr).value
-
-        genre_elem = content.xpath("//span[@property='v:genre']/text()")
-        if genre_elem:
-            genre = []
-            for g in genre_elem:
-                genre.append(genre_translator[g])
-        else:
-            genre = None
-
-        showtime_elem = content.xpath(
-            "//span[@property='v:initialReleaseDate']/text()")
-        if showtime_elem:
-            showtime = []
-            for st in showtime_elem:
-                parts = st.split('(')
-                if len(parts) == 1:
-                    time = st.split('(')[0]
-                    region = ''
-                else:
-                    time = st.split('(')[0]
-                    region = st.split('(')[1][0:-1]
-                showtime.append({time: region})
-        else:
-            showtime = None
-
-        site_elem = content.xpath(
-            "//div[@id='info']//span[text()='官方网站:']/following-sibling::a[1]/@href")
-        site = site_elem[0].strip() if site_elem else None
-
-        area_elem = content.xpath(
-            "//div[@id='info']//span[text()='制片国家/地区:']/following-sibling::text()[1]")
-        if area_elem:
-            area = [a.strip() for a in area_elem[0].split(' / ')]
-        else:
-            area = None
-
-        language_elem = content.xpath(
-            "//div[@id='info']//span[text()='语言:']/following-sibling::text()[1]")
-        if language_elem:
-            language = [a.strip() for a in language_elem[0].split(' / ')]
-        else:
-            language = None
-
-        year_elem = content.xpath("//span[@class='year']/text()")
-        year = int(year_elem[0][1:-1]) if year_elem else None
-
-        duration_elem = content.xpath("//span[@property='v:runtime']/text()")
-        other_duration_elem = content.xpath(
-            "//span[@property='v:runtime']/following-sibling::text()[1]")
-        if duration_elem:
-            duration = duration_elem[0].strip()
-            if other_duration_elem:
-                duration += other_duration_elem[0].rstrip()
-        else:
-            duration = None
-
-        season_elem = content.xpath(
-
-            "//*[@id='season']/option[@selected='selected']/text()")
-        if not season_elem:
-            season_elem = content.xpath(
-                "//div[@id='info']//span[text()='季数:']/following-sibling::text()[1]")
-            season = int(season_elem[0].strip()) if season_elem else None
-        else:
-            season = int(season_elem[0].strip())
-
-        episodes_elem = content.xpath(
-            "//div[@id='info']//span[text()='集数:']/following-sibling::text()[1]")
-        episodes = int(episodes_elem[0].strip()) if episodes_elem else None
-
-        single_episode_length_elem = content.xpath(
-            "//div[@id='info']//span[text()='单集片长:']/following-sibling::text()[1]")
-        single_episode_length = single_episode_length_elem[0].strip(
-        ) if single_episode_length_elem else None
-
-        # if has field `episodes` not none then must be series
-        is_series = True if episodes else False
-
-        brief_elem = content.xpath("//span[@class='all hidden']")
-        if not brief_elem:
-            brief_elem = content.xpath("//span[@property='v:summary']")
-        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
-            './text()')]) if brief_elem else None
-
-        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)
-
-        data = {
-            'title': title,
-            'orig_title': orig_title,
-            'other_title': other_title,
-            'imdb_code': imdb_code,
-            'director': director,
-            'playwright': playwright,
-            'actor': actor,
-            'genre': genre,
-            'showtime': showtime,
-            'site': site,
-            'area': area,
-            'language': language,
-            'year': year,
-            'duration': duration,
-            'season': season,
-            'episodes': episodes,
-            'single_episode_length': single_episode_length,
-            'brief': brief,
-            'is_series': is_series,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-class DoubanAlbumScraper(DoubanScrapperMixin, 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)
-
-        # parsing starts here
-        try:
-            title = content.xpath("//h1/span/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no album info")
-        if not title:
-            raise ValueError("given url contains no album info")
-            
-
-        artists_elem = content.xpath("""//div[@id='info']/span/span[@class='pl']/a/text()""")
-        artist = None if not artists_elem else artists_elem
-
-        genre_elem = content.xpath(
-            "//div[@id='info']//span[text()='流派:']/following::text()[1]")
-        genre = genre_elem[0].strip() if genre_elem else None
-
-        date_elem = content.xpath(
-            "//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
-        release_date = parse_date(date_elem[0].strip()) if date_elem else None
-
-        company_elem = content.xpath(
-            "//div[@id='info']//span[text()='出版者:']/following::text()[1]")
-        company = company_elem[0].strip() if company_elem else None
-
-        track_list_elem = content.xpath(
-            "//div[@class='track-list']/div[@class='indent']/div/text()"
-        )
-        if track_list_elem:
-            track_list = '\n'.join([track.strip() for track in track_list_elem])
-        else:
-            track_list = None
-
-        brief_elem = content.xpath("//span[@class='all hidden']")
-        if not brief_elem:
-            brief_elem = content.xpath("//span[@property='v:summary']")
-        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
-            './text()')]) if brief_elem else None
-
-        other_info = {}
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['又名'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['专辑类型'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['介质'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['ISRC'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['条形码'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['碟片数'] = other_elem[0].strip()
-
-        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)
-
-        data = {
-            'title': title,
-            'artist': artist,
-            'genre': genre,
-            'release_date': release_date,
-            'duration': None,
-            'company': company,
-            'track_list': track_list,
-            'brief': brief,
-            'other_info': other_info,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-spotify_token = None
-spotify_token_expire_time = time.time()
-
-class SpotifyTrackScraper(AbstractScraper):
-    site_name = SourceSiteEnum.SPOTIFY.value
-    host = 'https://open.spotify.com/track/'
-    data_class = Song
-    form_class = SongForm
-
-    regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+")
-
-    def scrape(self, url):
-        """
-        Request from API, not really scraping
-        """
-        global spotify_token, spotify_token_expire_time
-
-        if spotify_token is None or is_spotify_token_expired():
-            invoke_spotify_token()
-        effective_url = self.get_effective_url(url)
-        if effective_url is None:
-            raise ValueError("not valid url")
-
-        api_url = self.get_api_url(effective_url)
-        headers = {
-            'Authorization': f"Bearer {spotify_token}"
-        }
-        r = requests.get(api_url, headers=headers)
-        res_data = r.json()
-
-        artist = []
-        for artist_dict in res_data['artists']:
-            artist.append(artist_dict['name'])
-        if not artist:
-            artist = None
-
-        title = res_data['name']
-
-        release_date = parse_date(res_data['album']['release_date'])
-
-        duration = res_data['duration_ms']
-
-        if res_data['external_ids'].get('isrc'):
-            isrc = res_data['external_ids']['isrc']
-        else:
-            isrc = None
-
-        raw_img, ext = self.download_image(res_data['album']['images'][0]['url'])
-        
-        data = {
-            'title': title,
-            'artist': artist,
-            'genre': None,
-            'release_date': release_date,
-            'duration': duration,
-            'isrc': isrc,
-            'album': None,
-            'brief': None,
-            'other_info': None,
-            'source_site': self.site_name,
-            'source_url': effective_url,
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-    @classmethod
-    def get_effective_url(cls, raw_url):
-        code = cls.regex.findall(raw_url)
-        if code:
-            return f"https://open.spotify.com/track/{code[0]}"
-        else:
-            return None
-
-    @classmethod
-    def get_api_url(cls, url):
-        return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0]
-
-
-class SpotifyAlbumScraper(AbstractScraper):
-    site_name = SourceSiteEnum.SPOTIFY.value
-    # API URL
-    host = 'https://open.spotify.com/album/'
-    data_class = Album
-    form_class = AlbumForm
-
-    regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+")
-
-    def scrape(self, url):
-        """
-        Request from API, not really scraping
-        """
-        global spotify_token, spotify_token_expire_time
-
-        if spotify_token is None or is_spotify_token_expired():
-            invoke_spotify_token()
-        effective_url = self.get_effective_url(url)
-        if effective_url is None:
-            raise ValueError("not valid url")
-
-        api_url = self.get_api_url(effective_url)
-        headers = {
-            'Authorization': f"Bearer {spotify_token}"
-        }
-        r = requests.get(api_url, headers=headers)
-        res_data = r.json()
-
-        artist = []
-        for artist_dict in res_data['artists']:
-            artist.append(artist_dict['name'])
-
-        title = res_data['name']
-
-        genre = ', '.join(res_data['genres'])
-
-        company = []
-        for com in res_data['copyrights']:
-            company.append(com['text'])
-
-        duration = 0
-        track_list = []
-        track_urls = []
-        for track in res_data['tracks']['items']:
-            track_urls.append(track['external_urls']['spotify'])
-            duration += track['duration_ms']
-            if res_data['tracks']['items'][-1]['disc_number'] > 1:
-                # more than one disc
-                track_list.append(str(
-                    track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name'])
-            else:
-                track_list.append(str(track['track_number']) + '. ' + track['name'])
-        track_list = '\n'.join(track_list)
-
-        release_date = parse_date(res_data['release_date'])
-
-        other_info = {}
-        if res_data['external_ids'].get('upc'):
-            # bar code
-            other_info['UPC'] = res_data['external_ids']['upc']
-
-        raw_img, ext = self.download_image(res_data['images'][0]['url'])
-
-        data = {
-            'title': title,
-            'artist': artist,
-            'genre': genre,
-            'track_list': track_list,
-            'release_date': release_date,
-            'duration': duration,
-            'company': company,
-            'brief': None,
-            'other_info': other_info,
-            'source_site': self.site_name,
-            'source_url': effective_url,
-        }
-
-        # set tracks_data, used for adding tracks
-        self.track_urls = track_urls
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-    @classmethod
-    def get_effective_url(cls, raw_url):
-        code = cls.regex.findall(raw_url)
-        if code:
-            return f"https://open.spotify.com/album/{code[0]}"
-        else:
-            return None
-
-    @classmethod
-    def save(cls, request_user):
-        form = super().save(request_user)
-        task = Thread(
-            target=cls.add_tracks,
-            args=(form.instance, request_user),
-            daemon=True
-        )
-        task.start()
-        return form
-
-    @classmethod
-    def get_api_url(cls, url):
-        return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0]
-
-    @classmethod
-    def add_tracks(cls, album: Album, request_user):
-        to_be_updated_tracks = []
-        for track_url in cls.track_urls:
-            track = cls.get_track_or_none(track_url)
-            # seems lik if fire too many requests at the same time 
-            # spotify would limit access
-            if track is None:
-                task = Thread(
-                    target=cls.scrape_and_save_track,
-                    args=(track_url, album, request_user),
-                    daemon=True
-                )
-                task.start()
-                task.join()
-            else:
-                to_be_updated_tracks.append(track)
-        cls.bulk_update_track_album(to_be_updated_tracks, album, request_user)
-        
-    @classmethod
-    def get_track_or_none(cls, track_url: str):
-        try:
-            instance = Song.objects.get(source_url=track_url)
-            return instance
-        except ObjectDoesNotExist:
-            return None
-        
-    @classmethod
-    def scrape_and_save_track(cls, url: str, album: Album, request_user):
-        data, img = SpotifyTrackScraper.scrape(url)
-        SpotifyTrackScraper.raw_data['album'] = album
-        SpotifyTrackScraper.save(request_user)
-        
-    @classmethod
-    def bulk_update_track_album(cls, tracks, album, request_user):
-        for track in tracks:
-            track.last_editor = request_user
-            track.edited_time = timezone.now()
-            track.album = album
-        Song.objects.bulk_update(tracks, [
-            'last_editor',
-            'edited_time',
-            'album'
-        ])
-
-
-def is_spotify_token_expired():
-    global spotify_token_expire_time
-    return True if spotify_token_expire_time <= time.time() else False
-
-
-def invoke_spotify_token():
-    global spotify_token, spotify_token_expire_time
-    r = requests.post(
-        "https://accounts.spotify.com/api/token",
-        data={
-            "grant_type": "client_credentials"
-        },
-        headers={
-            "Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
-        }
-    )
-    data = r.json()
-    if r.status_code == 401:
-        # token expired, try one more time
-        # this maybe caused by external operations,
-        # for example debugging using a http client
-        r = requests.post(
-            "https://accounts.spotify.com/api/token",
-            data={
-                "grant_type": "client_credentials"
-            },
-            headers={
-                "Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
-            }
-        )
-        data = r.json()
-    elif r.status_code != 200:
-        raise Exception(f"Request to spotify API fails. Reason: {r.reason}")
-    # minus 2 for execution time error
-    spotify_token_expire_time = int(data['expires_in']) + time.time() - 2
-    spotify_token = data['access_token']
-
-
-class ImdbMovieScraper(AbstractScraper):
-    site_name = SourceSiteEnum.IMDB.value
-    host = 'https://www.imdb.com/title/'
-    data_class = Movie
-    form_class = MovieForm
-
-    regex = re.compile(r"(?<=https://www\.imdb\.com/title/)[a-zA-Z0-9]+")
-
-    def scrape(self, url):
-
-        effective_url = self.get_effective_url(url)
-        if effective_url is None:
-            raise ValueError("not valid url")
-
-        api_url = self.get_api_url(effective_url)
-        r = requests.get(api_url)
-        res_data = r.json()
-
-        if not res_data['type'] in ['Movie', 'TVSeries']:
-            raise ValueError("not movie/series item")
-
-        if res_data['type'] == 'Movie':
-            is_series = False
-        elif res_data['type'] == 'TVSeries':
-            is_series = True
-
-        title = res_data['title']
-        orig_title = res_data['originalTitle']
-        imdb_code = self.regex.findall(effective_url)[0]
-        director = []
-        for direct_dict in res_data['directorList']:
-            director.append(direct_dict['name'])
-        playwright = []
-        for writer_dict in res_data['writerList']:
-            playwright.append(writer_dict['name'])
-        actor = []
-        for actor_dict in res_data['actorList']:
-            actor.append(actor_dict['name'])
-        genre = res_data['genres'].split(', ')
-        area = res_data['countries'].split(', ')
-        language = res_data['languages'].split(', ')
-        year = int(res_data['year'])
-        duration = res_data['runtimeStr']
-        brief = res_data['plotLocal'] if res_data['plotLocal'] else res_data['plot']
-        if res_data['releaseDate']:
-            showtime = [{res_data['releaseDate']: "发布日期"}]
-        else:
-            showtime = None
-
-        other_info = {}
-        if res_data['contentRating']:
-            other_info['分级'] = res_data['contentRating'] 
-        if res_data['imDbRating']:
-            other_info['IMDb评分'] = res_data['imDbRating'] 
-        if res_data['metacriticRating']:
-            other_info['Metacritic评分'] = res_data['metacriticRating'] 
-        if res_data['awards']:
-            other_info['奖项'] = res_data['awards'] 
-
-        raw_img, ext = self.download_image(res_data['image'])
-
-        data = {
-            'title': title,
-            'orig_title': orig_title,
-            'other_title': None,
-            'imdb_code': imdb_code,
-            'director': director,
-            'playwright': playwright,
-            'actor': actor,
-            'genre': genre,
-            'showtime': showtime,
-            'site': None,
-            'area': area,
-            'language': language,
-            'year': year,
-            'duration': duration,
-            'season': None,
-            'episodes': None,
-            'single_episode_length': None,
-            'brief': brief,
-            'is_series': is_series,
-            'other_info': other_info,
-            'source_site': self.site_name,
-            'source_url': effective_url,
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-    @classmethod
-    def get_effective_url(cls, raw_url):
-        code = cls.regex.findall(raw_url)
-        if code:
-            return f"https://www.imdb.com/title/{code[0]}/"
-        else:
-            return None
-
-    @classmethod
-    def get_api_url(cls, url):
-        return f"https://imdb-api.com/zh/API/Title/{IMDB_API_KEY}/{cls.regex.findall(url)[0]}/FullActor,"
-
-
-class DoubanGameScraper(DoubanScrapperMixin, AbstractScraper):
-    site_name = SourceSiteEnum.DOUBAN.value
-    host = 'www.douban.com/game/'
-    data_class = Game
-    form_class = GameForm
-
-    regex = re.compile(r"https://www\.douban\.com/game/\d+/{0,1}")
-
-    def scrape(self, url):
-        headers = DEFAULT_REQUEST_HEADERS.copy()
-        headers['Host'] = 'www.douban.com'
-        content = self.download_page(url, headers)
-
-        try:
-            raw_title = content.xpath(
-                "//div[@id='content']/h1/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no game info")
-
-        title = raw_title
-
-        other_title_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='别名:']/following-sibling::dd[1]/text()")
-        other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None
-        
-        developer_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='开发商:']/following-sibling::dd[1]/text()")
-        developer = developer_elem[0].strip().split(' / ') if developer_elem else None
-
-        publisher_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='发行商:']/following-sibling::dd[1]/text()")
-        publisher = publisher_elem[0].strip().split(' / ') if publisher_elem else None
-
-        platform_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='平台:']/following-sibling::dd[1]/a/text()")
-        platform = platform_elem if platform_elem else None
-
-        genre_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='类型:']/following-sibling::dd[1]/a/text()")
-        genre = None
-        if genre_elem:
-            genre = [g for g in genre_elem if g != '游戏']
-
-        date_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='发行日期:']/following-sibling::dd[1]/text()")
-        release_date = parse_date(date_elem[0].strip()) if date_elem else None
-
-        brief_elem = content.xpath("//div[@class='mod item-desc']/p/text()")
-        brief = '\n'.join(brief_elem) if brief_elem else None
-
-        img_url_elem = content.xpath(
-            "//div[@class='item-subject-info']/div[@class='pic']//img/@src")
-        img_url = img_url_elem[0].strip() if img_url_elem else None
-        raw_img, ext = self.download_image(img_url)
-
-        data = {
-            'title': title,
-            'other_title': other_title,
-            'developer': developer,
-            'publisher': publisher,
-            'release_date': release_date,
-            'genre': genre,
-            'platform': platform,
-            'brief': brief,
-            'other_info': None,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-class SteamGameScraper(AbstractScraper):
-    site_name = SourceSiteEnum.STEAM.value
-    host = 'store.steampowered.com'
-    data_class = Game
-    form_class = GameForm
-
-    regex = re.compile(r"https://store\.steampowered\.com/app/\d+/{0,1}")
-
-    def scrape(self, url):
-        headers = DEFAULT_REQUEST_HEADERS.copy()
-        headers['Host'] = self.host
-        headers['Cookie'] = "wants_mature_content=1; birthtime=754700401;"
-        content = self.download_page(url, headers)
-        
-        title = content.xpath("//div[@class='apphub_AppName']/text()")[0]
-        developer = content.xpath("//div[@id='developers_list']/a/text()")
-        publisher = content.xpath("//div[@class='glance_ctn']//div[@class='dev_row'][2]//a/text()")
-        release_date = parse_date(
-            content.xpath(
-                "//div[@class='release_date']/div[@class='date']/text()")[0]
-        )
-
-        genre = content.xpath(
-            "//div[@class='details_block']/b[2]/following-sibling::a/text()")
-
-        platform = ['PC']
-
-        brief = content.xpath(
-            "//div[@class='game_description_snippet']/text()")[0].strip()
-
-        img_url = content.xpath(
-            "//img[@class='game_header_image_full']/@src"
-        )[0].replace("header.jpg", "library_600x900.jpg")
-        raw_img, ext = self.download_image(img_url)
-
-        # no 600x900 picture
-        if raw_img is None:
-            img_url = content.xpath("//img[@class='game_header_image_full']/@src")[0]
-            raw_img, ext = self.download_image(img_url)
-
-        data = {
-            'title': title,
-            'other_title': None,
-            'developer': developer,
-            'publisher': publisher,
-            'release_date': release_date,
-            'genre': genre,
-            'platform': platform,
-            'brief': brief,
-            'other_info': None,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-def find_entity(source_url):
-    """
-    for bangumi
-    """
-    # to be added when new scrape method is implemented
-    result = Game.objects.filter(source_url=source_url)
-    if result:
-        return result[0]
-    else:
-        raise ObjectDoesNotExist
-
-class BangumiScraper(AbstractScraper):
-    site_name = SourceSiteEnum.BANGUMI.value
-    host = 'bgm.tv'
-
-    # for interface coherence
-    data_class = type("FakeDataClass", (object,), {})()
-    data_class.objects = type("FakeObjectsClass", (object,), {})()
-    data_class.objects.get = find_entity
-    # should be set at scrape_* method
-    form_class = ''
-
-
-    regex = re.compile(r"https{0,1}://bgm\.tv/subject/\d+")
-
-    def scrape(self, url):
-        """
-        This is the scraping portal
-        """
-        headers = DEFAULT_REQUEST_HEADERS.copy()
-        headers['Host'] = self.host
-        content = self.download_page(url, headers)
-
-        # download image
-        img_url = 'http:' + content.xpath("//div[@class='infobox']//img[1]/@src")[0]
-        raw_img, ext = self.download_image(img_url)
-
-        # Test category
-        category_code = content.xpath("//div[@id='headerSearch']//option[@selected]/@value")[0]
-        handler_map = {
-            '1': self.scrape_book,
-            '2': self.scrape_movie,
-            '3': self.scrape_album,
-            '4': self.scrape_game
-        }
-        data = handler_map[category_code](self, content)
-        data['source_url'] = self.get_effective_url(url)
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-    def scrape_game(self, content):
-        self.data_class = Game
-        self.form_class = GameForm
-
-        title_elem = content.xpath("//a[@property='v:itemreviewed']/text()")
-        if not title_elem:
-            raise ValueError("no game info found on this page")
-            title = None
-        else:
-            title = title_elem[0].strip()
-
-        other_title_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/text()")
-        if not other_title_elem:
-            other_title_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/a/text()")
-        other_title = other_title_elem if other_title_elem else []
-
-        chinese_name_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/text()")
-        if not chinese_name_elem:
-            chinese_name_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/a/text()")
-        if chinese_name_elem:
-            chinese_name = chinese_name_elem[0]
-            # switch chinese name with original name
-            title, chinese_name = chinese_name, title
-            # actually the name appended is original
-            other_title.append(chinese_name)
-            
-        developer_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/text()")
-        if not developer_elem:
-            developer_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/a/text()")
-        developer = developer_elem if developer_elem else None
-
-        publisher_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/text()")
-        if not publisher_elem:
-            publisher_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/a/text()")
-        publisher = publisher_elem if publisher_elem else None
-
-        platform_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/text()")
-        if not platform_elem:
-            platform_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/a/text()")
-        platform = platform_elem if platform_elem else None
-
-        genre_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/text()")
-        if not genre_elem:
-            genre_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/a/text()")
-        genre = genre_elem if genre_elem else None
-
-        date_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/text()")
-        if not date_elem:
-            date_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/a/text()")
-        release_date = parse_date(date_elem[0]) if date_elem else None
-
-        brief = ''.join(content.xpath("//div[@property='v:summary']/text()"))
-        
-        other_info = {}
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'人数')]]/text()")
-        if other_elem:
-            other_info['游玩人数'] = other_elem[0]
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'引擎')]]/text()")
-        if other_elem:
-            other_info['引擎'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'售价')]]/text()")
-        if other_elem:
-            other_info['售价'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'官方网站')]]/text()")
-        if other_elem:
-            other_info['网站'] = other_elem[0]
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/text()")
-        if other_elem:
-            other_info['剧本'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/text()")
-        if other_elem:
-            other_info['编剧'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/text()")
-        if other_elem:
-            other_info['音乐'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/text()")
-        if other_elem:
-            other_info['美术'] = ' '.join(other_elem)
-
-        data = {
-            'title': title,
-            'other_title': None,
-            'developer': developer,
-            'publisher': publisher,
-            'release_date': release_date,
-            'genre': genre,
-            'platform': platform,
-            'brief': brief,
-            'other_info': other_info,
-            'source_site': self.site_name,
-        }
-
-        return data
-
-    def scrape_movie(self, content):
-        self.data_class = Movie
-        self.form_class = MovieForm
-        raise NotImplementedError
-
-    def scrape_book(self, content):
-        self.data_class = Book
-        self.form_class = BookForm
-        raise NotImplementedError
-
-    def scrape_album(self, content):
-        self.data_class = Album
-        self.form_class = AlbumForm
-        raise NotImplementedError
-
-
-# https://developers.google.com/youtube/v3/docs/?apix=true
-# https://developers.google.com/books/docs/v1/using
-
+from common.scrapers.bandcamp import BandcampAlbumScraper
+from common.scrapers.goodreads import GoodreadsScraper
+from common.scrapers.google import GoogleBooksScraper
+from common.scrapers.tmdb import TmdbMovieScraper
+from common.scrapers.steam import SteamGameScraper
+from common.scrapers.imdb import ImdbMovieScraper
+from common.scrapers.igdb import IgdbGameScraper
+from common.scrapers.spotify import SpotifyAlbumScraper, SpotifyTrackScraper
+from common.scrapers.douban import DoubanAlbumScraper, DoubanBookScraper, DoubanGameScraper, DoubanMovieScraper
+from common.scrapers.bangumi import BangumiScraper
+
+
+def get_scraper_by_url(url):
+    parsed_url = urllib.parse.urlparse(url)
+    hostname = parsed_url.netloc
+    for host in scraper_registry:
+        if host in url:
+            return scraper_registry[host]
+    # TODO move this logic to scraper class
+    try:
+        answers = dns.resolver.query(hostname, 'CNAME')
+        for rdata in answers:
+            if str(rdata.target) == 'dom.bandcamp.com.':
+                return BandcampAlbumScraper
+    except Exception as e:
+        pass
+    try:
+        answers = dns.resolver.query(hostname, 'A')
+        for rdata in answers:
+            if str(rdata.address) == '35.241.62.186':
+                return BandcampAlbumScraper
+    except Exception as e:
+        pass
+    return None
diff --git a/common/scrapers/bandcamp.py b/common/scrapers/bandcamp.py
new file mode 100644
index 00000000..5f939da6
--- /dev/null
+++ b/common/scrapers/bandcamp.py
@@ -0,0 +1,71 @@
+import re
+import dateparser
+import json
+from lxml import html
+from common.models import SourceSiteEnum
+from common.scraper import AbstractScraper
+from music.models import Album
+from music.forms import AlbumForm
+
+
+class BandcampAlbumScraper(AbstractScraper):
+    site_name = SourceSiteEnum.BANDCAMP.value
+    # API URL
+    host = '.bandcamp.com/'
+    data_class = Album
+    form_class = AlbumForm
+
+    regex = re.compile(r"https://[a-zA-Z0-9\-\.]+/album/[^?#]+")
+
+    def scrape(self, url, response=None):
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+        if response is not None:
+            content = html.fromstring(response.content.decode('utf-8'))
+        else:
+            content = self.download_page(url, {})
+        try:
+            title = content.xpath("//h2[@class='trackTitle']/text()")[0].strip()
+            artist = [content.xpath("//div[@id='name-section']/h3/span/a/text()")[0].strip()]
+        except IndexError:
+            raise ValueError("given url contains no valid info")
+
+        genre = []  # TODO: parse tags
+        track_list = []
+        release_nodes = content.xpath("//div[@class='tralbumData tralbum-credits']/text()")
+        release_date = dateparser.parse(re.sub(r'releas\w+ ', '', release_nodes[0].strip())) if release_nodes else None
+        duration = None
+        company = None
+        brief_nodes = content.xpath("//div[@class='tralbumData tralbum-about']/text()")
+        brief = "".join(brief_nodes) if brief_nodes else None
+        cover_url = content.xpath("//div[@id='tralbumArt']/a/@href")[0].strip()
+        bandcamp_page_data = json.loads(content.xpath(
+            "//meta[@name='bc-page-properties']/@content")[0].strip())
+        other_info = {}
+        other_info['bandcamp_album_id'] = bandcamp_page_data['item_id']
+
+        raw_img, ext = self.download_image(cover_url, url)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': genre,
+            'track_list': track_list,
+            'release_date': release_date,
+            'duration': duration,
+            'company': company,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+            'cover_url': cover_url,
+        }
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        url = cls.regex.findall(raw_url)
+        return url[0] if len(url) > 0 else None
diff --git a/common/scrapers/bangumi.py b/common/scrapers/bangumi.py
new file mode 100644
index 00000000..498ba849
--- /dev/null
+++ b/common/scrapers/bangumi.py
@@ -0,0 +1,199 @@
+import re
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from games.models import Game
+from games.forms import GameForm
+from common.scraper import *
+from django.core.exceptions import ObjectDoesNotExist
+
+
+def find_entity(source_url):
+    """
+    for bangumi
+    """
+    # to be added when new scrape method is implemented
+    result = Game.objects.filter(source_url=source_url)
+    if result:
+        return result[0]
+    else:
+        raise ObjectDoesNotExist
+
+
+class BangumiScraper(AbstractScraper):
+    site_name = SourceSiteEnum.BANGUMI.value
+    host = 'bgm.tv'
+
+    # for interface coherence
+    data_class = type("FakeDataClass", (object,), {})()
+    data_class.objects = type("FakeObjectsClass", (object,), {})()
+    data_class.objects.get = find_entity
+    # should be set at scrape_* method
+    form_class = ''
+
+    regex = re.compile(r"https{0,1}://bgm\.tv/subject/\d+")
+
+    def scrape(self, url):
+        """
+        This is the scraping portal
+        """
+        headers = DEFAULT_REQUEST_HEADERS.copy()
+        headers['Host'] = self.host
+        content = self.download_page(url, headers)
+
+        # download image
+        img_url = 'http:' + content.xpath("//div[@class='infobox']//img[1]/@src")[0]
+        raw_img, ext = self.download_image(img_url, url)
+
+        # Test category
+        category_code = content.xpath("//div[@id='headerSearch']//option[@selected]/@value")[0]
+        handler_map = {
+            '1': self.scrape_book,
+            '2': self.scrape_movie,
+            '3': self.scrape_album,
+            '4': self.scrape_game
+        }
+        data = handler_map[category_code](self, content)
+        data['source_url'] = self.get_effective_url(url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    def scrape_game(self, content):
+        self.data_class = Game
+        self.form_class = GameForm
+
+        title_elem = content.xpath("//a[@property='v:itemreviewed']/text()")
+        if not title_elem:
+            raise ValueError("no game info found on this page")
+            title = None
+        else:
+            title = title_elem[0].strip()
+
+        other_title_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/text()")
+        if not other_title_elem:
+            other_title_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/a/text()")
+        other_title = other_title_elem if other_title_elem else []
+
+        chinese_name_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/text()")
+        if not chinese_name_elem:
+            chinese_name_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/a/text()")
+        if chinese_name_elem:
+            chinese_name = chinese_name_elem[0]
+            # switch chinese name with original name
+            title, chinese_name = chinese_name, title
+            # actually the name appended is original
+            other_title.append(chinese_name)
+
+        developer_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/text()")
+        if not developer_elem:
+            developer_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/a/text()")
+        developer = developer_elem if developer_elem else None
+
+        publisher_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/text()")
+        if not publisher_elem:
+            publisher_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/a/text()")
+        publisher = publisher_elem if publisher_elem else None
+
+        platform_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/text()")
+        if not platform_elem:
+            platform_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/a/text()")
+        platform = platform_elem if platform_elem else None
+
+        genre_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/text()")
+        if not genre_elem:
+            genre_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/a/text()")
+        genre = genre_elem if genre_elem else None
+
+        date_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/text()")
+        if not date_elem:
+            date_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/a/text()")
+        release_date = parse_date(date_elem[0]) if date_elem else None
+
+        brief = ''.join(content.xpath("//div[@property='v:summary']/text()"))
+
+        other_info = {}
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'人数')]]/text()")
+        if other_elem:
+            other_info['游玩人数'] = other_elem[0]
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'引擎')]]/text()")
+        if other_elem:
+            other_info['引擎'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'售价')]]/text()")
+        if other_elem:
+            other_info['售价'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'官方网站')]]/text()")
+        if other_elem:
+            other_info['网站'] = other_elem[0]
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/text()")
+        if other_elem:
+            other_info['剧本'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/text()")
+        if other_elem:
+            other_info['编剧'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/text()")
+        if other_elem:
+            other_info['音乐'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/text()")
+        if other_elem:
+            other_info['美术'] = ' '.join(other_elem)
+
+        data = {
+            'title': title,
+            'other_title': None,
+            'developer': developer,
+            'publisher': publisher,
+            'release_date': release_date,
+            'genre': genre,
+            'platform': platform,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+        }
+
+        return data
+
+    def scrape_movie(self, content):
+        self.data_class = Movie
+        self.form_class = MovieForm
+        raise NotImplementedError
+
+    def scrape_book(self, content):
+        self.data_class = Book
+        self.form_class = BookForm
+        raise NotImplementedError
+
+    def scrape_album(self, content):
+        self.data_class = Album
+        self.form_class = AlbumForm
+        raise NotImplementedError
diff --git a/common/scrapers/douban.py b/common/scrapers/douban.py
new file mode 100644
index 00000000..6dcaff69
--- /dev/null
+++ b/common/scrapers/douban.py
@@ -0,0 +1,714 @@
+import requests
+import re
+import filetype
+from lxml import html
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album
+from music.forms import AlbumForm
+from games.models import Game
+from games.forms import GameForm
+from django.core.validators import URLValidator
+from django.conf import settings
+from PIL import Image
+from io import BytesIO
+from common.scraper import *
+
+
+class DoubanScrapperMixin:
+    @classmethod
+    def download_page(cls, url, headers):
+        url = cls.get_effective_url(url)
+        r = None
+        error = 'DoubanScrapper: error occured when downloading ' + url
+        content = None
+        last_error = None
+
+        def get(url):
+            nonlocal r
+            # print('Douban GET ' + url)
+            try:
+                r = requests.get(url, timeout=settings.SCRAPING_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, last_error
+            content = None
+            last_error = 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
+                    last_error = 'network'
+                elif content.find('<title>页面不存在</title>') != -1 or content.find('呃... 你想访问的条目豆瓣不收录。') != -1:  # re.search('不存在[^<]+</title>', content, re.MULTILINE):
+                    content = None
+                    last_error = 'censorship'
+                    error = error + 'Not found or hidden by Douban'
+            elif r.status_code == 204:
+                content = None
+                last_error = 'censorship'
+                error = error + 'Not found or hidden by Douban'
+            else:
+                content = None
+                last_error = 'network'
+                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)
+            if r.status_code == 200:
+                w = r.json()
+                if w['archived_snapshots'] and w['archived_snapshots']['closest']:
+                    get(w['archived_snapshots']['closest']['url'])
+                    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)
+            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)
+                    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 not None:
+                error = error + '\nScrapeStack: '
+                get(f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}')
+            elif settings.SCRAPERAPI_KEY is not None:
+                error = error + '\nScraperAPI: '
+                get(f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}')
+            else:
+                error = error + '\nDirect: '
+                get(url)
+            check_content()
+            if last_error == 'network' and settings.PROXYCRAWL_KEY is not None:
+                error = error + '\nProxyCrawl: '
+                get(f'https://api.proxycrawl.com/?token={settings.PROXYCRAWL_KEY}&url={url}')
+                check_content()
+            if last_error == 'censorship' and settings.LOCAL_PROXY is not None:
+                error = error + '\nLocal: '
+                get(f'{settings.LOCAL_PROXY}?url={url}')
+                check_content()
+
+        latest()
+        if content is None:
+            wayback_cdx()
+
+        if content is None:
+            raise RuntimeError(error)
+        # 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):
+        raw_img = None
+        ext = None
+
+        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}'
+        else:
+            dl_url = url
+
+        try:
+            img_response = requests.get(dl_url, timeout=settings.SCRAPING_TIMEOUT)
+            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.PROXYCRAWL_KEY is not None:
+            try:
+                dl_url = f'https://api.proxycrawl.com/?token={settings.PROXYCRAWL_KEY}&url={url}'
+                img_response = requests.get(dl_url, timeout=settings.SCRAPING_TIMEOUT)
+                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 DoubanBookScraper(DoubanScrapperMixin, 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)
+
+        isbn_elem = content.xpath("//div[@id='info']//span[text()='ISBN:']/following::text()")
+        isbn = isbn_elem[0].strip() if isbn_elem else None
+        title_elem = content.xpath("/html/body//h1/span/text()")
+        title = title_elem[0].strip() if title_elem else None
+        if not title:
+            if isbn:
+                title = 'isbn: ' + isbn
+            else:
+                raise ValueError("given url contains no book title or isbn")
+
+        subtitle_elem = content.xpath(
+            "//div[@id='info']//span[text()='副标题:']/following::text()")
+        subtitle = subtitle_elem[0].strip()[:500] if subtitle_elem else None
+
+        orig_title_elem = content.xpath(
+            "//div[@id='info']//span[text()='原作名:']/following::text()")
+        orig_title = orig_title_elem[0].strip()[:500] if orig_title_elem else None
+
+        language_elem = content.xpath(
+            "//div[@id='info']//span[text()='语言:']/following::text()")
+        language = language_elem[0].strip() if language_elem else None
+
+        pub_house_elem = content.xpath(
+            "//div[@id='info']//span[text()='出版社:']/following::text()")
+        pub_house = pub_house_elem[0].strip() if pub_house_elem else None
+
+        pub_date_elem = content.xpath(
+            "//div[@id='info']//span[text()='出版年:']/following::text()")
+        pub_date = pub_date_elem[0].strip() if pub_date_elem else ''
+        year_month_day = RE_NUMBERS.findall(pub_date)
+        if len(year_month_day) in (2, 3):
+            pub_year = int(year_month_day[0])
+            pub_month = int(year_month_day[1])
+        elif len(year_month_day) == 1:
+            pub_year = int(year_month_day[0])
+            pub_month = None
+        else:
+            pub_year = None
+            pub_month = None
+        if pub_year and pub_month and pub_year < pub_month:
+            pub_year, pub_month = pub_month, pub_year
+        pub_year = None if pub_year is not None and pub_year not in range(
+            0, 3000) else pub_year
+        pub_month = None if pub_month is not None and pub_month not in range(
+            1, 12) else pub_month
+
+        binding_elem = content.xpath(
+            "//div[@id='info']//span[text()='装帧:']/following::text()")
+        binding = binding_elem[0].strip() if binding_elem else None
+
+        price_elem = content.xpath(
+            "//div[@id='info']//span[text()='定价:']/following::text()")
+        price = price_elem[0].strip() if price_elem else None
+
+        pages_elem = content.xpath(
+            "//div[@id='info']//span[text()='页数:']/following::text()")
+        pages = pages_elem[0].strip() if pages_elem else None
+        if pages is not None:
+            pages = int(RE_NUMBERS.findall(pages)[
+                        0]) if RE_NUMBERS.findall(pages) else None
+            if pages and (pages > 999999 or pages < 1):
+                pages = None
+
+        brief_elem = content.xpath(
+            "//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()")
+        brief = '\n'.join(p.strip()
+                          for p in brief_elem) if brief_elem else None
+
+        contents = None
+        try:
+            contents_elem = content.xpath(
+                "//h2/span[text()='目录']/../following-sibling::div[1]")[0]
+            # if next the id of next sibling contains `dir`, that would be the full contents
+            if "dir" in contents_elem.getnext().xpath("@id")[0]:
+                contents_elem = contents_elem.getnext()
+                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
+                    "text()")[:-2]) if contents_elem else None
+            else:
+                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
+                    "text()")) if contents_elem else None
+        except Exception:
+            pass
+
+        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)
+
+        # there are two html formats for authors and translators
+        authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
+            preceding-sibling::a[preceding-sibling::span[text()='作者:']]/text()""")
+        if not authors_elem:
+            authors_elem = content.xpath(
+                """//div[@id='info']//span[text()=' 作者']/following-sibling::a/text()""")
+        if authors_elem:
+            authors = []
+            for author in authors_elem:
+                authors.append(RE_WHITESPACES.sub(' ', author.strip())[:200])
+        else:
+            authors = None
+
+        translators_elem = content.xpath("""//div[@id='info']//span[text()='译者:']/following-sibling::br[1]/
+            preceding-sibling::a[preceding-sibling::span[text()='译者:']]/text()""")
+        if not translators_elem:
+            translators_elem = content.xpath(
+                """//div[@id='info']//span[text()=' 译者']/following-sibling::a/text()""")
+        if translators_elem:
+            translators = []
+            for translator in translators_elem:
+                translators.append(RE_WHITESPACES.sub(' ', translator.strip()))
+        else:
+            translators = None
+
+        other = {}
+        cncode_elem = content.xpath(
+            "//div[@id='info']//span[text()='统一书号:']/following::text()")
+        if cncode_elem:
+            other['统一书号'] = cncode_elem[0].strip()
+        series_elem = content.xpath(
+            "//div[@id='info']//span[text()='丛书:']/following-sibling::a[1]/text()")
+        if series_elem:
+            other['丛书'] = series_elem[0].strip()
+        imprint_elem = content.xpath(
+            "//div[@id='info']//span[text()='出品方:']/following-sibling::a[1]/text()")
+        if imprint_elem:
+            other['出品方'] = imprint_elem[0].strip()
+
+        data = {
+            'title': title,
+            'subtitle': subtitle,
+            'orig_title': orig_title,
+            'author': authors,
+            'translator': translators,
+            'language': language,
+            'pub_house': pub_house,
+            'pub_year': pub_year,
+            'pub_month': pub_month,
+            'binding': binding,
+            'price': price,
+            'pages': pages,
+            'isbn': isbn,
+            'brief': brief,
+            'contents': contents,
+            'other_info': other,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+
+class DoubanMovieScraper(DoubanScrapperMixin, 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)
+
+        # parsing starts here
+        try:
+            raw_title = content.xpath(
+                "//span[@property='v:itemreviewed']/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no movie info")
+
+        orig_title = content.xpath(
+            "//img[@rel='v:image']/@alt")[0].strip()
+        title = raw_title.split(orig_title)[0].strip()
+        # if has no chinese title
+        if title == '':
+            title = orig_title
+
+        if title == orig_title:
+            orig_title = None
+
+        # there are two html formats for authors and translators
+        other_title_elem = content.xpath(
+            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
+        other_title = other_title_elem[0].strip().split(
+            ' / ') if other_title_elem else None
+
+        imdb_elem = content.xpath(
+            "//div[@id='info']//span[text()='IMDb链接:']/following-sibling::a[1]/text()")
+        if not imdb_elem:
+            imdb_elem = content.xpath(
+                "//div[@id='info']//span[text()='IMDb:']/following-sibling::text()[1]")
+        imdb_code = imdb_elem[0].strip() if imdb_elem else None
+
+        director_elem = content.xpath(
+            "//div[@id='info']//span[text()='导演']/following-sibling::span[1]/a/text()")
+        director = director_elem if director_elem else None
+
+        playwright_elem = content.xpath(
+            "//div[@id='info']//span[text()='编剧']/following-sibling::span[1]/a/text()")
+        playwright = list(map(lambda a: a[:200], playwright_elem)) if playwright_elem else None
+
+        actor_elem = content.xpath(
+            "//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()")
+        actor = list(map(lambda a: a[:200], actor_elem)) if actor_elem else None
+
+        # construct genre translator
+        genre_translator = {}
+        attrs = [attr for attr in dir(MovieGenreEnum) if '__' not in attr]
+        for attr in attrs:
+            genre_translator[getattr(MovieGenreEnum, attr).label] = getattr(
+                MovieGenreEnum, attr).value
+
+        genre_elem = content.xpath("//span[@property='v:genre']/text()")
+        if genre_elem:
+            genre = []
+            for g in genre_elem:
+                g = g.split(' ')[0]
+                if g == '紀錄片':  # likely some original data on douban was corrupted
+                    g = '纪录片'
+                elif g == '鬼怪':
+                    g = '惊悚'
+                if g in genre_translator:
+                    genre.append(genre_translator[g])
+                elif g in genre_translator.values():
+                    genre.append(g)
+                else:
+                    logger.error(f'unable to map genre {g}')
+        else:
+            genre = None
+
+        showtime_elem = content.xpath(
+            "//span[@property='v:initialReleaseDate']/text()")
+        if showtime_elem:
+            showtime = []
+            for st in showtime_elem:
+                parts = st.split('(')
+                if len(parts) == 1:
+                    time = st.split('(')[0]
+                    region = ''
+                else:
+                    time = st.split('(')[0]
+                    region = st.split('(')[1][0:-1]
+                showtime.append({time: region})
+        else:
+            showtime = None
+
+        site_elem = content.xpath(
+            "//div[@id='info']//span[text()='官方网站:']/following-sibling::a[1]/@href")
+        site = site_elem[0].strip()[:200] if site_elem else None
+        try:
+            validator = URLValidator()
+            validator(site)
+        except ValidationError:
+            site = None
+
+        area_elem = content.xpath(
+            "//div[@id='info']//span[text()='制片国家/地区:']/following-sibling::text()[1]")
+        if area_elem:
+            area = [a.strip()[:100] for a in area_elem[0].split('/')]
+        else:
+            area = None
+
+        language_elem = content.xpath(
+            "//div[@id='info']//span[text()='语言:']/following-sibling::text()[1]")
+        if language_elem:
+            language = [a.strip() for a in language_elem[0].split(' / ')]
+        else:
+            language = None
+
+        year_elem = content.xpath("//span[@class='year']/text()")
+        year = int(re.search(r'\d+', year_elem[0])[0]) if year_elem and re.search(r'\d+', year_elem[0]) else None
+
+        duration_elem = content.xpath("//span[@property='v:runtime']/text()")
+        other_duration_elem = content.xpath(
+            "//span[@property='v:runtime']/following-sibling::text()[1]")
+        if duration_elem:
+            duration = duration_elem[0].strip()
+            if other_duration_elem:
+                duration += other_duration_elem[0].rstrip()
+            duration = duration.split('/')[0].strip()
+        else:
+            duration = None
+
+        season_elem = content.xpath(
+            "//*[@id='season']/option[@selected='selected']/text()")
+        if not season_elem:
+            season_elem = content.xpath(
+                "//div[@id='info']//span[text()='季数:']/following-sibling::text()[1]")
+            season = int(season_elem[0].strip()) if season_elem else None
+        else:
+            season = int(season_elem[0].strip())
+
+        episodes_elem = content.xpath(
+            "//div[@id='info']//span[text()='集数:']/following-sibling::text()[1]")
+        episodes = int(episodes_elem[0].strip()) if episodes_elem and episodes_elem[0].isdigit() else None
+
+        single_episode_length_elem = content.xpath(
+            "//div[@id='info']//span[text()='单集片长:']/following-sibling::text()[1]")
+        single_episode_length = single_episode_length_elem[0].strip(
+        )[:100] if single_episode_length_elem else None
+
+        # if has field `episodes` not none then must be series
+        is_series = True if episodes else False
+
+        brief_elem = content.xpath("//span[@class='all hidden']")
+        if not brief_elem:
+            brief_elem = content.xpath("//span[@property='v:summary']")
+        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
+            './text()')]) if brief_elem else None
+
+        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)
+
+        data = {
+            'title': title,
+            'orig_title': orig_title,
+            'other_title': other_title,
+            'imdb_code': imdb_code,
+            'director': director,
+            'playwright': playwright,
+            'actor': actor,
+            'genre': genre,
+            'showtime': showtime,
+            'site': site,
+            'area': area,
+            'language': language,
+            'year': year,
+            'duration': duration,
+            'season': season,
+            'episodes': episodes,
+            'single_episode_length': single_episode_length,
+            'brief': brief,
+            'is_series': is_series,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+
+class DoubanAlbumScraper(DoubanScrapperMixin, 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)
+
+        # parsing starts here
+        try:
+            title = content.xpath("//h1/span/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no album info")
+        if not title:
+            raise ValueError("given url contains no album info")
+
+        artists_elem = content.xpath("//div[@id='info']/span/span[@class='pl']/a/text()")
+        artist = None if not artists_elem else list(map(lambda a: a[:200], artists_elem))
+
+        genre_elem = content.xpath(
+            "//div[@id='info']//span[text()='流派:']/following::text()[1]")
+        genre = genre_elem[0].strip() if genre_elem else None
+
+        date_elem = content.xpath(
+            "//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
+        release_date = parse_date(date_elem[0].strip()) if date_elem else None
+
+        company_elem = content.xpath(
+            "//div[@id='info']//span[text()='出版者:']/following::text()[1]")
+        company = company_elem[0].strip() if company_elem else None
+
+        track_list_elem = content.xpath(
+            "//div[@class='track-list']/div[@class='indent']/div/text()"
+        )
+        if track_list_elem:
+            track_list = '\n'.join([track.strip() for track in track_list_elem])
+        else:
+            track_list = None
+
+        brief_elem = content.xpath("//span[@class='all hidden']")
+        if not brief_elem:
+            brief_elem = content.xpath("//span[@property='v:summary']")
+        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
+            './text()')]) if brief_elem else None
+
+        other_info = {}
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['又名'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['专辑类型'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['介质'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['ISRC'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['条形码'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['碟片数'] = other_elem[0].strip()
+
+        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)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': genre,
+            'release_date': release_date,
+            'duration': None,
+            'company': company,
+            'track_list': track_list,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+
+class DoubanGameScraper(DoubanScrapperMixin, AbstractScraper):
+    site_name = SourceSiteEnum.DOUBAN.value
+    host = 'www.douban.com/game/'
+    data_class = Game
+    form_class = GameForm
+
+    regex = re.compile(r"https://www\.douban\.com/game/\d+/{0,1}")
+
+    def scrape(self, url):
+        headers = DEFAULT_REQUEST_HEADERS.copy()
+        headers['Host'] = 'www.douban.com'
+        content = self.download_page(url, headers)
+
+        try:
+            raw_title = content.xpath(
+                "//div[@id='content']/h1/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no game info")
+
+        title = raw_title
+
+        other_title_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='别名:']/following-sibling::dd[1]/text()")
+        other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None
+
+        developer_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='开发商:']/following-sibling::dd[1]/text()")
+        developer = developer_elem[0].strip().split(' / ') if developer_elem else None
+
+        publisher_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='发行商:']/following-sibling::dd[1]/text()")
+        publisher = publisher_elem[0].strip().split(' / ') if publisher_elem else None
+
+        platform_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='平台:']/following-sibling::dd[1]/a/text()")
+        platform = platform_elem if platform_elem else None
+
+        genre_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='类型:']/following-sibling::dd[1]/a/text()")
+        genre = None
+        if genre_elem:
+            genre = [g for g in genre_elem if g != '游戏']
+
+        date_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='发行日期:']/following-sibling::dd[1]/text()")
+        release_date = parse_date(date_elem[0].strip()) if date_elem else None
+
+        brief_elem = content.xpath("//div[@class='mod item-desc']/p/text()")
+        brief = '\n'.join(brief_elem) if brief_elem else None
+
+        img_url_elem = content.xpath(
+            "//div[@class='item-subject-info']/div[@class='pic']//img/@src")
+        img_url = img_url_elem[0].strip() if img_url_elem else None
+        raw_img, ext = self.download_image(img_url, url)
+
+        data = {
+            'title': title,
+            'other_title': other_title,
+            'developer': developer,
+            'publisher': publisher,
+            'release_date': release_date,
+            'genre': genre,
+            'platform': platform,
+            'brief': brief,
+            'other_info': None,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
diff --git a/common/scrapers/goodreads.py b/common/scrapers/goodreads.py
new file mode 100644
index 00000000..813fd063
--- /dev/null
+++ b/common/scrapers/goodreads.py
@@ -0,0 +1,157 @@
+import requests
+import re
+import filetype
+from lxml import html
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from games.models import Game
+from games.forms import GameForm
+from django.conf import settings
+from PIL import Image
+from io import BytesIO
+from common.scraper import *
+
+
+class GoodreadsScraper(AbstractScraper):
+    site_name = SourceSiteEnum.GOODREADS.value
+    host = "www.goodreads.com"
+    data_class = Book
+    form_class = BookForm
+    regex = re.compile(r"https://www\.goodreads\.com/book/show/\d+")
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        u = re.match(r".+/book/show/(\d+)", raw_url)
+        if not u:
+            u = re.match(r".+book/(\d+)", raw_url)
+        return "https://www.goodreads.com/book/show/" + u[1] if u else None
+
+    def scrape(self, url, response=None):
+        """
+        This is the scraping portal
+        """
+        if response is not None:
+            content = html.fromstring(response.content.decode('utf-8'))
+        else:
+            headers = None  # DEFAULT_REQUEST_HEADERS.copy()
+            content = self.download_page(url, headers)
+
+        try:
+            title = content.xpath("//h1[@id='bookTitle']/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no book info")
+
+        subtitle = None
+
+        orig_title_elem = content.xpath("//div[@id='bookDataBox']//div[text()='Original Title']/following-sibling::div/text()")
+        orig_title = orig_title_elem[0].strip() if orig_title_elem else None
+
+        language_elem = content.xpath('//div[@itemprop="inLanguage"]/text()')
+        language = language_elem[0].strip() if language_elem else None
+
+        pub_house_elem = content.xpath("//div[contains(text(), 'Published') and @class='row']/text()")
+        try:
+            months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
+            r = re.compile('.*Published.*(' + '|'.join(months) + ').*(\\d\\d\\d\\d).+by\\s*(.+)\\s*', re.DOTALL)
+            pub = r.match(pub_house_elem[0])
+            pub_year = pub[2]
+            pub_month = months.index(pub[1]) + 1
+            pub_house = pub[3].strip()
+        except Exception:
+            pub_year = None
+            pub_month = None
+            pub_house = None
+
+        pub_house_elem = content.xpath("//nobr[contains(text(), 'first published')]/text()")
+        try:
+            pub = re.match(r'.*first published\s+(.+\d\d\d\d).*', pub_house_elem[0], re.DOTALL)
+            first_pub = pub[1]
+        except Exception:
+            first_pub = None
+
+        binding_elem = content.xpath('//span[@itemprop="bookFormat"]/text()')
+        binding = binding_elem[0].strip() if binding_elem else None
+
+        pages_elem = content.xpath('//span[@itemprop="numberOfPages"]/text()')
+        pages = pages_elem[0].strip() if pages_elem else None
+        if pages is not None:
+            pages = int(RE_NUMBERS.findall(pages)[
+                        0]) if RE_NUMBERS.findall(pages) else None
+
+        isbn_elem = content.xpath('//span[@itemprop="isbn"]/text()')
+        if not isbn_elem:
+            isbn_elem = content.xpath('//div[@itemprop="isbn"]/text()')  # this is likely ASIN
+        isbn = isbn_elem[0].strip() if isbn_elem else None
+
+        brief_elem = content.xpath('//div[@id="description"]/span[@style="display:none"]/text()')
+        if brief_elem:
+            brief = '\n'.join(p.strip() for p in brief_elem)
+        else:
+            brief_elem = content.xpath('//div[@id="description"]/span/text()')
+            brief = '\n'.join(p.strip() for p in brief_elem) if brief_elem else None
+
+        genre = content.xpath('//div[@class="bigBoxBody"]/div/div/div/a/text()')
+        genre = genre[0] if genre else None
+        book_title = re.sub('\n', '', content.xpath('//h1[@id="bookTitle"]/text()')[0]).strip()
+        author = content.xpath('//a[@class="authorName"]/span/text()')[0]
+        contents = None
+
+        img_url_elem = content.xpath("//img[@id='coverImage']/@src")
+        img_url = img_url_elem[0].strip() if img_url_elem else None
+        raw_img, ext = self.download_image(img_url, url)
+
+        authors_elem = content.xpath("//a[@class='authorName'][not(../span[@class='authorName greyText smallText role'])]/span/text()")
+        if authors_elem:
+            authors = []
+            for author in authors_elem:
+                authors.append(RE_WHITESPACES.sub(' ', author.strip()))
+        else:
+            authors = None
+
+        translators = None
+        authors_elem = content.xpath("//a[@class='authorName'][../span/text()='(Translator)']/span/text()")
+        if authors_elem:
+            translators = []
+            for translator in authors_elem:
+                translators.append(RE_WHITESPACES.sub(' ', translator.strip()))
+        else:
+            translators = None
+
+        other = {}
+        if first_pub:
+            other['首版时间'] = first_pub
+        if genre:
+            other['分类'] = genre
+        series_elem = content.xpath("//h2[@id='bookSeries']/a/text()")
+        if series_elem:
+            other['丛书'] = re.sub(r'\(\s*(.+[^\s])\s*#.*\)', '\\1', series_elem[0].strip())
+
+        data = {
+            'title': title,
+            'subtitle': subtitle,
+            'orig_title': orig_title,
+            'author': authors,
+            'translator': translators,
+            'language': language,
+            'pub_house': pub_house,
+            'pub_year': pub_year,
+            'pub_month': pub_month,
+            'binding': binding,
+            'pages': pages,
+            'isbn': isbn,
+            'brief': brief,
+            'contents': contents,
+            'other_info': other,
+            'cover_url': img_url,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        data['source_url'] = self.get_effective_url(url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
diff --git a/common/scrapers/google.py b/common/scrapers/google.py
new file mode 100644
index 00000000..0082fb3e
--- /dev/null
+++ b/common/scrapers/google.py
@@ -0,0 +1,102 @@
+import requests
+import re
+import filetype
+from lxml import html
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from games.models import Game
+from games.forms import GameForm
+from django.conf import settings
+from PIL import Image
+from io import BytesIO
+from common.scraper import *
+
+
+# https://developers.google.com/youtube/v3/docs/?apix=true
+# https://developers.google.com/books/docs/v1/using
+class GoogleBooksScraper(AbstractScraper):
+    site_name = SourceSiteEnum.GOOGLEBOOKS.value
+    host = ["books.google.com", "www.google.com/books"]
+    data_class = Book
+    form_class = BookForm
+    regex = re.compile(r"https://books\.google\.com/books\?id=([^&#]+)")
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        # https://books.google.com/books?id=wUHxzgEACAAJ
+        # https://books.google.com/books/about/%E7%8F%BE%E5%A0%B4%E6%AD%B7%E5%8F%B2.html?id=nvNoAAAAIAAJ
+        # https://www.google.com/books/edition/_/nvNoAAAAIAAJ?hl=en&gbpv=1
+        u = re.match(r"https://books\.google\.com/books.*id=([^&#]+)", raw_url)
+        if not u:
+            u = re.match(r"https://www\.google\.com/books/edition/[^/]+/([^&#?]+)", raw_url)
+        return 'https://books.google.com/books?id=' + u[1] if u else None
+
+    def scrape(self, url, response=None):
+        url = self.get_effective_url(url)
+        m = self.regex.match(url)
+        if m:
+            api_url = f'https://www.googleapis.com/books/v1/volumes/{m[1]}'
+        else:
+            raise ValueError("not valid url")
+        b = requests.get(api_url).json()
+        other = {}
+        title = b['volumeInfo']['title']
+        subtitle = b['volumeInfo']['subtitle'] if 'subtitle' in b['volumeInfo'] else None
+        pub_year = None
+        pub_month = None
+        if 'publishedDate' in b['volumeInfo']:
+            pub_date = b['volumeInfo']['publishedDate'].split('-')
+            pub_year = pub_date[0]
+            pub_month = pub_date[1] if len(pub_date) > 1 else None
+        pub_house = b['volumeInfo']['publisher'] if 'publisher' in b['volumeInfo'] else None
+        language = b['volumeInfo']['language'] if 'language' in b['volumeInfo'] else None
+        pages = b['volumeInfo']['pageCount'] if 'pageCount' in b['volumeInfo'] else None
+        if 'mainCategory' in b['volumeInfo']:
+            other['分类'] = b['volumeInfo']['mainCategory']
+        authors = b['volumeInfo']['authors'] if 'authors' in b['volumeInfo'] else None
+        if 'description' in b['volumeInfo']:
+            brief = b['volumeInfo']['description']
+        elif 'textSnippet' in b['volumeInfo']:
+            brief = b["volumeInfo"]["textSnippet"]["searchInfo"]
+        else:
+            brief = ''
+        brief = re.sub(r'<.*?>', '', brief.replace('<br', '\n<br'))
+        img_url = b['volumeInfo']['imageLinks']['thumbnail'] if 'imageLinks' in b['volumeInfo'] else None
+        isbn10 = None
+        isbn13 = None
+        for iid in b['volumeInfo']['industryIdentifiers'] if 'industryIdentifiers' in b['volumeInfo'] else []:
+            if iid['type'] == 'ISBN_10':
+                isbn10 = iid['identifier']
+            if iid['type'] == 'ISBN_13':
+                isbn13 = iid['identifier']
+        isbn = isbn13 if isbn13 is not None else isbn10
+
+        data = {
+            'title': title,
+            'subtitle': subtitle,
+            'orig_title': None,
+            'author': authors,
+            'translator': None,
+            'language': language,
+            'pub_house': pub_house,
+            'pub_year': pub_year,
+            'pub_month': pub_month,
+            'binding': None,
+            'pages': pages,
+            'isbn': isbn,
+            'brief': brief,
+            'contents': None,
+            'other_info': other,
+            'cover_url': img_url,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        raw_img, ext = self.download_image(img_url, url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
diff --git a/common/scrapers/igdb.py b/common/scrapers/igdb.py
new file mode 100644
index 00000000..eb635f5c
--- /dev/null
+++ b/common/scrapers/igdb.py
@@ -0,0 +1,88 @@
+import requests
+import re
+from common.models import SourceSiteEnum
+from games.models import Game
+from games.forms import GameForm
+from django.conf import settings
+from common.scraper import *
+from igdb.wrapper import IGDBWrapper
+import json
+import datetime
+
+
+wrapper = IGDBWrapper(settings.IGDB_CLIENT_ID, settings.IGDB_ACCESS_TOKEN)
+
+
+class IgdbGameScraper(AbstractScraper):
+    site_name = SourceSiteEnum.IGDB.value
+    host = 'https://www.igdb.com/'
+    data_class = Game
+    form_class = GameForm
+    regex = re.compile(r"https://www\.igdb\.com/games/([a-zA-Z0-9\-_]+)")
+
+    def scrape_steam(self, steam_url):
+        r = json.loads(wrapper.api_request('websites', f'fields *, game.*; where url = "{steam_url}";'))
+        if not r:
+            raise ValueError("Cannot find steam url in IGDB")
+        r = sorted(r, key=lambda w: w['game']['id'])
+        return self.scrape(r[0]['game']['url'])
+
+    def scrape(self, url):
+        m = self.regex.match(url)
+        if m:
+            effective_url = m[0]
+        else:
+            raise ValueError("not valid url")
+        effective_url = m[0]
+        slug = m[1]
+        fields = '*, cover.url, genres.name, platforms.name, involved_companies.*, involved_companies.company.name'
+        r = json.loads(wrapper.api_request('games', f'fields {fields}; where url = "{effective_url}";'))[0]
+        brief = r['summary'] if 'summary' in r else ''
+        brief += "\n\n" + r['storyline'] if 'storyline' in r else ''
+        developer = None
+        publisher = None
+        release_date = None
+        genre = None
+        platform = None
+        if 'involved_companies' in r:
+            developer = next(iter([c['company']['name'] for c in r['involved_companies'] if c['developer'] == True]), None)
+            publisher = next(iter([c['company']['name'] for c in r['involved_companies'] if c['publisher'] == True]), None)
+        if 'platforms' in r:
+            ps = sorted(r['platforms'], key=lambda p: p['id'])
+            platform = [(p['name'] if p['id'] != 6 else 'Windows') for p in ps]
+        if 'first_release_date' in r:
+            release_date = datetime.datetime.fromtimestamp(r['first_release_date'], datetime.timezone.utc)
+        if 'genres' in r:
+            genre = [g['name'] for g in r['genres']]
+        other_info = {'igdb_id': r['id']}
+        websites = json.loads(wrapper.api_request('websites', f'fields *; where game.url = "{effective_url}";'))
+        for website in websites:
+            if website['category'] == 1:
+                other_info['official_site'] = website['url']
+            elif website['category'] == 13:
+                other_info['steam_url'] = website['url']
+        data = {
+            'title': r['name'],
+            'other_title': None,
+            'developer': developer,
+            'publisher': publisher,
+            'release_date': release_date,
+            'genre': genre,
+            'platform': platform,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        raw_img, ext = self.download_image('https:' + r['cover']['url'].replace('t_thumb', 't_cover_big'), url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        m = cls.regex.match(raw_url)
+        if m:
+            return m[0]
+        else:
+            return None
diff --git a/common/scrapers/imdb.py b/common/scrapers/imdb.py
new file mode 100644
index 00000000..97c040bb
--- /dev/null
+++ b/common/scrapers/imdb.py
@@ -0,0 +1,116 @@
+import requests
+import re
+from common.models import SourceSiteEnum
+from movies.forms import MovieForm
+from movies.models import Movie
+from django.conf import settings
+from common.scraper import *
+
+
+class ImdbMovieScraper(AbstractScraper):
+    site_name = SourceSiteEnum.IMDB.value
+    host = 'https://www.imdb.com/title/'
+    data_class = Movie
+    form_class = MovieForm
+
+    regex = re.compile(r"(?<=https://www\.imdb\.com/title/)[a-zA-Z0-9]+")
+
+    def scrape(self, url):
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+        code = self.regex.findall(effective_url)[0]
+        s = TmdbMovieScraper()
+        s.scrape_imdb(code)
+        self.raw_data = s.raw_data
+        self.raw_img = s.raw_img
+        self.img_ext = s.img_ext
+        self.raw_data['source_site'] = self.site_name
+        self.raw_data['source_url'] = effective_url
+        return self.raw_data, self.raw_img
+
+        api_url = self.get_api_url(effective_url)
+        r = requests.get(api_url)
+        res_data = r.json()
+
+        if not res_data['type'] in ['Movie', 'TVSeries']:
+            raise ValueError("not movie/series item")
+
+        if res_data['type'] == 'Movie':
+            is_series = False
+        elif res_data['type'] == 'TVSeries':
+            is_series = True
+
+        title = res_data['title']
+        orig_title = res_data['originalTitle']
+        imdb_code = self.regex.findall(effective_url)[0]
+        director = []
+        for direct_dict in res_data['directorList']:
+            director.append(direct_dict['name'])
+        playwright = []
+        for writer_dict in res_data['writerList']:
+            playwright.append(writer_dict['name'])
+        actor = []
+        for actor_dict in res_data['actorList']:
+            actor.append(actor_dict['name'])
+        genre = res_data['genres'].split(', ')
+        area = res_data['countries'].split(', ')
+        language = res_data['languages'].split(', ')
+        year = int(res_data['year'])
+        duration = res_data['runtimeStr']
+        brief = res_data['plotLocal'] if res_data['plotLocal'] else res_data['plot']
+        if res_data['releaseDate']:
+            showtime = [{res_data['releaseDate']: "发布日期"}]
+        else:
+            showtime = None
+
+        other_info = {}
+        if res_data['contentRating']:
+            other_info['分级'] = res_data['contentRating']
+        if res_data['imDbRating']:
+            other_info['IMDb评分'] = res_data['imDbRating']
+        if res_data['metacriticRating']:
+            other_info['Metacritic评分'] = res_data['metacriticRating']
+        if res_data['awards']:
+            other_info['奖项'] = res_data['awards']
+
+        raw_img, ext = self.download_image(res_data['image'], url)
+
+        data = {
+            'title': title,
+            'orig_title': orig_title,
+            'other_title': None,
+            'imdb_code': imdb_code,
+            'director': director,
+            'playwright': playwright,
+            'actor': actor,
+            'genre': genre,
+            'showtime': showtime,
+            'site': None,
+            'area': area,
+            'language': language,
+            'year': year,
+            'duration': duration,
+            'season': None,
+            'episodes': None,
+            'single_episode_length': None,
+            'brief': brief,
+            'is_series': is_series,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        code = cls.regex.findall(raw_url)
+        if code:
+            return f"https://www.imdb.com/title/{code[0]}/"
+        else:
+            return None
+
+    @classmethod
+    def get_api_url(cls, url):
+        return f"https://imdb-api.com/zh/API/Title/{settings.IMDB_API_KEY}/{cls.regex.findall(url)[0]}/FullActor,"
diff --git a/common/scrapers/spotify.py b/common/scrapers/spotify.py
new file mode 100644
index 00000000..da891d69
--- /dev/null
+++ b/common/scrapers/spotify.py
@@ -0,0 +1,287 @@
+import requests
+import re
+import time
+from common.models import SourceSiteEnum
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from django.conf import settings
+from common.scraper import *
+from threading import Thread
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils import timezone
+
+
+spotify_token = None
+spotify_token_expire_time = time.time()
+
+
+class SpotifyTrackScraper(AbstractScraper):
+    site_name = SourceSiteEnum.SPOTIFY.value
+    host = 'https://open.spotify.com/track/'
+    data_class = Song
+    form_class = SongForm
+
+    regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+")
+
+    def scrape(self, url):
+        """
+        Request from API, not really scraping
+        """
+        global spotify_token, spotify_token_expire_time
+
+        if spotify_token is None or is_spotify_token_expired():
+            invoke_spotify_token()
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+
+        api_url = self.get_api_url(effective_url)
+        headers = {
+            'Authorization': f"Bearer {spotify_token}"
+        }
+        r = requests.get(api_url, headers=headers)
+        res_data = r.json()
+
+        artist = []
+        for artist_dict in res_data['artists']:
+            artist.append(artist_dict['name'])
+        if not artist:
+            artist = None
+
+        title = res_data['name']
+
+        release_date = parse_date(res_data['album']['release_date'])
+
+        duration = res_data['duration_ms']
+
+        if res_data['external_ids'].get('isrc'):
+            isrc = res_data['external_ids']['isrc']
+        else:
+            isrc = None
+
+        raw_img, ext = self.download_image(res_data['album']['images'][0]['url'], url)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': None,
+            'release_date': release_date,
+            'duration': duration,
+            'isrc': isrc,
+            'album': None,
+            'brief': None,
+            'other_info': None,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        code = cls.regex.findall(raw_url)
+        if code:
+            return f"https://open.spotify.com/track/{code[0]}"
+        else:
+            return None
+
+    @classmethod
+    def get_api_url(cls, url):
+        return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0]
+
+
+class SpotifyAlbumScraper(AbstractScraper):
+    site_name = SourceSiteEnum.SPOTIFY.value
+    # API URL
+    host = 'https://open.spotify.com/album/'
+    data_class = Album
+    form_class = AlbumForm
+
+    regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+")
+
+    def scrape(self, url):
+        """
+        Request from API, not really scraping
+        """
+        global spotify_token, spotify_token_expire_time
+
+        if spotify_token is None or is_spotify_token_expired():
+            invoke_spotify_token()
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+
+        api_url = self.get_api_url(effective_url)
+        headers = {
+            'Authorization': f"Bearer {spotify_token}"
+        }
+        r = requests.get(api_url, headers=headers)
+        res_data = r.json()
+
+        artist = []
+        for artist_dict in res_data['artists']:
+            artist.append(artist_dict['name'])
+
+        title = res_data['name']
+
+        genre = ', '.join(res_data['genres'])
+
+        company = []
+        for com in res_data['copyrights']:
+            company.append(com['text'])
+
+        duration = 0
+        track_list = []
+        track_urls = []
+        for track in res_data['tracks']['items']:
+            track_urls.append(track['external_urls']['spotify'])
+            duration += track['duration_ms']
+            if res_data['tracks']['items'][-1]['disc_number'] > 1:
+                # more than one disc
+                track_list.append(str(
+                    track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name'])
+            else:
+                track_list.append(str(track['track_number']) + '. ' + track['name'])
+        track_list = '\n'.join(track_list)
+
+        release_date = parse_date(res_data['release_date'])
+
+        other_info = {}
+        if res_data['external_ids'].get('upc'):
+            # bar code
+            other_info['UPC'] = res_data['external_ids']['upc']
+
+        raw_img, ext = self.download_image(res_data['images'][0]['url'], url)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': genre,
+            'track_list': track_list,
+            'release_date': release_date,
+            'duration': duration,
+            'company': company,
+            'brief': None,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+
+        # set tracks_data, used for adding tracks
+        self.track_urls = track_urls
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        code = cls.regex.findall(raw_url)
+        if code:
+            return f"https://open.spotify.com/album/{code[0]}"
+        else:
+            return None
+
+    # @classmethod
+    # def save(cls, request_user):
+    #     form = super().save(request_user)
+    #     task = Thread(
+    #         target=cls.add_tracks,
+    #         args=(form.instance, request_user),
+    #         daemon=True
+    #     )
+    #     task.start()
+    #     return form
+
+    @classmethod
+    def get_api_url(cls, url):
+        return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0]
+
+    @classmethod
+    def add_tracks(cls, album: Album, request_user):
+        to_be_updated_tracks = []
+        for track_url in cls.track_urls:
+            track = cls.get_track_or_none(track_url)
+            # seems lik if fire too many requests at the same time
+            # spotify would limit access
+            if track is None:
+                task = Thread(
+                    target=cls.scrape_and_save_track,
+                    args=(track_url, album, request_user),
+                    daemon=True
+                )
+                task.start()
+                task.join()
+            else:
+                to_be_updated_tracks.append(track)
+        cls.bulk_update_track_album(to_be_updated_tracks, album, request_user)
+
+    @classmethod
+    def get_track_or_none(cls, track_url: str):
+        try:
+            instance = Song.objects.get(source_url=track_url)
+            return instance
+        except ObjectDoesNotExist:
+            return None
+
+    @classmethod
+    def scrape_and_save_track(cls, url: str, album: Album, request_user):
+        data, img = SpotifyTrackScraper.scrape(url)
+        SpotifyTrackScraper.raw_data['album'] = album
+        SpotifyTrackScraper.save(request_user)
+
+    @classmethod
+    def bulk_update_track_album(cls, tracks, album, request_user):
+        for track in tracks:
+            track.last_editor = request_user
+            track.edited_time = timezone.now()
+            track.album = album
+        Song.objects.bulk_update(tracks, [
+            'last_editor',
+            'edited_time',
+            'album'
+        ])
+
+
+def get_spotify_token():
+    global spotify_token, spotify_token_expire_time
+    if spotify_token is None or is_spotify_token_expired():
+        invoke_spotify_token()
+    return spotify_token
+
+    
+def is_spotify_token_expired():
+    global spotify_token_expire_time
+    return True if spotify_token_expire_time <= time.time() else False
+
+
+def invoke_spotify_token():
+    global spotify_token, spotify_token_expire_time
+    r = requests.post(
+        "https://accounts.spotify.com/api/token",
+        data={
+            "grant_type": "client_credentials"
+        },
+        headers={
+            "Authorization": f"Basic {settings.SPOTIFY_CREDENTIAL}"
+        }
+    )
+    data = r.json()
+    if r.status_code == 401:
+        # token expired, try one more time
+        # this maybe caused by external operations,
+        # for example debugging using a http client
+        r = requests.post(
+            "https://accounts.spotify.com/api/token",
+            data={
+                "grant_type": "client_credentials"
+            },
+            headers={
+                "Authorization": f"Basic {settings.SPOTIFY_CREDENTIAL}"
+            }
+        )
+        data = r.json()
+    elif r.status_code != 200:
+        raise Exception(f"Request to spotify API fails. Reason: {r.reason}")
+    # minus 2 for execution time error
+    spotify_token_expire_time = int(data['expires_in']) + time.time() - 2
+    spotify_token = data['access_token']
diff --git a/common/scrapers/steam.py b/common/scrapers/steam.py
new file mode 100644
index 00000000..43f1c76b
--- /dev/null
+++ b/common/scrapers/steam.py
@@ -0,0 +1,92 @@
+import re
+from common.models import SourceSiteEnum
+from games.models import Game
+from games.forms import GameForm
+from common.scraper import *
+from common.scrapers.igdb import IgdbGameScraper
+
+
+class SteamGameScraper(AbstractScraper):
+    site_name = SourceSiteEnum.STEAM.value
+    host = 'store.steampowered.com'
+    data_class = Game
+    form_class = GameForm
+
+    regex = re.compile(r"https://store\.steampowered\.com/app/\d+")
+
+    def scrape(self, url):
+        m = self.regex.match(url)
+        if m:
+            effective_url = m[0]
+        else:
+            raise ValueError("not valid url")
+        try:
+            s = IgdbGameScraper()
+            s.scrape_steam(effective_url)
+            self.raw_data = s.raw_data
+            self.raw_img = s.raw_img
+            self.img_ext = s.img_ext
+            self.raw_data['source_site'] = self.site_name
+            self.raw_data['source_url'] = effective_url
+            # return self.raw_data, self.raw_img
+        except:
+            self.raw_img = None
+            self.raw_data = {}
+        headers = DEFAULT_REQUEST_HEADERS.copy()
+        headers['Host'] = self.host
+        headers['Cookie'] = "wants_mature_content=1; birthtime=754700401;"
+        content = self.download_page(url, headers)
+
+        title = content.xpath("//div[@class='apphub_AppName']/text()")[0]
+        developer = content.xpath("//div[@id='developers_list']/a/text()")
+        publisher = content.xpath("//div[@class='glance_ctn']//div[@class='dev_row'][2]//a/text()")
+        release_date = parse_date(
+            content.xpath(
+                "//div[@class='release_date']/div[@class='date']/text()")[0]
+        )
+
+        genre = content.xpath(
+            "//div[@class='details_block']/b[2]/following-sibling::a/text()")
+
+        platform = ['PC']
+
+        brief = content.xpath(
+            "//div[@class='game_description_snippet']/text()")[0].strip()
+
+        img_url = content.xpath(
+            "//img[@class='game_header_image_full']/@src"
+        )[0].replace("header.jpg", "library_600x900.jpg")
+        raw_img, img_ext = self.download_image(img_url, url)
+
+        # no 600x900 picture
+        if raw_img is None:
+            img_url = content.xpath("//img[@class='game_header_image_full']/@src")[0]
+            raw_img, img_ext = self.download_image(img_url, url)
+
+        if raw_img is not None:
+            self.raw_img = raw_img
+            self.img_ext = img_ext
+
+        data = {
+            'title': title if title else self.raw_data['title'],
+            'other_title': None,
+            'developer': developer if 'developer' not in self.raw_data else self.raw_data['developer'],
+            'publisher': publisher if 'publisher' not in self.raw_data else self.raw_data['publisher'],
+            'release_date': release_date if 'release_date' not in self.raw_data else self.raw_data['release_date'],
+            'genre': genre if 'genre' not in self.raw_data else self.raw_data['genre'],
+            'platform': platform if 'platform' not in self.raw_data else self.raw_data['platform'],
+            'brief': brief if brief else self.raw_data['brief'],
+            'other_info': None if 'other_info' not in self.raw_data else self.raw_data['other_info'],
+            'source_site': self.site_name,
+            'source_url': effective_url
+        }
+        self.raw_data = data
+        return self.raw_data, self.raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        m = cls.regex.match(raw_url)
+        if m:
+            return m[0]
+        else:
+            return None
diff --git a/common/scrapers/tmdb.py b/common/scrapers/tmdb.py
new file mode 100644
index 00000000..15072add
--- /dev/null
+++ b/common/scrapers/tmdb.py
@@ -0,0 +1,150 @@
+import requests
+import re
+from common.models import SourceSiteEnum
+from movies.models import Movie
+from movies.forms import MovieForm
+from django.conf import settings
+from common.scraper import *
+
+
+class TmdbMovieScraper(AbstractScraper):
+    site_name = SourceSiteEnum.TMDB.value
+    host = 'https://www.themoviedb.org/'
+    data_class = Movie
+    form_class = MovieForm
+    regex = re.compile(r"https://www\.themoviedb\.org/(movie|tv)/([a-zA-Z0-9]+)")
+    # http://api.themoviedb.org/3/genre/movie/list?api_key=&language=zh
+    # http://api.themoviedb.org/3/genre/tv/list?api_key=&language=zh
+    genre_map = {
+        'Sci-Fi & Fantasy': 'Sci-Fi',
+        'War & Politics':   'War',
+        '儿童':             'Kids',
+        '冒险':             'Adventure',
+        '剧情':             'Drama',
+        '动作':             'Action',
+        '动作冒险':         'Action',
+        '动画':             'Animation',
+        '历史':             'History',
+        '喜剧':             'Comedy',
+        '奇幻':             'Fantasy',
+        '家庭':             'Family',
+        '恐怖':             'Horror',
+        '悬疑':             'Mystery',
+        '惊悚':             'Thriller',
+        '战争':             'War',
+        '新闻':             'News',
+        '爱情':             'Romance',
+        '犯罪':             'Crime',
+        '电视电影':         'TV Movie',
+        '真人秀':           'Reality-TV',
+        '科幻':             'Sci-Fi',
+        '纪录':             'Documentary',
+        '肥皂剧':           'Soap',
+        '脱口秀':           'Talk-Show',
+        '西部':             'Western',
+        '音乐':             'Music',
+    }
+
+    def scrape_imdb(self, imdb_code):
+        api_url = f"https://api.themoviedb.org/3/find/{imdb_code}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&external_source=imdb_id"
+        r = requests.get(api_url)
+        res_data = r.json()
+        if 'movie_results' in res_data and len(res_data['movie_results']) > 0:
+            url = f"https://www.themoviedb.org/movie/{res_data['movie_results'][0]['id']}"
+        elif 'tv_results' in res_data and len(res_data['tv_results']) > 0:
+            url = f"https://www.themoviedb.org/tv/{res_data['tv_results'][0]['id']}"
+        else:
+            raise ValueError("Cannot find IMDb ID in TMDB")
+        return self.scrape(url)
+
+    def scrape(self, url):
+        m = self.regex.match(url)
+        if m:
+            effective_url = m[0]
+        else:
+            raise ValueError("not valid url")
+        effective_url = m[0]
+        is_series = m[1] == 'tv'
+        id = m[2]
+        if is_series:
+            api_url = f"https://api.themoviedb.org/3/tv/{id}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&append_to_response=external_ids,credits"
+        else:
+            api_url = f"https://api.themoviedb.org/3/movie/{id}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&append_to_response=external_ids,credits"
+        r = requests.get(api_url)
+        res_data = r.json()
+
+        if is_series:
+            title = res_data['name']
+            orig_title = res_data['original_name']
+            year = int(res_data['first_air_date'].split('-')[0]) if res_data['first_air_date'] else None
+            imdb_code = res_data['external_ids']['imdb_id']
+            showtime = [{res_data['first_air_date']: "首播日期"}] if res_data['first_air_date'] else None
+            duration = None
+        else:
+            title = res_data['title']
+            orig_title = res_data['original_title']
+            year = int(res_data['release_date'].split('-')[0]) if res_data['release_date'] else None
+            showtime = [{res_data['release_date']: "发布日期"}] if res_data['release_date'] else None
+            imdb_code = res_data['imdb_id']
+            duration = res_data['runtime'] if res_data['runtime'] else None # in minutes
+
+        genre = list(map(lambda x: self.genre_map[x['name']] if x['name'] in self.genre_map else 'Other', res_data['genres']))
+        language = list(map(lambda x: x['name'], res_data['spoken_languages']))
+        brief = res_data['overview']
+
+        if is_series:
+            director = list(map(lambda x: x['name'], res_data['created_by']))
+        else:
+            director = list(map(lambda x: x['name'], filter(lambda c: c['job'] == 'Director', res_data['credits']['crew'])))
+        playwright = list(map(lambda x: x['name'], filter(lambda c: c['job'] == 'Screenplay', res_data['credits']['crew'])))
+        actor = list(map(lambda x: x['name'], res_data['credits']['cast']))
+        area = []
+
+        other_info = {}
+        other_info['TMDB评分'] = res_data['vote_average']
+        # other_info['分级'] = res_data['contentRating']
+        # other_info['Metacritic评分'] = res_data['metacriticRating']
+        # other_info['奖项'] = res_data['awards']
+        other_info['TMDB_ID'] = id
+        if is_series:
+            other_info['Seasons'] = res_data['number_of_seasons']
+            other_info['Episodes'] = res_data['number_of_episodes']
+
+        img_url = ('https://image.tmdb.org/t/p/original/' + res_data['poster_path']) if res_data['poster_path'] is not None else None
+        # TODO: use GET /configuration to get base url
+        raw_img, ext = self.download_image(img_url, url)
+
+        data = {
+            'title': title,
+            'orig_title': orig_title,
+            'other_title': None,
+            'imdb_code': imdb_code,
+            'director': director,
+            'playwright': playwright,
+            'actor': actor,
+            'genre': genre,
+            'showtime': showtime,
+            'site': None,
+            'area': area,
+            'language': language,
+            'year': year,
+            'duration': duration,
+            'season': None,
+            'episodes': None,
+            'single_episode_length': None,
+            'brief': brief,
+            'is_series': is_series,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        m = cls.regex.match(raw_url)
+        if raw_url:
+            return m[0]
+        else:
+            return None
diff --git a/common/search/meilisearch.py b/common/search/meilisearch.py
new file mode 100644
index 00000000..d7a13a2f
--- /dev/null
+++ b/common/search/meilisearch.py
@@ -0,0 +1,183 @@
+import logging
+import meilisearch
+from django.conf import settings
+from django.db.models.signals import post_save, post_delete
+import types
+
+
+INDEX_NAME = 'items'
+SEARCHABLE_ATTRIBUTES = ['title', 'orig_title', 'other_title', 'subtitle', 'artist', 'author', 'translator', 'developer', 'director', 'actor', 'playwright', 'pub_house', 'company', 'publisher', 'isbn', 'imdb_code']
+INDEXABLE_DIRECT_TYPES = ['BigAutoField', 'BooleanField', 'CharField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'TextField', 'ArrayField']
+INDEXABLE_TIME_TYPES = ['DateTimeField']
+INDEXABLE_DICT_TYPES = ['JSONField']
+INDEXABLE_FLOAT_TYPES = ['DecimalField']
+# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
+SEARCH_PAGE_SIZE = 20
+
+
+logger = logging.getLogger(__name__)
+
+
+def item_post_save_handler(sender, instance, created, **kwargs):
+    if not created and settings.SEARCH_INDEX_NEW_ONLY:
+        return
+    Indexer.replace_item(instance)
+
+
+def item_post_delete_handler(sender, instance, **kwargs):
+    Indexer.delete_item(instance)
+
+
+def tag_post_save_handler(sender, instance, **kwargs):
+    pass
+
+
+def tag_post_delete_handler(sender, instance, **kwargs):
+    pass
+
+
+class Indexer:
+    class_map = {}
+    _instance = None
+
+    @classmethod
+    def instance(self):
+        if self._instance is None:
+            self._instance = meilisearch.Client(settings.MEILISEARCH_SERVER, settings.MEILISEARCH_KEY).index(INDEX_NAME)
+        return self._instance
+
+    @classmethod
+    def init(self):
+        meilisearch.Client(settings.MEILISEARCH_SERVER, settings.MEILISEARCH_KEY).create_index(INDEX_NAME, {'primaryKey': '_id'})
+        self.update_settings()
+
+    @classmethod
+    def update_settings(self):
+        self.instance().update_searchable_attributes(SEARCHABLE_ATTRIBUTES)
+        self.instance().update_filterable_attributes(['_class', 'tags', 'source_site'])
+        self.instance().update_settings({'displayedAttributes': ['_id', '_class', 'id', 'title', 'tags']})
+
+    @classmethod
+    def get_stats(self):
+        return self.instance().get_stats()
+
+    @classmethod
+    def busy(self):
+        return self.instance().get_stats()['isIndexing']
+
+    @classmethod
+    def update_model_indexable(self, model):
+        if settings.SEARCH_BACKEND is None:
+            return
+        self.class_map[model.__name__] = model
+        model.indexable_fields = ['tags']
+        model.indexable_fields_time = []
+        model.indexable_fields_dict = []
+        model.indexable_fields_float = []
+        for field in model._meta.get_fields():
+            type = field.get_internal_type()
+            if type in INDEXABLE_DIRECT_TYPES:
+                model.indexable_fields.append(field.name)
+            elif type in INDEXABLE_TIME_TYPES:
+                model.indexable_fields_time.append(field.name)
+            elif type in INDEXABLE_DICT_TYPES:
+                model.indexable_fields_dict.append(field.name)
+            elif type in INDEXABLE_FLOAT_TYPES:
+                model.indexable_fields_float.append(field.name)
+        post_save.connect(item_post_save_handler, sender=model)
+        post_delete.connect(item_post_delete_handler, sender=model)
+
+    @classmethod
+    def obj_to_dict(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        item = {
+            '_id': pk,
+            '_class': obj.__class__.__name__,
+            # 'id': obj.id
+        }
+        for field in obj.__class__.indexable_fields:
+            item[field] = getattr(obj, field)
+        for field in obj.__class__.indexable_fields_time:
+            item[field] = getattr(obj, field).timestamp()
+        for field in obj.__class__.indexable_fields_float:
+            item[field] = float(getattr(obj, field)) if getattr(obj, field) else None
+        for field in obj.__class__.indexable_fields_dict:
+            d = getattr(obj, field)
+            if d.__class__ is dict:
+                item.update(d)
+        item = {k: v for k, v in item.items() if v}
+        return item
+
+    @classmethod
+    def replace_item(self, obj):
+        try:
+            self.instance().add_documents([self.obj_to_dict(obj)])
+        except Exception as e:
+            logger.error(f"replace item error: \n{e}")
+
+    @classmethod
+    def replace_batch(self, objects):
+        try:
+            self.instance().update_documents(documents=objects)
+        except Exception as e:
+            logger.error(f"replace batch error: \n{e}")
+
+    @classmethod
+    def delete_item(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        try:
+            self.instance().delete_document(pk)
+        except Exception as e:
+            logger.error(f"delete item error: \n{e}")
+
+    @classmethod
+    def patch_item(self, obj, fields):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        data = {}
+        for f in fields:
+            data[f] = getattr(obj, f)
+        try:
+            self.instance().update_documents(documents=[data], primary_key=[pk])
+        except Exception as e:
+            logger.error(f"patch item error: \n{e}")
+
+    @classmethod
+    def search(self, q, page=1, category=None, tag=None, sort=None):
+        if category or tag:
+            f = []
+            if category == 'music':
+                f.append("(_class = 'Album' OR _class = 'Song')")
+            elif category:
+                f.append(f"_class = '{category}'")
+            if tag:
+                t = tag.replace("'", "\'")
+                f.append(f"tags = '{t}'")
+            filter = ' AND '.join(f)
+        else:
+            filter = None
+        options = {
+            'offset': (page - 1) * SEARCH_PAGE_SIZE,
+            'limit': SEARCH_PAGE_SIZE,
+            'filter': filter,
+            'facetsDistribution': ['_class'],
+            'sort': None
+        }
+        try:
+            r = self.instance().search(q, options)
+        except Exception as e:
+            logger.error(f"MeiliSearch error: \n{e}")
+            r = {'nbHits': 0, 'hits': []}
+        # print(r)
+        results = types.SimpleNamespace()
+        results.items = list([x for x in map(lambda i: self.item_to_obj(i), r['hits']) if x is not None])
+        results.num_pages = (r['nbHits'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
+        # print(results)
+        return results
+
+    @classmethod
+    def item_to_obj(self, item):
+        try:
+            return self.class_map[item['_class']].objects.get(id=item['id'])
+        except Exception as e:
+            logger.error(f"unable to load search result item from db:\n{item}")
+            return None
diff --git a/common/search/typesense.py b/common/search/typesense.py
new file mode 100644
index 00000000..523fb370
--- /dev/null
+++ b/common/search/typesense.py
@@ -0,0 +1,215 @@
+import logging
+import typesense
+from django.conf import settings
+from django.db.models.signals import post_save, post_delete
+
+
+INDEX_NAME = 'items'
+SEARCHABLE_ATTRIBUTES = ['title', 'orig_title', 'other_title', 'subtitle', 'artist', 'author', 'translator',
+                         'developer', 'director', 'actor', 'playwright', 'pub_house', 'company', 'publisher', 'isbn', 'imdb_code']
+FILTERABLE_ATTRIBUTES = ['_class', 'tags', 'source_site']
+INDEXABLE_DIRECT_TYPES = ['BigAutoField', 'BooleanField', 'CharField',
+                          'PositiveIntegerField', 'PositiveSmallIntegerField', 'TextField', 'ArrayField']
+INDEXABLE_TIME_TYPES = ['DateTimeField']
+INDEXABLE_DICT_TYPES = ['JSONField']
+INDEXABLE_FLOAT_TYPES = ['DecimalField']
+SORTING_ATTRIBUTE = None
+# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
+SEARCH_PAGE_SIZE = 20
+
+
+logger = logging.getLogger(__name__)
+
+
+def item_post_save_handler(sender, instance, created, **kwargs):
+    if not created and settings.SEARCH_INDEX_NEW_ONLY:
+        return
+    Indexer.replace_item(instance)
+
+
+def item_post_delete_handler(sender, instance, **kwargs):
+    Indexer.delete_item(instance)
+
+
+def tag_post_save_handler(sender, instance, **kwargs):
+    pass
+
+
+def tag_post_delete_handler(sender, instance, **kwargs):
+    pass
+
+
+class Indexer:
+    class_map = {}
+    _instance = None
+
+    @classmethod
+    def instance(self):
+        if self._instance is None:
+            self._instance = typesense.Client(settings.TYPESENSE_CONNECTION)
+        return self._instance
+
+    @classmethod
+    def init(self):
+        # self.instance().collections[INDEX_NAME].delete()
+        # fields = [
+        #     {"name": "_class", "type": "string", "facet": True},
+        #     {"name": "source_site", "type": "string", "facet": True},
+        #     {"name": ".*", "type": "auto", "locale": "zh"},
+        # ]
+        # use dumb schema below before typesense fix a bug
+        fields = [
+            {'name': 'id', 'type': 'string'},
+            {'name': '_id', 'type': 'int64'},
+            {'name': '_class', 'type': 'string', "facet": True},
+            {'name': 'source_site', 'type': 'string', "facet": True},
+            {'name': 'isbn', 'optional': True, 'type': 'string'},
+            {'name': 'imdb_code', 'optional': True, 'type': 'string'},
+            {'name': 'author', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'orig_title', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'pub_house', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'title', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'translator', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'subtitle', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'artist', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'company', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'developer', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'other_title', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'publisher', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'actor', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'director', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'playwright', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'tags', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': '.*', 'optional': True, 'locale': 'zh', 'type': 'auto'},
+        ]
+
+        self.instance().collections.create({
+            "name": INDEX_NAME,
+            "fields": fields
+        })
+
+    @classmethod
+    def update_settings(self):
+        # https://github.com/typesense/typesense/issues/96
+        print('not supported by typesense yet')
+        pass
+
+    @classmethod
+    def get_stats(self):
+        return self.instance().collections[INDEX_NAME].retrieve()
+
+    @classmethod
+    def busy(self):
+        return False
+
+    @classmethod
+    def update_model_indexable(self, model):
+        if settings.SEARCH_BACKEND is None:
+            return
+        self.class_map[model.__name__] = model
+        model.indexable_fields = ['tags']
+        model.indexable_fields_time = []
+        model.indexable_fields_dict = []
+        model.indexable_fields_float = []
+        for field in model._meta.get_fields():
+            type = field.get_internal_type()
+            if type in INDEXABLE_DIRECT_TYPES:
+                model.indexable_fields.append(field.name)
+            elif type in INDEXABLE_TIME_TYPES:
+                model.indexable_fields_time.append(field.name)
+            elif type in INDEXABLE_DICT_TYPES:
+                model.indexable_fields_dict.append(field.name)
+            elif type in INDEXABLE_FLOAT_TYPES:
+                model.indexable_fields_float.append(field.name)
+        post_save.connect(item_post_save_handler, sender=model)
+        post_delete.connect(item_post_delete_handler, sender=model)
+
+    @classmethod
+    def obj_to_dict(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        item = {
+            '_class': obj.__class__.__name__,
+        }
+        for field in obj.__class__.indexable_fields:
+            item[field] = getattr(obj, field)
+        for field in obj.__class__.indexable_fields_time:
+            item[field] = getattr(obj, field).timestamp()
+        for field in obj.__class__.indexable_fields_float:
+            item[field] = float(getattr(obj, field)) if getattr(
+                obj, field) else None
+        for field in obj.__class__.indexable_fields_dict:
+            d = getattr(obj, field)
+            if d.__class__ is dict:
+                item.update(d)
+        item = {k: v for k, v in item.items() if v and (
+            k in SEARCHABLE_ATTRIBUTES or k in FILTERABLE_ATTRIBUTES or k == 'id')}
+        item['_id'] = item['id']
+        # typesense requires primary key to be named 'id', type string
+        item['id'] = pk
+        return item
+
+    @classmethod
+    def replace_item(self, obj):
+        try:
+            self.instance().collections[INDEX_NAME].documents.upsert(self.obj_to_dict(obj), {
+                'dirty_values': 'coerce_or_drop'
+            })
+        except Exception as e:
+            logger.error(f"replace item error: \n{e}")
+
+    @classmethod
+    def replace_batch(self, objects):
+        try:
+            self.instance().collections[INDEX_NAME].documents.import_(
+                objects, {'action': 'upsert'})
+        except Exception as e:
+            logger.error(f"replace batch error: \n{e}")
+
+    @classmethod
+    def delete_item(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        try:
+            self.instance().collections[INDEX_NAME].documents[pk].delete()
+        except Exception as e:
+            logger.error(f"delete item error: \n{e}")
+
+    @classmethod
+    def search(self, q, page=1, category=None, tag=None, sort=None):
+        f = []
+        if category == 'music':
+            f.append('_class:= [Album, Song]')
+        elif category:
+            f.append('_class:= ' + category)
+        else:
+            f.append('')
+        if tag:
+            f.append(f"tags:= '{tag}'")
+        filter = ' && '.join(f)
+        options = {
+            'q': q,
+            'page': page,
+            'per_page': SEARCH_PAGE_SIZE,
+            'query_by': ','.join(SEARCHABLE_ATTRIBUTES),
+            'filter_by': filter,
+            # 'facetsDistribution': ['_class'],
+            # 'sort_by': None,
+        }
+        # print(q)
+        r = self.instance().collections[INDEX_NAME].documents.search(options)
+        # print(r)
+        import types
+        results = types.SimpleNamespace()
+        results.items = list([x for x in map(lambda i: self.item_to_obj(
+            i['document']), r['hits']) if x is not None])
+        results.num_pages = (
+            r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
+        # print(results)
+        return results
+
+    @classmethod
+    def item_to_obj(self, item):
+        try:
+            return self.class_map[item['_class']].objects.get(id=item['_id'])
+        except Exception as e:
+            logger.error(f"unable to load search result item from db:\n{item}")
+            return None
diff --git a/common/searcher.py b/common/searcher.py
new file mode 100644
index 00000000..48c43af9
--- /dev/null
+++ b/common/searcher.py
@@ -0,0 +1,209 @@
+from urllib.parse import quote_plus
+from enum import Enum
+from common.models import SourceSiteEnum
+from django.conf import settings
+from common.scrapers.goodreads import GoodreadsScraper
+from common.scrapers.spotify import get_spotify_token
+import requests
+from lxml import html
+import logging
+
+SEARCH_PAGE_SIZE = 5  # not all apis support page size
+logger = logging.getLogger(__name__)
+
+
+class Category(Enum):
+    Book = '书籍'
+    Movie = '电影'
+    Music = '音乐'
+    Game = '游戏'
+    TV = '剧集'
+
+
+class SearchResultItem:
+    def __init__(self, category, source_site, source_url, title, subtitle, brief, cover_url):
+        self.category = category
+        self.source_site = source_site
+        self.source_url = source_url
+        self.title = title
+        self.subtitle = subtitle
+        self.brief = brief
+        self.cover_url = cover_url
+
+    @property
+    def verbose_category_name(self):
+        return self.category.value
+
+    @property
+    def link(self):
+        return f"/search?q={quote_plus(self.source_url)}"
+
+    @property
+    def scraped(self):
+        return False
+
+
+class ProxiedRequest:
+    @classmethod
+    def get(cls, url):
+        u = f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={quote_plus(url)}'
+        return requests.get(u, timeout=10)
+
+
+class Goodreads:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            search_url = f'https://www.goodreads.com/search?page={page}&q={quote_plus(q)}'
+            r = requests.get(search_url)
+            if r.url.startswith('https://www.goodreads.com/book/show/'):
+                # Goodreads will 302 if only one result matches ISBN
+                data, img = GoodreadsScraper.scrape(r.url, r)
+                subtitle = f"{data['pub_year']} {', '.join(data['author'])} {', '.join(data['translator'] if data['translator'] else [])}"
+                results.append(SearchResultItem(Category.Book, SourceSiteEnum.GOODREADS,
+                                                data['source_url'], data['title'], subtitle,
+                                                data['brief'], data['cover_url']))
+            else:
+                h = html.fromstring(r.content.decode('utf-8'))
+                for c in h.xpath('//tr[@itemtype="http://schema.org/Book"]'):
+                    el_cover = c.xpath('.//img[@class="bookCover"]/@src')
+                    cover = el_cover[0] if el_cover else None
+                    el_title = c.xpath('.//a[@class="bookTitle"]//text()')
+                    title = ''.join(el_title).strip() if el_title else None
+                    el_url = c.xpath('.//a[@class="bookTitle"]/@href')
+                    url = 'https://www.goodreads.com' + \
+                        el_url[0] if el_url else None
+                    el_authors = c.xpath('.//a[@class="authorName"]//text()')
+                    subtitle = ', '.join(el_authors) if el_authors else None
+                    results.append(SearchResultItem(
+                        Category.Book, SourceSiteEnum.GOODREADS, url, title, subtitle, '', cover))
+        except Exception as e:
+            logger.error(f"Goodreads search '{q}' error: {e}")
+        return results
+
+
+class GoogleBooks:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            api_url = f'https://www.googleapis.com/books/v1/volumes?country=us&q={quote_plus(q)}&startIndex={SEARCH_PAGE_SIZE*(page-1)}&maxResults={SEARCH_PAGE_SIZE}&maxAllowedMaturityRating=MATURE'
+            j = requests.get(api_url).json()
+            if 'items' in j:
+                for b in j['items']:
+                    if 'title' not in b['volumeInfo']:
+                        continue
+                    title = b['volumeInfo']['title']
+                    subtitle = ''
+                    if 'publishedDate' in b['volumeInfo']:
+                        subtitle += b['volumeInfo']['publishedDate'] + ' '
+                    if 'authors' in b['volumeInfo']:
+                        subtitle += ', '.join(b['volumeInfo']['authors'])
+                    if 'description' in b['volumeInfo']:
+                        brief = b['volumeInfo']['description']
+                    elif 'textSnippet' in b['volumeInfo']:
+                        brief = b["volumeInfo"]["textSnippet"]["searchInfo"]
+                    else:
+                        brief = ''
+                    category = Category.Book
+                    # b['volumeInfo']['infoLink'].replace('http:', 'https:')
+                    url = 'https://books.google.com/books?id=' + b['id']
+                    cover = b['volumeInfo']['imageLinks']['thumbnail'] if 'imageLinks' in b['volumeInfo'] else None
+                    results.append(SearchResultItem(
+                        category, SourceSiteEnum.GOOGLEBOOKS, url, title, subtitle, brief, cover))
+        except Exception as e:
+            logger.error(f"GoogleBooks search '{q}' error: {e}")
+        return results
+
+
+class TheMovieDatabase:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            api_url = f'https://api.themoviedb.org/3/search/multi?query={quote_plus(q)}&page={page}&api_key={settings.TMDB_API3_KEY}&language=zh-CN&include_adult=true'
+            j = requests.get(api_url).json()
+            for m in j['results']:
+                if m['media_type'] in ['tv', 'movie']:
+                    url = f"https://www.themoviedb.org/{m['media_type']}/{m['id']}"
+                    if m['media_type'] == 'tv':
+                        cat = Category.TV
+                        title = m['name']
+                        subtitle = f"{m.get('first_air_date')} {m.get('original_name')}"
+                    else:
+                        cat = Category.Movie
+                        title = m['title']
+                        subtitle = f"{m.get('release_date')} {m.get('original_name')}"
+                    cover = f"https://image.tmdb.org/t/p/w500/{m.get('poster_path')}"
+                    results.append(SearchResultItem(
+                        cat, SourceSiteEnum.TMDB, url, title, subtitle, m.get('overview'), cover))
+        except Exception as e:
+            logger.error(f"TMDb search '{q}' error: {e}")
+        return results
+
+
+class Spotify:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            api_url = f"https://api.spotify.com/v1/search?q={q}&type=album&limit={SEARCH_PAGE_SIZE}&offset={page*SEARCH_PAGE_SIZE}"
+            headers = {
+                'Authorization': f"Bearer {get_spotify_token()}"
+            }
+            j = requests.get(api_url, headers=headers).json()
+            for a in j['albums']['items']:
+                title = a['name']
+                subtitle = a['release_date']
+                for artist in a['artists']:
+                    subtitle += ' ' + artist['name']
+                url = a['external_urls']['spotify']
+                cover = a['images'][0]['url']
+                results.append(SearchResultItem(
+                    Category.Music, SourceSiteEnum.SPOTIFY, url, title, subtitle, '', cover))
+        except Exception as e:
+            logger.error(f"Spotify search '{q}' error: {e}")
+        return results
+
+
+class Bandcamp:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            search_url = f'https://bandcamp.com/search?from=results&item_type=a&page={page}&q={quote_plus(q)}'
+            r = requests.get(search_url)
+            h = html.fromstring(r.content.decode('utf-8'))
+            for c in h.xpath('//li[@class="searchresult data-search"]'):
+                el_cover = c.xpath('.//div[@class="art"]/img/@src')
+                cover = el_cover[0] if el_cover else None
+                el_title = c.xpath('.//div[@class="heading"]//text()')
+                title = ''.join(el_title).strip() if el_title else None
+                el_url = c.xpath('..//div[@class="itemurl"]/a/@href')
+                url = el_url[0] if el_url else None
+                el_authors = c.xpath('.//div[@class="subhead"]//text()')
+                subtitle = ', '.join(el_authors) if el_authors else None
+                results.append(SearchResultItem(Category.Music, SourceSiteEnum.BANDCAMP, url, title, subtitle, '', cover))
+        except Exception as e:
+            logger.error(f"Goodreads search '{q}' error: {e}")
+        return results
+
+
+class ExternalSources:
+    @classmethod
+    def search(self, c, q, page=1):
+        if not q:
+            return []
+        results = []
+        if c == '' or c is None:
+            c = 'all'
+        if c == 'all' or c == 'movie':
+            results.extend(TheMovieDatabase.search(q, page))
+        if c == 'all' or c == 'book':
+            results.extend(GoogleBooks.search(q, page))
+            results.extend(Goodreads.search(q, page))
+        if c == 'all' or c == 'music':
+            results.extend(Spotify.search(q, page))
+            results.extend(Bandcamp.search(q, page))
+        return results
diff --git a/common/static/css/boofilsic.css b/common/static/css/boofilsic.css
index 514f1aba..b93d83b6 100644
--- a/common/static/css/boofilsic.css
+++ b/common/static/css/boofilsic.css
@@ -270,16 +270,13 @@ h6 {
 
 img {
   max-width: 100%;
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
 }
 
 img.emoji {
   height: 14px;
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
-  -o-object-fit: contain;
-     object-fit: contain;
+  box-sizing: border-box;
+  object-fit: contain;
   position: relative;
   top: 3px;
 }
@@ -315,13 +312,11 @@ img.emoji--large {
 *,
 *:after,
 *:before {
-  -webkit-box-sizing: inherit;
-          box-sizing: inherit;
+  box-sizing: inherit;
 }
 
 html {
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
+  box-sizing: border-box;
   height: 100%;
 }
 
@@ -379,16 +374,12 @@ input[type='time'],
 input[type='color'],
 textarea,
 select {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
+  appearance: none;
   background-color: transparent;
   border: 0.1rem solid #ccc;
   border-radius: .4rem;
-  -webkit-box-shadow: none;
-          box-shadow: none;
-  -webkit-box-sizing: inherit;
-          box-sizing: inherit;
+  box-shadow: none;
+  box-sizing: inherit;
   padding: .6rem 1.0rem;
 }
 
@@ -408,51 +399,6 @@ select:focus {
   outline: 0;
 }
 
-input[type='email']::-webkit-input-placeholder,
-input[type='number']::-webkit-input-placeholder,
-input[type='password']::-webkit-input-placeholder,
-input[type='search']::-webkit-input-placeholder,
-input[type='tel']::-webkit-input-placeholder,
-input[type='text']::-webkit-input-placeholder,
-input[type='url']::-webkit-input-placeholder,
-input[type='date']::-webkit-input-placeholder,
-input[type='time']::-webkit-input-placeholder,
-input[type='color']::-webkit-input-placeholder,
-textarea::-webkit-input-placeholder,
-select::-webkit-input-placeholder {
-  color: #ccc;
-}
-
-input[type='email']:-ms-input-placeholder,
-input[type='number']:-ms-input-placeholder,
-input[type='password']:-ms-input-placeholder,
-input[type='search']:-ms-input-placeholder,
-input[type='tel']:-ms-input-placeholder,
-input[type='text']:-ms-input-placeholder,
-input[type='url']:-ms-input-placeholder,
-input[type='date']:-ms-input-placeholder,
-input[type='time']:-ms-input-placeholder,
-input[type='color']:-ms-input-placeholder,
-textarea:-ms-input-placeholder,
-select:-ms-input-placeholder {
-  color: #ccc;
-}
-
-input[type='email']::-ms-input-placeholder,
-input[type='number']::-ms-input-placeholder,
-input[type='password']::-ms-input-placeholder,
-input[type='search']::-ms-input-placeholder,
-input[type='tel']::-ms-input-placeholder,
-input[type='text']::-ms-input-placeholder,
-input[type='url']::-ms-input-placeholder,
-input[type='date']::-ms-input-placeholder,
-input[type='time']::-ms-input-placeholder,
-input[type='color']::-ms-input-placeholder,
-textarea::-ms-input-placeholder,
-select::-ms-input-placeholder {
-  color: #ccc;
-}
-
 input[type='email']::placeholder,
 input[type='number']::placeholder,
 input[type='password']::placeholder,
@@ -468,11 +414,6 @@ select::placeholder {
   color: #ccc;
 }
 
-::-moz-selection {
-  color: white;
-  background-color: #00a1cc;
-}
-
 ::selection {
   color: white;
   background-color: #00a1cc;
@@ -480,29 +421,21 @@ select::placeholder {
 
 .navbar {
   background-color: #f7f7f7;
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
+  box-sizing: border-box;
   padding: 10px 0;
   margin-bottom: 50px;
   border-bottom: #ccc 0.5px solid;
 }
 
 .navbar .navbar__wrapper {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: justify;
-      -ms-flex-pack: justify;
-          justify-content: space-between;
-  -webkit-box-align: center;
-      -ms-flex-align: center;
-          align-items: center;
+  justify-content: space-between;
+  align-items: center;
   position: relative;
 }
 
 .navbar .navbar__logo {
-  -ms-flex-preferred-size: 100px;
-      flex-basis: 100px;
+  flex-basis: 100px;
 }
 
 .navbar .navbar__logo-link {
@@ -511,11 +444,8 @@ select::placeholder {
 
 .navbar .navbar__link-list {
   margin: 0;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -ms-flex-pack: distribute;
-      justify-content: space-around;
+  justify-content: space-around;
 }
 
 .navbar .navbar__link {
@@ -533,12 +463,8 @@ select::placeholder {
 
 .navbar .navbar__search-box {
   margin: 0 12% 0 15px;
-  display: -webkit-inline-box;
-  display: -ms-inline-flexbox;
   display: inline-flex;
-  -webkit-box-flex: 1;
-      -ms-flex: 1;
-          flex: 1;
+  flex: 1;
 }
 
 .navbar .navbar__search-box > input[type="search"] {
@@ -556,9 +482,7 @@ select::placeholder {
   padding: 0;
   padding-left: 10px;
   color: #606c76;
-  -webkit-appearance: auto;
-     -moz-appearance: auto;
-          appearance: auto;
+  appearance: auto;
   background-color: white;
   height: 32px;
   width: 80px;
@@ -596,7 +520,6 @@ select::placeholder {
   .navbar .navbar__link-list {
     margin-top: 7px;
     max-height: 0;
-    -webkit-transition: max-height 0.6s ease-out;
     transition: max-height 0.6s ease-out;
     overflow: hidden;
   }
@@ -605,12 +528,10 @@ select::placeholder {
     position: absolute;
     right: 5px;
     top: 3px;
-    -webkit-transform: scale(0.7);
-            transform: scale(0.7);
+    transform: scale(0.7);
   }
   .navbar .navbar__dropdown-btn:hover + .navbar__link-list {
     max-height: 500px;
-    -webkit-transition: max-height 0.6s ease-in;
     transition: max-height 0.6s ease-in;
   }
   .navbar .navbar__search-box {
@@ -654,15 +575,9 @@ select::placeholder {
   width: 26%;
   float: right;
   position: relative;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-orient: vertical;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: column;
-          flex-direction: column;
-  -ms-flex-pack: distribute;
-      justify-content: space-around;
+  flex-direction: column;
+  justify-content: space-around;
 }
 
 .grid::after {
@@ -673,10 +588,7 @@ select::placeholder {
 
 @media (max-width: 575.98px) {
   .grid .grid__aside {
-    -webkit-box-orient: vertical !important;
-    -webkit-box-direction: normal !important;
-        -ms-flex-direction: column !important;
-            flex-direction: column !important;
+    flex-direction: column !important;
   }
 }
 
@@ -688,28 +600,19 @@ select::placeholder {
   .grid .grid__aside {
     width: 100%;
     float: none;
-    -webkit-box-orient: horizontal;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: row;
-            flex-direction: row;
+    flex-direction: row;
   }
   .grid .grid__aside--tablet-column {
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
   }
   .grid--reverse-order {
-    -webkit-transform: scaleY(-1);
-            transform: scaleY(-1);
+    transform: scaleY(-1);
   }
   .grid .grid__main--reverse-order {
-    -webkit-transform: scaleY(-1);
-            transform: scaleY(-1);
+    transform: scaleY(-1);
   }
   .grid .grid__aside--reverse-order {
-    -webkit-transform: scaleY(-1);
-            transform: scaleY(-1);
+    transform: scaleY(-1);
   }
 }
 
@@ -778,8 +681,7 @@ select::placeholder {
   margin-bottom: 4px !important;
   position: absolute !important;
   left: 50%;
-  -webkit-transform: translateX(-50%);
-          transform: translateX(-50%);
+  transform: translateX(-50%);
   bottom: 0;
   width: 100%;
 }
@@ -839,17 +741,14 @@ select::placeholder {
   display: inline-block;
   position: relative;
   left: 50%;
-  -webkit-transform: translateX(-50%) scale(0.4);
-          transform: translateX(-50%) scale(0.4);
+  transform: translateX(-50%) scale(0.4);
   width: 80px;
   height: 80px;
 }
 
 .spinner div {
-  -webkit-transform-origin: 40px 40px;
-          transform-origin: 40px 40px;
-  -webkit-animation: spinner 1.2s linear infinite;
-          animation: spinner 1.2s linear infinite;
+  transform-origin: 40px 40px;
+  animation: spinner 1.2s linear infinite;
 }
 
 .spinner div::after {
@@ -865,96 +764,63 @@ select::placeholder {
 }
 
 .spinner div:nth-child(1) {
-  -webkit-transform: rotate(0deg);
-          transform: rotate(0deg);
-  -webkit-animation-delay: -1.1s;
-          animation-delay: -1.1s;
+  transform: rotate(0deg);
+  animation-delay: -1.1s;
 }
 
 .spinner div:nth-child(2) {
-  -webkit-transform: rotate(30deg);
-          transform: rotate(30deg);
-  -webkit-animation-delay: -1s;
-          animation-delay: -1s;
+  transform: rotate(30deg);
+  animation-delay: -1s;
 }
 
 .spinner div:nth-child(3) {
-  -webkit-transform: rotate(60deg);
-          transform: rotate(60deg);
-  -webkit-animation-delay: -0.9s;
-          animation-delay: -0.9s;
+  transform: rotate(60deg);
+  animation-delay: -0.9s;
 }
 
 .spinner div:nth-child(4) {
-  -webkit-transform: rotate(90deg);
-          transform: rotate(90deg);
-  -webkit-animation-delay: -0.8s;
-          animation-delay: -0.8s;
+  transform: rotate(90deg);
+  animation-delay: -0.8s;
 }
 
 .spinner div:nth-child(5) {
-  -webkit-transform: rotate(120deg);
-          transform: rotate(120deg);
-  -webkit-animation-delay: -0.7s;
-          animation-delay: -0.7s;
+  transform: rotate(120deg);
+  animation-delay: -0.7s;
 }
 
 .spinner div:nth-child(6) {
-  -webkit-transform: rotate(150deg);
-          transform: rotate(150deg);
-  -webkit-animation-delay: -0.6s;
-          animation-delay: -0.6s;
+  transform: rotate(150deg);
+  animation-delay: -0.6s;
 }
 
 .spinner div:nth-child(7) {
-  -webkit-transform: rotate(180deg);
-          transform: rotate(180deg);
-  -webkit-animation-delay: -0.5s;
-          animation-delay: -0.5s;
+  transform: rotate(180deg);
+  animation-delay: -0.5s;
 }
 
 .spinner div:nth-child(8) {
-  -webkit-transform: rotate(210deg);
-          transform: rotate(210deg);
-  -webkit-animation-delay: -0.4s;
-          animation-delay: -0.4s;
+  transform: rotate(210deg);
+  animation-delay: -0.4s;
 }
 
 .spinner div:nth-child(9) {
-  -webkit-transform: rotate(240deg);
-          transform: rotate(240deg);
-  -webkit-animation-delay: -0.3s;
-          animation-delay: -0.3s;
+  transform: rotate(240deg);
+  animation-delay: -0.3s;
 }
 
 .spinner div:nth-child(10) {
-  -webkit-transform: rotate(270deg);
-          transform: rotate(270deg);
-  -webkit-animation-delay: -0.2s;
-          animation-delay: -0.2s;
+  transform: rotate(270deg);
+  animation-delay: -0.2s;
 }
 
 .spinner div:nth-child(11) {
-  -webkit-transform: rotate(300deg);
-          transform: rotate(300deg);
-  -webkit-animation-delay: -0.1s;
-          animation-delay: -0.1s;
+  transform: rotate(300deg);
+  animation-delay: -0.1s;
 }
 
 .spinner div:nth-child(12) {
-  -webkit-transform: rotate(330deg);
-          transform: rotate(330deg);
-  -webkit-animation-delay: 0s;
-          animation-delay: 0s;
-}
-
-@-webkit-keyframes spinner {
-  0% {
-    opacity: 1;
-  }
-  100% {
-    opacity: 0;
-  }
+  transform: rotate(330deg);
+  animation-delay: 0s;
 }
 
 @keyframes spinner {
@@ -969,8 +835,7 @@ select::placeholder {
 .bg-mask {
   background-color: black;
   z-index: 1;
-  -webkit-filter: opacity(20%);
-          filter: opacity(20%);
+  filter: opacity(20%);
   position: fixed;
   width: 100%;
   height: 100%;
@@ -986,8 +851,7 @@ select::placeholder {
   width: 500px;
   top: 50%;
   left: 50%;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   background-color: #f7f7f7;
   padding: 20px 20px 10px 20px;
   color: #606c76;
@@ -1107,8 +971,7 @@ select::placeholder {
   width: 500px;
   top: 50%;
   left: 50%;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   background-color: #f7f7f7;
   padding: 20px 20px 10px 20px;
   color: #606c76;
@@ -1146,8 +1009,7 @@ select::placeholder {
   width: 500px;
   top: 50%;
   left: 50%;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   background-color: #f7f7f7;
   padding: 20px 20px 10px 20px;
   color: #606c76;
@@ -1196,8 +1058,46 @@ select::placeholder {
   word-break: break-all;
 }
 
+.add-to-list-modal {
+  z-index: 2;
+  display: none;
+  position: fixed;
+  width: 500px;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background-color: #f7f7f7;
+  padding: 20px 20px 10px 20px;
+  color: #606c76;
+}
+
+.add-to-list-modal .add-to-list-modal__head {
+  margin-bottom: 20px;
+}
+
+.add-to-list-modal .add-to-list-modal__head::after {
+  content: ' ';
+  clear: both;
+  display: table;
+}
+
+.add-to-list-modal .add-to-list-modal__title {
+  font-weight: bold;
+  font-size: 1.2em;
+  float: left;
+}
+
+.add-to-list-modal .add-to-list-modal__close-button {
+  float: right;
+  cursor: pointer;
+}
+
+.add-to-list-modal .add-to-list-modal__confirm-button {
+  float: right;
+}
+
 @media (max-width: 575.98px) {
-  .mark-modal, .confirm-modal, .announcement-modal {
+  .mark-modal, .confirm-modal, .announcement-modal .add-to-list-modal {
     width: 100%;
   }
 }
@@ -1246,6 +1146,13 @@ select::placeholder {
   font-weight: bold;
 }
 
+.source-label.source-label__igdb {
+  background-color: #323A44;
+  color: #DFE1E2;
+  border: none;
+  font-weight: bold;
+}
+
 .source-label.source-label__steam {
   background: linear-gradient(30deg, #1387b8, #111d2e);
   color: white;
@@ -1261,6 +1168,37 @@ select::placeholder {
   font-weight: 600;
 }
 
+.source-label.source-label__goodreads {
+  background: #F4F1EA;
+  color: #372213;
+  font-weight: lighter;
+}
+
+.source-label.source-label__tmdb {
+  background: linear-gradient(90deg, #91CCA3, #1FB4E2);
+  color: white;
+  border: none;
+  font-weight: lighter;
+  padding-top: 2px;
+}
+
+.source-label.source-label__googlebooks {
+  color: white;
+  background-color: #4285F4;
+  border-color: #4285F4;
+}
+
+.source-label.source-label__bandcamp {
+  color: white;
+  background-color: #28A0C1;
+  display: inline-block;
+}
+
+.source-label.source-label__bandcamp span {
+  display: inline-block;
+  margin: 0 4px;
+}
+
 .main-section-wrapper {
   padding: 32px 48px 32px 36px;
   background-color: #f7f7f7;
@@ -1276,8 +1214,6 @@ select::placeholder {
 }
 
 .entity-list .entity-list__entity {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   margin-bottom: 36px;
 }
@@ -1289,8 +1225,7 @@ select::placeholder {
 }
 
 .entity-list .entity-list__entity-img {
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
   min-width: 130px;
   max-width: 130px;
 }
@@ -1368,15 +1303,12 @@ select::placeholder {
 .entity-detail .entity-detail__img {
   height: 210px;
   float: left;
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
   max-width: 150px;
-  -o-object-position: top;
-     object-position: top;
+  object-position: top;
 }
 
 .entity-detail .entity-detail__img-origin {
-  cursor: -webkit-zoom-in;
   cursor: zoom-in;
 }
 
@@ -1451,14 +1383,10 @@ select::placeholder {
 }
 
 .entity-desc .entity-desc__unfold-button {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   color: #00a1cc;
   background-color: transparent;
-  -webkit-box-pack: center;
-      -ms-flex-pack: center;
-          justify-content: center;
+  justify-content: center;
   text-align: center;
 }
 
@@ -1597,20 +1525,14 @@ select::placeholder {
 }
 
 .entity-sort .entity-sort__entity-list {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: start;
-      -ms-flex-pack: start;
-          justify-content: flex-start;
-  -ms-flex-wrap: wrap;
-      flex-wrap: wrap;
+  justify-content: flex-start;
+  flex-wrap: wrap;
 }
 
 .entity-sort .entity-sort__entity {
   padding: 0 10px;
-  -ms-flex-preferred-size: 20%;
-      flex-basis: 20%;
+  flex-basis: 20%;
   text-align: center;
   display: inline-block;
   color: #606c76;
@@ -1658,12 +1580,8 @@ select::placeholder {
 }
 
 .entity-sort-control {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: end;
-      -ms-flex-pack: end;
-          justify-content: flex-end;
+  justify-content: flex-end;
 }
 
 .entity-sort-control__button {
@@ -1693,12 +1611,8 @@ select::placeholder {
 }
 
 .related-user-list .related-user-list__user {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: start;
-      -ms-flex-pack: start;
-          justify-content: flex-start;
+  justify-content: flex-start;
   margin-bottom: 20px;
 }
 
@@ -1791,12 +1705,9 @@ select::placeholder {
   overflow: auto;
   scroll-behavior: smooth;
   scrollbar-width: none;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   margin: auto;
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
+  box-sizing: border-box;
   padding-bottom: 10px;
 }
 
@@ -1820,8 +1731,7 @@ select::placeholder {
 }
 
 .track-carousel__track img {
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
 }
 
 .track-carousel__track-title {
@@ -1829,14 +1739,9 @@ select::placeholder {
 }
 
 .track-carousel__button {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: center;
-      -ms-flex-pack: center;
-          justify-content: center;
-  -ms-flex-line-pack: center;
-      align-content: center;
+  justify-content: center;
+  align-content: center;
   background: white;
   border: none;
   padding: 8px;
@@ -1849,22 +1754,17 @@ select::placeholder {
 
 .track-carousel__button--prev {
   left: 0;
-  -webkit-transform: translate(50%, -50%);
-          transform: translate(50%, -50%);
+  transform: translate(50%, -50%);
 }
 
 .track-carousel__button--next {
   right: 0;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
 }
 
 @media (max-width: 575.98px) {
   .entity-list .entity-list__entity {
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
     margin-bottom: 30px;
   }
   .entity-list .entity-list__entity-text {
@@ -1883,10 +1783,7 @@ select::placeholder {
     -webkit-line-clamp: 5;
   }
   .entity-detail {
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
   }
   .entity-detail .entity-detail__title {
     margin-bottom: 5px;
@@ -1894,13 +1791,8 @@ select::placeholder {
   .entity-detail .entity-detail__info {
     margin-left: 0;
     float: none;
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
     width: 100%;
   }
   .entity-detail .entity-detail__img {
@@ -1920,8 +1812,7 @@ select::placeholder {
     margin-top: 24px;
   }
   .entity-sort .entity-sort__entity {
-    -ms-flex-preferred-size: 50%;
-        flex-basis: 50%;
+    flex-basis: 50%;
   }
   .entity-sort .entity-sort__entity-img {
     height: 130px;
@@ -1947,23 +1838,14 @@ select::placeholder {
     padding: 32px 28px 28px 28px;
   }
   .entity-detail {
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
   }
 }
 
 .aside-section-wrapper {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-flex: 1;
-      -ms-flex: 1;
-          flex: 1;
-  -webkit-box-orient: vertical;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: column;
-          flex-direction: column;
+  flex: 1;
+  flex-direction: column;
   width: 100%;
   padding: 28px 25px 12px 25px;
   background-color: #f7f7f7;
@@ -2005,18 +1887,12 @@ select::placeholder {
 }
 
 .action-panel .action-panel__button-group {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: justify;
-      -ms-flex-pack: justify;
-          justify-content: space-between;
+  justify-content: space-between;
 }
 
 .action-panel .action-panel__button-group--center {
-  -webkit-box-pack: center;
-      -ms-flex-pack: center;
-          justify-content: center;
+  justify-content: center;
 }
 
 .action-panel .action-panel__button {
@@ -2084,12 +1960,8 @@ select::placeholder {
 }
 
 .user-profile .user-profile__header {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-align: start;
-      -ms-flex-align: start;
-          align-items: flex-start;
+  align-items: flex-start;
   margin-bottom: 15px;
 }
 
@@ -2118,12 +1990,8 @@ select::placeholder {
 }
 
 .user-relation .user-relation__related-user-list {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: start;
-      -ms-flex-pack: start;
-          justify-content: flex-start;
+  justify-content: flex-start;
 }
 
 .user-relation .user-relation__related-user-list:last-of-type {
@@ -2131,8 +1999,7 @@ select::placeholder {
 }
 
 .user-relation .user-relation__related-user {
-  -ms-flex-preferred-size: 25%;
-      flex-basis: 25%;
+  flex-basis: 25%;
   padding: 0px 3px;
   text-align: center;
   display: inline-block;
@@ -2268,7 +2135,7 @@ select::placeholder {
   background-color: #d5d5d5;
   border-radius: 0;
   height: 10px;
-  width: 65%;
+  width: 54%;
 }
 
 .import-panel .import-panel__progress progress::-webkit-progress-bar {
@@ -2310,21 +2177,13 @@ select::placeholder {
 }
 
 .entity-card {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   margin-bottom: 10px;
-  -webkit-box-orient: vertical;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: column;
-          flex-direction: column;
+  flex-direction: column;
 }
 
 .entity-card--horizontal {
-  -webkit-box-orient: horizontal;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: row;
-          flex-direction: row;
+  flex-direction: row;
 }
 
 .entity-card .entity-card__img {
@@ -2353,8 +2212,7 @@ select::placeholder {
 }
 
 .entity-card .entity-card__img-wrapper {
-  -ms-flex-preferred-size: 100px;
-      flex-basis: 100px;
+  flex-basis: 100px;
 }
 
 @media (max-width: 575.98px) {
@@ -2373,16 +2231,10 @@ select::placeholder {
     margin-bottom: 20px !important;
   }
   .action-panel {
-    -webkit-box-orient: vertical !important;
-    -webkit-box-direction: normal !important;
-        -ms-flex-direction: column !important;
-            flex-direction: column !important;
+    flex-direction: column !important;
   }
   .entity-card--horizontal {
-    -webkit-box-orient: vertical !important;
-    -webkit-box-direction: normal !important;
-        -ms-flex-direction: column !important;
-            flex-direction: column !important;
+    flex-direction: column !important;
   }
   .entity-card .entity-card__info-wrapper {
     margin-left: 10px !important;
@@ -2394,11 +2246,8 @@ select::placeholder {
 
 @media (max-width: 991.98px) {
   .add-entity-entries {
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
-    -ms-flex-pack: distribute;
-        justify-content: space-around;
+    justify-content: space-around;
   }
   .aside-section-wrapper {
     padding: 24px 25px 10px 25px;
@@ -2419,15 +2268,10 @@ select::placeholder {
     margin: 0;
   }
   .action-panel {
-    -webkit-box-orient: horizontal;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: row;
-            flex-direction: row;
+    flex-direction: row;
   }
   .action-panel .action-panel__button-group {
-    -webkit-box-pack: space-evenly;
-        -ms-flex-pack: space-evenly;
-            justify-content: space-evenly;
+    justify-content: space-evenly;
   }
   .relation-dropdown {
     margin-bottom: 20px;
@@ -2436,54 +2280,36 @@ select::placeholder {
     padding-bottom: 10px;
     background-color: #f7f7f7;
     width: 100%;
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
-    -webkit-box-pack: center;
-        -ms-flex-pack: center;
-            justify-content: center;
-    -webkit-box-align: center;
-        -ms-flex-align: center;
-            align-items: center;
+    justify-content: center;
+    align-items: center;
     cursor: pointer;
-    -webkit-transition: -webkit-transform 0.3s;
-    transition: -webkit-transform 0.3s;
     transition: transform 0.3s;
-    transition: transform 0.3s, -webkit-transform 0.3s;
   }
   .relation-dropdown .relation-dropdown__button:focus {
     background-color: red;
   }
   .relation-dropdown .relation-dropdown__button > .icon-arrow {
-    -webkit-transition: -webkit-transform 0.3s;
-    transition: -webkit-transform 0.3s;
     transition: transform 0.3s;
-    transition: transform 0.3s, -webkit-transform 0.3s;
   }
   .relation-dropdown .relation-dropdown__button:hover > .icon-arrow > svg {
     fill: #00a1cc;
   }
   .relation-dropdown .relation-dropdown__button > .icon-arrow--expand {
-    -webkit-transform: rotate(-180deg);
-            transform: rotate(-180deg);
+    transform: rotate(-180deg);
   }
   .relation-dropdown .relation-dropdown__button + .relation-dropdown__body--expand {
     max-height: 2000px;
-    -webkit-transition: max-height 1s ease-in;
     transition: max-height 1s ease-in;
   }
   .relation-dropdown .relation-dropdown__body {
     background-color: #f7f7f7;
     max-height: 0;
-    -webkit-transition: max-height 1s ease-out;
     transition: max-height 1s ease-out;
     overflow: hidden;
   }
   .entity-card {
-    -webkit-box-orient: horizontal;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: row;
-            flex-direction: row;
+    flex-direction: row;
   }
   .entity-card .entity-card__info-wrapper {
     margin-left: 30px;
@@ -2510,21 +2336,7 @@ select::placeholder {
   overflow: auto;
 }
 
-.entity-form > input[type='email'],
-.entity-form > input[type='number'],
-.entity-form > input[type='password'],
-.entity-form > input[type='search'],
-.entity-form > input[type='tel'],
-.entity-form > input[type='text'],
-.entity-form > input[type='url'],
-.entity-form textarea, .review-form > input[type='email'],
-.review-form > input[type='number'],
-.review-form > input[type='password'],
-.review-form > input[type='search'],
-.review-form > input[type='tel'],
-.review-form > input[type='text'],
-.review-form > input[type='url'],
-.review-form textarea {
+.entity-form > input[type='email'], .entity-form > input[type='number'], .entity-form > input[type='password'], .entity-form > input[type='search'], .entity-form > input[type='tel'], .entity-form > input[type='text'], .entity-form > input[type='url'], .entity-form textarea, .review-form > input[type='email'], .review-form > input[type='number'], .review-form > input[type='password'], .review-form > input[type='search'], .review-form > input[type='tel'], .review-form > input[type='text'], .review-form > input[type='url'], .review-form textarea {
   width: 100%;
 }
 
@@ -2614,16 +2426,12 @@ select::placeholder {
 
 .ms-parent > .ms-choice {
   margin-bottom: 1.5rem;
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
+  appearance: none;
   background-color: transparent;
   border: 0.1rem solid #ccc;
   border-radius: .4rem;
-  -webkit-box-shadow: none;
-          box-shadow: none;
-  -webkit-box-sizing: inherit;
-          box-sizing: inherit;
+  box-shadow: none;
+  box-sizing: inherit;
   padding: .6rem 1.0rem;
   width: 100%;
   height: 30.126px;
@@ -2664,7 +2472,9 @@ select::placeholder {
 }
 
 .tag-input input {
-  -webkit-box-flex: 1;
-      -ms-flex-positive: 1;
-          flex-grow: 1;
+  flex-grow: 1;
+}
+
+.tools-section-wrapper input, .tools-section-wrapper select {
+  width: unset;
 }
diff --git a/common/static/css/boofilsic.min.css b/common/static/css/boofilsic.min.css
index 19ea74b6..4a5c94d6 100644
--- a/common/static/css/boofilsic.min.css
+++ b/common/static/css/boofilsic.min.css
@@ -1 +1 @@
-@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;-o-object-fit:contain;object-fit:contain}img.emoji{height:14px;-webkit-box-sizing:border-box;box-sizing:border-box;-o-object-fit:contain;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::-webkit-input-placeholder,input[type='number']::-webkit-input-placeholder,input[type='password']::-webkit-input-placeholder,input[type='search']::-webkit-input-placeholder,input[type='tel']::-webkit-input-placeholder,input[type='text']::-webkit-input-placeholder,input[type='url']::-webkit-input-placeholder,input[type='date']::-webkit-input-placeholder,input[type='time']::-webkit-input-placeholder,input[type='color']::-webkit-input-placeholder,textarea::-webkit-input-placeholder,select::-webkit-input-placeholder{color:#ccc}input[type='email']:-ms-input-placeholder,input[type='number']:-ms-input-placeholder,input[type='password']:-ms-input-placeholder,input[type='search']:-ms-input-placeholder,input[type='tel']:-ms-input-placeholder,input[type='text']:-ms-input-placeholder,input[type='url']:-ms-input-placeholder,input[type='date']:-ms-input-placeholder,input[type='time']:-ms-input-placeholder,input[type='color']:-ms-input-placeholder,textarea:-ms-input-placeholder,select:-ms-input-placeholder{color:#ccc}input[type='email']::-ms-input-placeholder,input[type='number']::-ms-input-placeholder,input[type='password']::-ms-input-placeholder,input[type='search']::-ms-input-placeholder,input[type='tel']::-ms-input-placeholder,input[type='text']::-ms-input-placeholder,input[type='url']::-ms-input-placeholder,input[type='date']::-ms-input-placeholder,input[type='time']::-ms-input-placeholder,input[type='color']::-ms-input-placeholder,textarea::-ms-input-placeholder,select::-ms-input-placeholder{color:#ccc}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::-moz-selection{color:white;background-color:#00a1cc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative}.navbar .navbar__logo{-ms-flex-preferred-size:100px;flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;-webkit-transform:scale(0.7);transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.grid .grid__aside--tablet-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.grid--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__main--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__aside--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;-webkit-transform:translateX(-50%) scale(0.4);transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:spinner 1.2s linear infinite;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){-webkit-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg);-webkit-animation-delay:-1s;animation-delay:-1s}.spinner div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation-delay:-.8s;animation-delay:-.8s}.spinner div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg);-webkit-animation-delay:-.7s;animation-delay:-.7s}.spinner div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg);-webkit-animation-delay:-.6s;animation-delay:-.6s}.spinner div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation-delay:-.5s;animation-delay:-.5s}.spinner div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg);-webkit-animation-delay:-.4s;animation-delay:-.4s}.spinner div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg);-webkit-animation-delay:-.3s;animation-delay:-.3s}.spinner div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg);-webkit-animation-delay:-.2s;animation-delay:-.2s}.spinner div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg);-webkit-animation-delay:-.1s;animation-delay:-.1s}.spinner div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg);-webkit-animation-delay:0s;animation-delay:0s}@-webkit-keyframes spinner{0%{opacity:1}100%{opacity:0}}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;-webkit-filter:opacity(20%);filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.source-label.source-label__bangumi{background:#FCFCFC;color:#F09199;font-style:italic;font-weight:600}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{-o-object-fit:contain;object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;-o-object-fit:contain;object-fit:contain;max-width:150px;-o-object-position:top;object-position:top}.entity-detail .entity-detail__img-origin{cursor:-webkit-zoom-in;cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:-webkit-box;display:-ms-flexbox;display:flex;color:#00a1cc;background-color:transparent;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:8px}.entity-sort .entity-sort__count{color:#bbb}.entity-sort .entity-sort__count::before{content:'('}.entity-sort .entity-sort__count::after{content:')'}.entity-sort .entity-sort__entity-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;-ms-flex-preferred-size:20%;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:-webkit-box;display:-ms-flexbox;display:flex;margin:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{-o-object-fit:contain;object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-line-pack:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;-webkit-transform:translate(50%, -50%);transform:translate(50%, -50%)}.track-carousel__button--next{right:0;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{-ms-flex-preferred-size:50%;flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:-webkit-box;display:-ms-flexbox;display:flex}}.aside-section-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.action-panel .action-panel__button-group--center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.user-relation .user-relation__related-user-list:last-of-type{margin-bottom:0}.user-relation .user-relation__related-user{-ms-flex-preferred-size:25%;flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");width:48px;height:48px}@media (min-width: 575.98px) and (max-width: 991.98px){.user-relation .user-relation__related-user-avatar{height:unset;width:60%;max-width:96px}}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__body{padding-left:0}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.import-panel{overflow-x:hidden}.import-panel .import-panel__label{display:inline-block;margin-bottom:10px}.import-panel .import-panel__body{padding-left:0;border:2px dashed #00a1cc;padding:6px 9px}.import-panel .import-panel__body form{margin:0}@media (max-width: 991.98px){.import-panel .import-panel__body{border:unset;padding-left:0}}.import-panel .import-panel__help{background-color:#e5e5e5;border-radius:100000px;display:inline-block;width:16px;height:16px;text-align:center;font-size:12px;cursor:help}.import-panel .import-panel__checkbox{display:inline-block;margin-right:10px}.import-panel .import-panel__checkbox label{display:inline}.import-panel .import-panel__checkbox input[type="checkbox"]{margin:0;position:relative;top:2px}.import-panel .import-panel__checkbox--last{margin-right:0}.import-panel .import-panel__file-input{margin-top:10px}.import-panel .import-panel__button{line-height:unset;height:unset;padding:4px 15px}.import-panel .import-panel__progress{padding-top:10px}.import-panel .import-panel__progress:not(:first-child){border-top:#bbb 1px dashed}.import-panel .import-panel__progress label{display:inline}.import-panel .import-panel__progress progress{background-color:#d5d5d5;border-radius:0;height:10px;width:65%}.import-panel .import-panel__progress progress::-webkit-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__progress progress::-webkit-progress-value{background-color:#00a1cc}.import-panel .import-panel__progress progress::-moz-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__last-task:not(:first-child){padding-top:4px;border-top:#bbb 1px dashed}.import-panel .import-panel__last-task .index:not(:last-of-type){margin-right:8px}.import-panel .import-panel__fail-urls{margin-top:10px}.import-panel .import-panel__fail-urls li{word-break:break-all}.import-panel .import-panel__fail-urls ul{max-height:100px;overflow-y:auto}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-card--horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{-ms-flex-preferred-size:100px;flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card--horizontal{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.action-panel .action-panel__button-group{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:2000px;-webkit-transition:max-height 1s ease-in;transition:max-height 1s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;-webkit-transition:max-height 1s ease-out;transition:max-height 1s ease-out;overflow:hidden}.entity-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset}.tippy-box{border:#606c76 1px solid;background-color:#f7f7f7;padding:3px 5px}.tag-input input{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}
+@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;object-fit:contain}img.emoji{height:14px;box-sizing:border-box;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:flex;justify-content:space-between;align-items:center;position:relative}.navbar .navbar__logo{flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:flex;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:inline-flex;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:flex;flex-direction:column;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;flex-direction:row}.grid .grid__aside--tablet-column{flex-direction:column}.grid--reverse-order{transform:scaleY(-1)}.grid .grid__main--reverse-order{transform:scaleY(-1)}.grid .grid__aside--reverse-order{transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{transform-origin:40px 40px;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){transform:rotate(0deg);animation-delay:-1.1s}.spinner div:nth-child(2){transform:rotate(30deg);animation-delay:-1s}.spinner div:nth-child(3){transform:rotate(60deg);animation-delay:-.9s}.spinner div:nth-child(4){transform:rotate(90deg);animation-delay:-.8s}.spinner div:nth-child(5){transform:rotate(120deg);animation-delay:-.7s}.spinner div:nth-child(6){transform:rotate(150deg);animation-delay:-.6s}.spinner div:nth-child(7){transform:rotate(180deg);animation-delay:-.5s}.spinner div:nth-child(8){transform:rotate(210deg);animation-delay:-.4s}.spinner div:nth-child(9){transform:rotate(240deg);animation-delay:-.3s}.spinner div:nth-child(10){transform:rotate(270deg);animation-delay:-.2s}.spinner div:nth-child(11){transform:rotate(300deg);animation-delay:-.1s}.spinner div:nth-child(12){transform:rotate(330deg);animation-delay:0s}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}.add-to-list-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.add-to-list-modal .add-to-list-modal__head{margin-bottom:20px}.add-to-list-modal .add-to-list-modal__head::after{content:' ';clear:both;display:table}.add-to-list-modal .add-to-list-modal__title{font-weight:bold;font-size:1.2em;float:left}.add-to-list-modal .add-to-list-modal__close-button{float:right;cursor:pointer}.add-to-list-modal .add-to-list-modal__confirm-button{float:right}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal .add-to-list-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__igdb{background-color:#323A44;color:#DFE1E2;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.source-label.source-label__bangumi{background:#FCFCFC;color:#F09199;font-style:italic;font-weight:600}.source-label.source-label__goodreads{background:#F4F1EA;color:#372213;font-weight:lighter}.source-label.source-label__tmdb{background:linear-gradient(90deg, #91CCA3, #1FB4E2);color:white;border:none;font-weight:lighter;padding-top:2px}.source-label.source-label__googlebooks{color:white;background-color:#4285F4;border-color:#4285F4}.source-label.source-label__bandcamp{color:#fff;background-color:#28A0C1;display:inline-block}.source-label.source-label__bandcamp span{display:inline-block;margin:0 4px}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;object-fit:contain;max-width:150px;object-position:top}.entity-detail .entity-detail__img-origin{cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:flex;color:#00a1cc;background-color:transparent;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:8px}.entity-sort .entity-sort__count{color:#bbb}.entity-sort .entity-sort__count::before{content:'('}.entity-sort .entity-sort__count::after{content:')'}.entity-sort .entity-sort__entity-list{display:flex;justify-content:flex-start;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:flex;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:flex;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:flex;margin:auto;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:flex;justify-content:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;transform:translate(50%, -50%)}.track-carousel__button--next{right:0;transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:flex;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:flex}}.aside-section-wrapper{display:flex;flex:1;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:flex;justify-content:space-between}.action-panel .action-panel__button-group--center{justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:flex;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:flex;justify-content:flex-start}.user-relation .user-relation__related-user-list:last-of-type{margin-bottom:0}.user-relation .user-relation__related-user{flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");width:48px;height:48px}@media (min-width: 575.98px) and (max-width: 991.98px){.user-relation .user-relation__related-user-avatar{height:unset;width:60%;max-width:96px}}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__body{padding-left:0}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.import-panel{overflow-x:hidden}.import-panel .import-panel__label{display:inline-block;margin-bottom:10px}.import-panel .import-panel__body{padding-left:0;border:2px dashed #00a1cc;padding:6px 9px}.import-panel .import-panel__body form{margin:0}@media (max-width: 991.98px){.import-panel .import-panel__body{border:unset;padding-left:0}}.import-panel .import-panel__help{background-color:#e5e5e5;border-radius:100000px;display:inline-block;width:16px;height:16px;text-align:center;font-size:12px;cursor:help}.import-panel .import-panel__checkbox{display:inline-block;margin-right:10px}.import-panel .import-panel__checkbox label{display:inline}.import-panel .import-panel__checkbox input[type="checkbox"]{margin:0;position:relative;top:2px}.import-panel .import-panel__checkbox--last{margin-right:0}.import-panel .import-panel__file-input{margin-top:10px}.import-panel .import-panel__button{line-height:unset;height:unset;padding:4px 15px}.import-panel .import-panel__progress{padding-top:10px}.import-panel .import-panel__progress:not(:first-child){border-top:#bbb 1px dashed}.import-panel .import-panel__progress label{display:inline}.import-panel .import-panel__progress progress{background-color:#d5d5d5;border-radius:0;height:10px;width:54%}.import-panel .import-panel__progress progress::-webkit-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__progress progress::-webkit-progress-value{background-color:#00a1cc}.import-panel .import-panel__progress progress::-moz-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__last-task:not(:first-child){padding-top:4px;border-top:#bbb 1px dashed}.import-panel .import-panel__last-task .index:not(:last-of-type){margin-right:8px}.import-panel .import-panel__fail-urls{margin-top:10px}.import-panel .import-panel__fail-urls li{word-break:break-all}.import-panel .import-panel__fail-urls ul{max-height:100px;overflow-y:auto}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:flex;margin-bottom:10px;flex-direction:column}.entity-card--horizontal{flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{flex-direction:column !important}.entity-card--horizontal{flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:flex;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{flex-direction:row}.action-panel .action-panel__button-group{justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:flex;justify-content:center;align-items:center;cursor:pointer;transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:2000px;transition:max-height 1s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;transition:max-height 1s ease-out;overflow:hidden}.entity-card{flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset}.tippy-box{border:#606c76 1px solid;background-color:#f7f7f7;padding:3px 5px}.tag-input input{flex-grow:1}.tools-section-wrapper input,.tools-section-wrapper select{width:unset}
diff --git a/common/static/img/fediverse.svg b/common/static/img/fediverse.svg
new file mode 100644
index 00000000..c3ab108c
--- /dev/null
+++ b/common/static/img/fediverse.svg
@@ -0,0 +1,5 @@
+<svg width="850" height="850" xmlns="http://www.w3.org/2000/svg" version="1.1">
+ <g>
+  <path d="m464.16327,0q-32,0 -55,22t-25,55t20.5,58t56,27t58.5,-20.5t27,-56t-20.5,-59t-56.5,-26.5l-5,0zm-87,95l-232,118q20,20 25,48l231,-118q-19,-20 -24,-48zm167,27q-13,25 -38,38l183,184q13,-25 39,-38l-184,-184zm-142,22l-135,265l40,40l143,-280q-28,-5 -48,-25zm104,16q-22,11 -46,10l-8,-1l21,132l56,9l-23,-150zm-426,34q-32,0 -55,22.5t-25,55t20.5,58t56.5,27t59,-21t26.5,-56t-21,-58.5t-55.5,-27l-6,0zm90,68q1,9 1,18q-1,19 -10,35l132,21l26,-50l-149,-24zm225,36l-26,51l311,49q-1,-8 -1,-17q1,-19 10,-36l-294,-47zm372,6q-32,1 -55,23t-24.5,55t21,58t56,27t58.5,-20.5t27,-56.5t-20.5,-59t-56.5,-27l-6,0zm-606,13q-13,25 -39,38l210,210l51,-25l-222,-223zm-40,38q-21,11 -44,10l-9,-1l40,256q21,-10 45,-9l8,1l-40,-257zm364,22l48,311q21,-10 44,-9l10,1l-46,-294l-56,-9zm195,23l-118,60l8,56l135,-68q-20,-20 -25,-48zm26,49l-119,231q28,5 48,25l119,-231q-28,-5 -48,-25zm-475,29l-68,134q28,5 48,25l60,-119l-40,-40zm262,17l-281,143q19,20 24,48l265,-135l-8,-56zm-55,100l-51,25l106,107q13,-25 39,-38l-94,-94zm-291,24q-32,0 -55.5,22.5t-25,55t21,57.5t56,27t58.5,-20.5t27,-56t-20.5,-58.5t-56.5,-27l-5,0zm89,68q2,9 1,18q-1,19 -9,35l256,41q-1,-9 -1,-18q1,-18 10,-35l-257,-41zm335,0q-32,0 -55,22.5t-24.5,55t20.5,58t56,27t59,-21t27,-56t-20.5,-58.5t-56.5,-27l-6,0z"/>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/common/static/img/logo.svg b/common/static/img/logo.svg
index 0570333b..cc10cff8 100644
--- a/common/static/img/logo.svg
+++ b/common/static/img/logo.svg
@@ -1 +1,140 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 101.25 37.29"><title>logo</title><path d="M1.55,30.26H2.78l2.1,3.81.7,1.44h0c-.06-.7-.16-1.56-.16-2.31V30.26H6.61V37H5.38l-2.1-3.81-.7-1.44h0c.06.72.15,1.53.15,2.29v3H1.55Z" style="fill:#606c76"/><path d="M13.09,30.26h1.2V37h-1.2Z" style="fill:#606c76"/><path d="M20.41,33.67c0-2.22,1.38-3.53,3.1-3.53a2.66,2.66,0,0,1,1.95.86l-.64.77a1.78,1.78,0,0,0-1.29-.59c-1.1,0-1.89.93-1.89,2.45s.73,2.48,1.86,2.48A1.94,1.94,0,0,0,25,35.42l.64.75a2.68,2.68,0,0,1-2.13,1C21.76,37.15,20.41,35.91,20.41,33.67Z" style="fill:#606c76"/><path d="M31.48,30.26h4.07v1H32.68V33h2.43v1H32.68v2h3v1H31.48Z" style="fill:#606c76"/><path d="M41.79,30.26H43.6a3,3,0,0,1,3.3,3.36c0,2.24-1.23,3.41-3.24,3.41H41.79Zm1.73,5.8c1.36,0,2.14-.78,2.14-2.44s-.78-2.38-2.14-2.38H43v4.82Z" style="fill:#606c76"/><path d="M53,30.26h2.17c1.39,0,2.41.43,2.41,1.67a1.53,1.53,0,0,1-.94,1.47v0A1.55,1.55,0,0,1,58,35c0,1.36-1.11,2-2.6,2H53Zm2.06,2.8c.94,0,1.35-.37,1.35-1s-.45-.9-1.32-.9h-.89v1.86Zm.16,3c1,0,1.55-.35,1.55-1.11s-.54-1-1.55-1h-1v2.14Z" style="fill:#606c76"/><path d="M63.73,36.35a.77.77,0,1,1,1.54,0,.77.77,0,1,1-1.54,0Z" style="fill:#606c76"/><path d="M71.19,33.62c0-2.19,1.22-3.48,3-3.48s3,1.3,3,3.48-1.22,3.53-3,3.53S71.19,35.81,71.19,33.62Zm4.73,0c0-1.52-.69-2.44-1.75-2.44s-1.75.92-1.75,2.44.69,2.49,1.75,2.49S75.92,35.14,75.92,33.62Z" style="fill:#606c76"/><path d="M83.27,30.26h2.28c1.41,0,2.49.5,2.49,2S87,34.39,85.55,34.39H84.47V37h-1.2Zm2.16,3.16c.92,0,1.43-.38,1.43-1.15s-.51-1.05-1.43-1.05h-1v2.2Zm-.1.64.86-.71L88.31,37H87Z" style="fill:#606c76"/><path d="M93.79,33.67A3.2,3.2,0,0,1,97,30.14,2.78,2.78,0,0,1,99,31l-.65.77A1.81,1.81,0,0,0,97,31.18c-1.2,0-2,.93-2,2.45s.72,2.48,2.07,2.48a1.54,1.54,0,0,0,1-.3V34.35H96.78v-1h2.36v3a3.08,3.08,0,0,1-2.16.8C95.15,37.15,93.79,35.91,93.79,33.67Z" style="fill:#606c76"/><path d="M15.48,26.35h-.15L1.39,23.41a.71.71,0,0,1-.56-.69V2.32a.74.74,0,0,1,.26-.55.73.73,0,0,1,.59-.14L15.62,4.56a.71.71,0,0,1,.56.69v20.4a.7.7,0,0,1-.26.54A.66.66,0,0,1,15.48,26.35ZM2.23,22.15l12.55,2.64v-19L2.23,3.18Z" style="fill:#00a1cc"/><path d="M15.48,26.35a.65.65,0,0,1-.44-.16.67.67,0,0,1-.26-.54V5.25a.7.7,0,0,1,.55-.69l14-2.93a.7.7,0,0,1,.84.69v20.4a.71.71,0,0,1-.55.69L15.62,26.34Zm.7-20.54v19l12.54-2.64v-19ZM29.42,22.72h0Z" style="fill:#00a1cc"/><path d="M87.55,26.48a13.17,13.17,0,1,1,13.17-13.17A13.19,13.19,0,0,1,87.55,26.48Zm0-24.94A11.77,11.77,0,1,0,99.32,13.31,11.78,11.78,0,0,0,87.55,1.54Z" style="fill:#00a1cc"/><path d="M87.55,18.38a5.08,5.08,0,1,1,5.08-5.07A5.08,5.08,0,0,1,87.55,18.38Zm0-8.75a3.68,3.68,0,1,0,3.67,3.68A3.68,3.68,0,0,0,87.55,9.63Z" style="fill:#00a1cc"/><path d="M60.63,22.63H36.21a.7.7,0,0,1-.7-.7V4.69a.7.7,0,0,1,.7-.7H60.63a.7.7,0,0,1,.7.7V21.93A.7.7,0,0,1,60.63,22.63Zm-23.72-1.4h23V5.39h-23Z" style="fill:#00a1cc"/><path d="M69.17,22.63a.68.68,0,0,1-.31-.08l-8.55-4.29a.71.71,0,0,1-.38-.62V9a.71.71,0,0,1,.38-.63l8.55-4.29a.7.7,0,0,1,1,.63V21.93a.68.68,0,0,1-.33.59A.7.7,0,0,1,69.17,22.63Zm-7.84-5.42,7.14,3.58v-15L61.33,9.41Z" style="fill:#00a1cc"/></svg>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="220.10335"
+   height="107.60636"
+   viewBox="0 0 220.10335 107.60636"
+   data-svgdocument=""
+   id="_3tPmdtZQ0LGlljQGEkT-3"
+   class="fl-svgdocument"
+   x="0"
+   y="0"
+   version="1.1"
+   sodipodi:docname="logo.svg"
+   inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview21"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     units="px"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="1.4707031"
+     inkscape:cx="298.83666"
+     inkscape:cy="77.853919"
+     inkscape:window-width="1792"
+     inkscape:window-height="1067"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="_3tPmdtZQ0LGlljQGEkT-3"
+     inkscape:document-units="mm"
+     inkscape:snap-global="false" />
+  <defs
+     id="_SabXoCmWUJmxvMwSINRGH"
+     transform="matrix(1.1728515134752755, 0, 0, 1.1728515134752755, -34.46172819025348, -40.976997984526726)">
+    <rect
+       x="86.353256"
+       y="25.158035"
+       width="217.58301"
+       height="60.515274"
+       id="rect4965" />
+  </defs>
+  <g
+     id="_WTrHMt3eZfxcQKrYJirRK"
+     transform="matrix(0.95294163,0,0,0.95294163,-45.741197,-190.14988)">
+    <path
+       id="_1MYZsbAYvxe5D-50H3pho"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.519653,192.79657)"
+       data-type="polygon"
+       d="M 82.1,74 75,81.1 v 0 L 82.1,74 74,65.9 v 0 z" />
+    <path
+       id="_oL3QJlwkbW1Nmdh3qAlDC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.45702,193.17499)"
+       data-type="rect"
+       data-x="64.5"
+       data-y="51.5"
+       data-width="0"
+       data-height="9.8"
+       d="M 64.5,51.5" />
+    <path
+       id="_sK1htog-8Op55rgRI5hDg"
+       d="m 61.1,52.9 v 0 0 l -4.9,4.9 c 2.5,1.2 4.3,3.8 4.3,6.9 0,4.2 -3.4,7.6 -7.6,7.6 -3,0 -5.6,-1.8 -6.9,-4.3 L 40.1,73.9 40,74 61,95 69.1,86.9 v 0 c -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 l 7.1,-7.1 -8.1,-8 v 0 l -0.6,-0.6 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 0,-1.5 -0.6,-2.8 -1.5,-3.9 -1,-0.9 -2.4,-1.5 -3.9,-1.5 -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 68,59.9 v 0 z"
+       fill="#083b66"
+       transform="matrix(1.2602785,0,0,1.2602785,27.51993,192.73364)"
+       style="fill:#ffb380" />
+    <path
+       id="_OMWHsyuaoclMfPE9tvM7V"
+       d="m 46.5,49.5 c -4.2,0 -7.6,-3.4 -7.6,-7.6 0,-3 1.7,-5.6 4.2,-6.8 l -5.9,-5.9 v 0 l -21,21 22.2,22.2 9.5,-9.5 c -0.3,0.6 -0.4,1.3 -0.4,2 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-2.9 -2.4,-5.3 -5.3,-5.3 -0.7,0 -1.4,0.1 -2,0.4 l 8.6,-8.6 -6,-6 c -1.3,2.3 -3.9,4.1 -6.9,4.1 z"
+       fill="#8dcaff"
+       transform="matrix(1.2602785,0,0,1.2602785,27.583487,192.79709)" />
+    <path
+       id="_jAk8zpl6O80_21e1JT4hk"
+       d="m 83.3,27.4 -8.7,-8.7 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 C 80.9,9.6 78.3,7 75,7 c -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 61,5.2 l -21,21 -0.7,0.8 -0.4,0.4 9.5,9.5 c -0.6,-0.2 -1.2,-0.3 -1.8,-0.3 -2.9,0 -5.3,2.4 -5.3,5.3 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-0.5 -0.1,-0.9 -0.2,-1.4 0,-0.2 -0.1,-0.3 -0.1,-0.5 l 0.5,0.5 9.2,9.2 0.4,-0.4 0.8,-0.8 6.8,-6.8 c 0,0 0,0 0,0 -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 0,0 0,0 0,0 z"
+       fill="#c0d9b4"
+       transform="matrix(1.2602785,0,0,1.2602785,27.709267,192.98645)" />
+    <path
+       id="_GDSYpBtYoW2GJpRZoPCot"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_NldltLD5fZQlQrgFsakaC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_TbX_b0jxU8J0Cb3YWVDy6"
+       d="m 51.6,40.5 h 0.3 L 51.5,40 c 0,0.1 0,0.3 0.1,0.5 z"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.708767,192.98587)" />
+  </g>
+  <g
+     aria-label="NeoDB"
+     transform="matrix(1.5308318,0,0,1.7424549,-43.057047,-45.264217)"
+     id="text4963"
+     style="font-size:40px;line-height:1.25;white-space:pre;shape-inside:url(#rect4965);fill:#083b66">
+    <path
+       d="m 107.58017,70.426948 c 0,-1.866665 -0.85333,-6.026663 -1.28,-7.786662 -0.10667,-0.426666 0,-0.853333 -0.10667,-1.279999 -0.90666,-3.786665 -1.44,-7.733329 -1.92,-11.573327 -0.15999,-1.546665 -0.58666,-3.039998 -0.58666,-4.586664 0,-0.266666 -0.10667,-1.919998 0.42667,-1.919998 0.74666,0 0.90666,1.226666 1.59999,1.226666 0.26667,0 0.64,-0.106667 0.64,-0.426667 0,-1.013333 -1.92,-1.866665 -2.82666,-1.866665 -3.09333,0 -2.93333,3.626664 -2.93333,5.919996 0,2.079999 0.32,4.373331 0.58666,6.45333 0.16,1.279999 0.10667,2.559998 0.32,3.839997 0.16,1.12 0.64,2.719999 0.64,3.839998 0,0.906666 0.16,1.706666 0.26667,2.559998 -0.48,-0.746666 -0.69333,-1.493332 -1.12,-2.239998 -0.64,-1.066666 -1.28,-2.186666 -1.813332,-3.306665 -0.373333,-0.746666 -0.8,-1.439999 -1.226666,-2.186665 -0.426667,-0.746667 -0.693333,-1.546666 -1.119999,-2.293332 -1.919999,-3.253332 -3.626665,-6.666663 -5.493331,-9.919994 -0.213333,-0.373333 -0.693332,-0.426667 -1.066666,-0.426667 -0.693332,0 -2.933331,0.533333 -3.039998,1.333333 -0.16,1.333332 -0.16,2.719998 -0.16,4.053331 v 4.53333 c 0,1.013333 0.106667,1.973332 0.106667,2.933332 0,1.386666 0.16,2.719998 0.16,4.053331 0,0.799999 0.16,1.546666 0.16,2.346665 0,1.333333 0.05333,2.826665 0.266666,4.159998 0.106667,0.533333 0.213333,1.279999 0.213333,1.866665 0,2.399999 -0.266666,4.053331 2.826665,4.053331 0.8,0 3.093332,-0.426666 3.093332,-1.439999 0,-0.48 -0.16,-0.959999 -0.16,-1.493333 0,-2.666665 -0.213333,-5.759996 -0.64,-8.426661 -0.159999,-1.119999 -0.426666,-2.559999 -0.426666,-3.679998 0,-2.559998 -0.693333,-4.746664 -0.693333,-7.413329 1.279999,2.933332 9.599998,20.853321 11.786658,20.853321 0.8,0 3.52,-0.799999 3.52,-1.759999 z M 90.246847,53.573625 c 0.266666,0.106666 0.266666,1.333332 0.266666,1.546666 0,1.119999 -0.16,2.239998 -0.16,3.359998 -0.16,-1.493333 -0.16,-2.986665 -0.16,-4.479998 0,-0.16 0,-0.266666 0.05333,-0.426666 z m 0.266666,18.453322 0.05333,-0.106666 c 0,-0.373334 -0.106666,-0.746667 -0.106666,-1.12 v -0.48 c 0.05333,0.266667 0.106666,0.906667 0.32,1.12 0,-0.64 -0.16,-1.226666 -0.16,-1.866666 0,-0.213333 0,-0.639999 0.213333,-0.799999 0,1.119999 0.05333,2.293332 0.05333,3.413331 l -0.16,-0.16 v 0.106667 h -0.213333 z m -0.266666,-7.199996 c -0.05333,-0.266666 -0.05333,-0.479999 -0.05333,-0.746666 v -0.693333 c 0.213334,-0.266666 0.16,-2.453332 0.16,-2.719998 0.106667,0.426666 0.16,1.813332 0.16,2.346665 0,0.266667 0,1.653333 -0.266666,1.813332 z m -1.28,-15.83999 c 0.106667,-0.64 0.05333,-1.333333 0.05333,-1.973332 0.213333,1.706665 0,3.413331 0.426666,5.066663 l -0.106666,0.05333 -0.213334,-1.066666 c 0,0.533333 0.05333,1.119999 0.05333,1.653332 l -0.05333,-0.05333 v -0.533333 c -0.05333,-0.106667 -0.16,-0.32 -0.16,-0.426667 0,-0.32 0.05333,-0.639999 0.05333,-0.959999 l 0.106666,0.32 c -0.05333,-0.693333 0,-1.386666 -0.16,-2.079999 z m 0.266667,12.373326 c 0.05333,-0.373333 -0.05333,-2.239999 -0.16,-2.559999 0.16,-0.266666 0.16,-1.493332 0.16,-1.813332 0.106667,0.213333 0.106667,1.813332 0.106667,2.133332 0,0.426667 0,1.866666 -0.106667,2.239999 z m 14.613326,-0.373333 c -0.0533,0 -0.10667,-0.693333 -0.10667,-0.746667 -0.0533,0.106667 -0.0533,0.266667 -0.0533,0.373334 0,0.106666 0.0533,0.213333 0.0533,0.319999 l -0.0533,0.05333 h 0.0533 c 0,0.32 0.0533,0.639999 -0.0533,0.959999 0,-0.533333 -0.0533,-1.066666 -0.0533,-1.599999 v -0.64 0.266667 c 0.0533,-0.213333 0.0533,-0.426666 0.0533,-0.64 0.16,0.05333 0.16,1.173333 0.16,1.386666 z m -0.10667,-3.466665 c 0,0.213333 -0.0533,0.373333 0,0.586666 v -0.319999 c 0.0533,0.16 0.10667,0.373333 0.10667,0.586666 0,0.213333 -0.0533,0.48 -0.16,0.693333 0,-0.213333 0,-0.426667 -0.0533,-0.64 v 0.266667 -1.973332 c 0,0.05333 0.10666,0.213333 0.10666,0.266666 z m -13.546657,1.493333 c -0.106666,-0.373334 -0.16,-0.853333 -0.106666,-1.226666 l -0.05333,0.16 c 0.106667,-0.586667 -0.05333,-1.12 -0.05333,-1.706666 0,-0.213333 0,-0.426667 0.106667,-0.64 0.05333,1.12 0.05333,2.293332 0.106666,3.413332 z m 0.16,12.586659 c -0.05333,0.16 -0.05333,0.32 -0.05333,0.479999 l -0.16,-0.05333 c 0,-0.266666 -0.05333,-1.119999 0.16,-1.333332 0,0.159999 -0.05333,0.426666 0.05333,0.533333 0.05333,0.266666 0.106667,0.586666 0.106667,0.853332 v 0.05333 L 90.353513,72.08028 Z M 89.926847,57.733622 c 0,0.16 0,0.266667 0.05333,0.426667 -0.16,0.799999 -0.05333,1.759999 -0.05333,2.506665 -0.106667,-0.48 -0.106667,-0.96 -0.106667,-1.493333 0,-0.479999 0,-0.959999 0.106667,-1.439999 z m 0.05333,4.053331 c 0,-0.426666 0.106667,-0.799999 0,-1.279999 l 0.16,0.16 v 0.959999 c 0,0.373333 0,0.8 -0.05333,1.173333 -0.05333,-0.32 -0.106667,-0.693333 -0.106667,-1.013333 z m 0.586666,-11.093326 c 0,0.266666 -0.05333,0.479999 -0.106666,0.746666 -0.05333,-0.05333 -0.05333,-0.16 -0.05333,-0.266667 V 50.58696 c 0,-0.266667 0,-0.48 0.05333,-0.746666 0.05333,0.213333 0.05333,0.479999 0.05333,0.693333 h 0.05333 z m -0.05333,17.173323 c 0,0.213333 0,0.426666 -0.05333,0.639999 -0.05333,-0.213333 -0.05333,-0.479999 -0.05333,-0.693333 V 67.22695 c 0,-0.106667 0,-0.213333 0.05333,-0.32 0.05333,0.32 0.106666,0.64 0.106666,0.96 z m 13.706657,-3.679998 0.16,0.32 c 0,0.213333 -0.10667,0.373333 -0.21333,0.533333 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.16 0,-0.266667 0.0533,-0.426667 z m -0.32,-0.16 c 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 l 0.0533,-0.106667 -0.0533,0.05333 c 0,-0.106666 -0.0533,-0.159999 -0.0533,-0.266666 v -0.64 l 0.10666,-0.05333 c 0,0.48 0,0.906666 0,1.386666 z M 89.180181,54.213624 c -0.05333,-0.16 -0.106667,-0.319999 -0.106667,-0.533333 0,-0.266666 0.05333,-0.586666 0.05333,-0.906666 0.05333,0.16 0.05333,0.373333 0.05333,0.533333 z M 102.78017,51.22696 c -0.0533,0.373333 0.21334,0.853332 0.21334,1.226665 v 0.05333 c -0.16,-0.373333 -0.21334,-0.8 -0.37334,-1.226666 z m -11.359991,8.799994 c -0.05333,-0.32 -0.05333,-0.639999 -0.05333,-0.959999 v -0.373333 c 0.05333,0.213333 0.106667,0.586666 0.106667,0.799999 0,0.16 -0.05333,0.373333 -0.05333,0.533333 z m -2.239998,-4.319997 -0.106667,0.266666 v -0.266666 c 0,-0.213333 0,-0.373333 0.05333,-0.533333 0.05333,0.106666 0.05333,0.213333 0.05333,0.32 l 0.106666,-0.106667 v 0.426666 l -0.05333,-0.05333 -0.05333,0.213333 z m 0.213333,-1.226666 c 0,-0.05333 -0.106667,-0.586666 0,-0.48 -0.05333,-0.266666 0,-0.693333 -0.106667,-0.906666 l 0.106667,0.05333 c 0,0.32 0.05333,0.586667 0.05333,0.906666 0,0.16 0,0.32 -0.05333,0.426667 z m 0.906666,-6.399996 c 0,0.32 0,0.639999 0,0.959999 l -0.106667,-0.48 c 0,-0.106666 0.05333,-0.319999 0.106667,-0.479999 z m 12.74666,5.599996 c 0,0.213334 -0.0533,0.373333 -0.0533,0.586667 -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 v -0.32 c 0.0533,0.106667 0.10667,0.266666 0.10667,0.426666 z m 1.06667,8.693329 0.0533,-0.05333 V 62.05362 l 0.10666,0.32 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 l -0.10666,-0.213334 v 0.32 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0,-0.16 0,-0.373333 0.0533,-0.533333 z m -13.81333,6.613329 c 0,0.266666 -0.05333,0.906666 0.05333,1.226666 l -0.05333,0.106666 c -0.106667,-0.106666 -0.05333,-1.226665 0,-1.333332 z m 0.32,-2.399999 c -0.106667,0 -0.16,0.106667 -0.16,0.213334 0.05333,-0.266667 -0.05333,-0.586667 0,-0.746667 l 0.106666,0.05333 v 0.213333 h 0.05333 z m 12.53333,-8.746661 c 0.0533,0.266666 0.0533,0.586666 0,0.853333 -0.0533,-0.32 -0.0533,-0.533333 0,-0.853333 z m -0.53334,-7.253329 c 0.0533,-0.213333 0.0533,-0.426667 0.0533,-0.64 l 0.0533,0.05333 v 0.693333 z m -13.386656,6.346663 c 0,-0.16 0,-0.373333 -0.106667,-0.533333 l 0.106667,-0.266667 c 0,0.16 0,0.373333 0,0.533333 z m 1.333332,-4.853331 c -0.05333,-0.16 -0.106666,-0.319999 -0.106666,-0.479999 0,0.106666 0.05333,0.16 0.16,0.16 0,0.106666 -0.05333,0.159999 -0.05333,0.319999 0,0 0.05333,-0.05333 0.05333,-0.05333 v 0.05333 z m -0.319999,-4.479997 c -0.106667,-0.32 -0.106667,-0.693333 -0.106667,-1.066666 z m 0.16,11.839993 c 0.159999,0.05333 0.05333,0.373333 0,0.48 z m 0.213333,-6.45333 c 0,0.106667 -0.05333,0.266667 -0.05333,0.373334 v -0.48 z m -0.213333,5.493331 c 0,0.05333 0.159999,0.426666 0,0.426666 z m 0.159999,6.666662 c 0.05333,0.05333 0.05333,0.16 0.05333,0.266667 l -0.05333,0.05333 z m 13.866654,-2.773331 0.0533,0.05333 c 0,0.213333 0,0.426666 -0.0533,0.639999 z M 90.353513,53.200292 c 0,0.106666 0,0.16 -0.05333,0.266666 0,-0.106666 -0.05333,-0.266666 -0.05333,-0.373333 z m 0.16,2.879998 v -0.586666 c 0.05333,0.213333 0.05333,0.373333 0.05333,0.586666 z M 104.9135,70.160282 c 0,0.05333 0.0533,0.106666 0.10667,0.159999 l -0.10667,0.05333 h -0.0533 l -0.0533,-0.05333 z m -0.69333,-9.066662 c 0,0.16 0.10667,0.32 -0.0533,0.48 0,-0.16 0,-0.32 0.0533,-0.48 z m -13.973323,4.159998 c 0,-0.106667 0,-0.213333 0.05333,-0.32 l 0.05333,0.16 c 0,0.106667 -0.05333,0.16 -0.106666,0.16 z m 0.319999,-2.933332 c 0,0.16 0,0.373333 -0.05333,0.533333 v -0.533333 z m 0,-10.18666 v 0.106666 l 0.05333,0.106667 -0.05333,0.05333 v -0.16 l -0.106666,0.05333 v -0.05333 z m 14.079994,11.466659 c 0,-0.106666 0,-0.159999 0.0533,-0.266666 v 0.32 z m -2.13333,-11.893326 c 0,-0.106666 0,-0.16 0.0533,-0.266666 0,0.106666 0,0.16 0.0533,0.266666 z M 90.46018,69.840282 c 0,-0.106667 0,-0.213333 0.05333,-0.32 v 0.213333 z m 0.32,-1.333333 c 0,0.05333 0.05333,0.106667 0.05333,0.106667 0,0.05333 -0.05333,0.106666 -0.05333,0.106666 l -0.05333,-0.05333 c 0,-0.05333 0,-0.106666 0.05333,-0.16 z m -1.386666,-3.466664 c -0.05333,-0.106667 -0.05333,-0.266667 -0.05333,-0.373333 0.05333,0.106666 0.05333,0.266666 0.05333,0.373333 z m 14.933326,0.586666 0.0533,-0.106667 v 0.266667 z m -0.21333,-5.119997 c -0.0533,-0.16 -0.0533,-0.266667 0,-0.373333 z m 0,4.746664 h 0.0533 v 0.16 l -0.0533,-0.05333 z m -13.386664,6.293329 c 0,0.05333 0,0.16 0.05333,0.213334 0,-0.05333 0,-0.16 -0.05333,-0.213334 z m -0.213333,-14.773324 0.05333,0.106666 c 0,0.05333 0,0.05333 -0.05333,0.106667 z m -1.333332,-2.186665 c -0.05333,-0.05333 -0.05333,-0.16 0,-0.266667 z m 15.199989,11.946659 c -0.0533,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.0533,-2.613332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 0,-0.05333 0.0533,-0.05333 0.0533,-0.05333 z m -0.21333,-3.199998 -0.0533,-0.05333 0.0533,-0.106667 z m 0.16,5.33333 c -0.0533,-0.05333 -0.0533,-0.16 0,-0.213333 z m -1.44,-13.173325 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0.0533,0.05333 0.0533,0.106667 0.0533,0.16 z m -13.599989,0 c 0.05333,0.05333 0.05333,0.106666 0.05333,0.16 -0.05333,-0.05333 -0.05333,-0.106667 -0.05333,-0.16 z m 14.773329,8.373328 -0.0533,-0.106666 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m -13.599997,7.679996 v -0.106667 h 0.05333 z m -1.119999,-4.906664 v -0.106667 z m 1.173332,1.973332 v 0.106667 z m 13.119994,-8.533328 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -14.613326,-0.16 v -0.05333 h 0.05333 v 0.05333 z m 1.493332,-0.693333 c 0,0 -0.05333,-0.05333 -0.05333,-0.05333 0,0 0.05333,-0.05333 0.05333,-0.05333 z m 13.599994,7.413329 v -0.05333 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106666 z M 89.446847,63.653619 h -0.05333 v -0.05333 z m 15.253323,-0.533333 h -0.0533 l 0.0533,-0.05333 z m -13.91999,4.959997 0.05333,0.05333 h -0.05333 z m 13.38666,-7.146663 0.0533,-0.05333 v 0.05333 z M 91.366846,71.653614 h 0.05333 l -0.05333,0.05333 z M 104.06017,61.57362 v -0.05333 h 0.0533 z m 0.0533,-1.546666 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -14.719996,-5.33333 0.05333,0.05333 h -0.05333 z m 15.626656,15.626657 v -0.05333 l 0.0533,0.05333 z m -2.02666,-15.306657 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z m -12.479997,6.826663 0.05333,-0.05333 v 0.05333 z M 103.63351,60.93362 c 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 1.33333,8.906662 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path920" />
+    <path
+       d="m 126.07266,60.666954 c 0,-0.266667 -0.16,-0.586666 -0.48,-0.586666 -0.58667,0 -0.64,0.693332 -0.69333,1.119999 -0.42667,2.506665 -2.77334,7.306662 -5.70667,7.306662 -1.76,0 -1.97333,-0.479999 -3.14666,-1.706665 -0.32,-0.96 -0.96,-1.919999 -0.96,-2.986665 v -0.106667 c 0.37333,-0.106667 0.69333,-0.373333 1.01333,-0.586666 1.97333,-1.12 3.94667,-2.613332 3.94667,-5.119997 0,-1.706666 -1.44,-3.413331 -3.2,-3.413331 -4.16,0 -5.6,2.773331 -6.29333,6.293329 -0.10667,0.533333 -0.32,1.173333 -0.32,1.706666 0,1.706665 0.53333,3.253331 1.01333,4.85333 0.16,0.64 2.08,2.506665 2.66667,2.879998 0.8,0.533333 2.02666,0.8 2.98666,0.8 0.64,0 2.82667,-0.266667 3.30667,-0.746666 3.30666,-1.066666 5.86666,-6.45333 5.86666,-9.706661 z m -9.28,-4.213331 c 0.74667,0 1.33334,0.746666 1.33334,1.439999 0,1.973332 -1.65334,3.306665 -3.25333,4.159998 -0.0533,-0.213333 -0.10667,-0.96 -0.10667,-1.173333 v -0.64 c 0,-1.226665 0.42667,-3.786664 2.02666,-3.786664 z m -3.67999,4.266664 c 0,-0.16 0,-0.32 -0.0533,-0.48 v 0.48 l 0.0533,0.05333 v 0.266667 c -0.0533,-0.106667 -0.0533,-0.213333 -0.0533,-0.32 v 0.586667 l 0.0533,-0.106667 c 0.10666,0.213333 0.16,0.426666 0.16,0.64 0,0.159999 -0.0533,0.319999 -0.0533,0.426666 -0.0533,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.106667 0.0533,-0.426667 -0.10667,-0.48 -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 -0.10667,-0.586666 -0.16,-1.279999 -0.21333,-1.973332 -0.0533,0.426666 -0.0533,0.959999 -0.10667,1.013332 0,-0.16 -0.0533,-0.319999 -0.10667,-0.479999 l 0.0533,-0.106667 c -0.0533,0 -0.10667,-0.05333 -0.10667,-0.16 v -0.05333 h 0.16 v -0.213333 c 0,-0.106667 0,-0.266667 -0.0533,-0.373333 l -0.0533,0.213333 v -0.266667 l -0.0533,-0.106666 v -0.05333 l 0.16,-0.05333 c 0,-0.106667 0,-0.213333 -0.0533,-0.32 l 0.0533,-0.106666 c 0,0 0.0533,0.106666 0.0533,0.213333 0,-0.32 0.0533,-0.64 0.32,-0.906666 v 0.479999 l 0.0533,-0.05333 v 0.213333 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0.0533,0.213333 0.10666,0.426666 0.10666,0.639999 0,0.32 0,0.906666 -0.10666,1.226666 z m 1.54666,7.626662 c -0.0533,0.16 -0.0533,0.32 -0.10666,0.48 l -0.16,-0.16 c 0,-0.373333 0.0533,-0.853333 0.16,-1.173333 0.0533,0.213334 0.10666,0.586667 0.10666,0.8 L 114.766,68.02695 v -0.213334 c 0.16,0.16 0.32,0.32 0.42667,0.48 -0.0533,0.16 -0.0533,0.373333 -0.21334,0.533333 0,0.106667 0.0533,0.213333 0.0533,0.266667 l -0.10667,-0.05333 v -0.479999 c -0.0533,0.159999 -0.0533,0.319999 -0.0533,0.479999 L 114.766,68.933616 c 0.0533,-0.106667 0.0533,-0.16 0.0533,-0.266667 l -0.0533,-0.05333 -0.0533,0.16 c 0,-0.16 0,-0.32 -0.0533,-0.426667 z m -1.76,-1.439999 c -0.0533,-0.32 -0.0533,-0.693333 -0.0533,-1.013333 v -1.866665 l -0.0533,0.106666 v -0.106666 l -0.0533,-0.106667 h 0.10667 v -0.639999 l 0.0533,-0.106667 c 0,0.64 0.0533,1.226666 0.0533,1.866666 v 1.013332 c 0,0.266667 0,0.586667 -0.0533,0.853333 z m 1.17334,1.066666 c 0,-0.106666 0,-0.16 0.0533,-0.266666 v -0.48 l -0.0533,0.05333 c 0,-0.266666 0.0533,-0.479999 0.0533,-0.746666 0.0533,0.05333 0.16,0.32 0.16,0.373333 v 0.64 c 0,0.213333 -0.0533,0.48 -0.10667,0.693333 z M 113.006,59.546955 c 0.0533,-0.16 0,-0.32 -0.10667,-0.48 v 0.213333 c 0,0.32 0,0.64 0.0533,0.959999 0.0533,-0.106666 0.10666,-0.159999 0.10666,-0.266666 l 0.0533,0.16 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,-0.16 -0.10667,-0.373333 -0.10667,-0.533333 z m -0.32,3.519997 c -0.0533,-0.159999 -0.10667,-1.013332 0,-1.173332 z m 0.90667,-5.49333 c 0,0.16 0,0.32 -0.0533,0.426667 l -0.0533,0.05333 v -0.373333 z m -1.49334,6.346663 c 0.0533,0.32 0.0533,0.586667 0.0533,0.906666 l -0.0533,-0.106666 z m 0.69334,-3.999997 c 0,-0.16 0,-0.32 0,-0.426667 0,-0.05333 0,-0.266666 -0.0533,-0.426666 0,0.266666 0,0.533333 0.0533,0.853333 z M 112.206,61.41362 c -0.0533,-0.16 -0.0533,-0.32 -0.0533,-0.48 l 0.0533,-0.106666 z m 0.32,-0.64 c 0.0533,0.106667 0.0533,0.266667 0,0.373334 l -0.0533,-0.213334 z m 0.10667,1.12 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 v 0.32 z m -0.74667,0.426666 c 0,0.106667 0.0533,0.213333 0,0.32 z m 2.08,4.586664 -0.0533,-0.106666 0.0533,-0.05333 z m 0,0.373333 -0.0533,-0.106666 0.0533,-0.05333 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path922" />
+    <path
+       d="m 134.04765,67.86695 c 1.17334,-0.64 1.92,-1.866666 2.4,-3.093332 1.38667,-0.533333 4.69333,-2.399998 4.69333,-4.106664 0,-0.213333 -0.21333,-0.426667 -0.42666,-0.426667 -0.32,0 -0.58667,0.16 -0.74667,0.426667 -0.8,1.226666 -1.92,2.239999 -3.30667,2.773332 0,-2.079999 -0.63999,-4.159998 -2.13333,-5.65333 -0.48,-0.48 -2.50666,-1.706666 -3.2,-1.706666 -0.26666,0 -0.58666,0 -0.85333,0 -0.96,0 -1.97333,0.05333 -2.77333,0.586666 -0.42667,0.266667 -0.53333,0.586667 -0.85333,0.906666 -1.44,1.599999 -2.18667,2.133332 -2.18667,4.533331 0,3.946664 3.09333,6.45333 6.82666,6.45333 0.32,0 2.24,-0.533333 2.56,-0.693333 z m -4.95999,-9.386661 c 0,-0.373334 0.26666,-1.44 0.8,-1.44 2.29333,0 2.93333,4.906664 3.14666,6.506663 -1.97333,-0.746666 -2.88,-1.706665 -3.52,-3.733331 -0.10666,-0.426666 -0.42666,-0.906666 -0.42666,-1.333332 z m 2.55999,8.159995 c -1.65333,0 -1.91999,-2.826665 -2.34666,-3.946665 1.22667,1.546666 1.86666,1.706666 3.68,2.293332 -0.21334,0.746667 -0.32,1.653333 -1.33334,1.653333 z m 2.98667,-2.986665 c 0,-0.426667 -0.10667,-0.8 -0.10667,-1.226666 v -0.106667 c 0.10667,-0.106666 0.0533,-0.799999 0,-0.906666 h -0.0533 v 0.05333 l -0.0533,0.05333 c 0.0533,-0.16 0.10666,-0.373333 0.16,-0.533333 h 0.10666 c 0.0533,0.373333 0.10667,0.746666 0.10667,1.066666 0,0.213333 -0.0533,0.426666 -0.10667,0.586666 0,0.373333 0.10667,0.693333 -0.0533,1.013333 z m -7.2,0.106666 c 0.21334,0.213334 0.16,0.64 0.16,0.906667 v 0.853332 c -0.10666,-0.373333 -0.16,-0.799999 -0.16,-1.226666 0.10667,-0.106666 -0.0533,-0.319999 -0.0533,-0.426666 0,-0.05333 0.0533,-0.106667 0.0533,-0.106667 z m 7.25333,1.066666 c 0,-0.213333 0.0533,-1.333332 0.10667,-1.439999 0,0.05333 0.0533,0.106667 0.0533,0.16 0,-0.05333 0,-0.05333 0.0533,-0.106666 v 0.213333 c 0,0.373333 -0.0533,0.693333 -0.10667,1.013333 z m 0.26667,-0.693333 c 0,-0.266666 0.0533,-0.533333 -0.0533,-0.799999 0.10666,-0.426666 0.0533,-0.959999 0.0533,-1.386666 0.0533,0.213333 0.10667,0.426667 0.10667,0.64 0,0.373333 0,1.386666 -0.10667,1.546665 z m -0.16,-0.799999 v -0.05333 l 0.0533,0.05333 z m -0.21333,-2.079999 0.0533,0.05333 v -0.05333 z m 0.0533,1.439999 v -0.106666 z m -7.14666,-0.639999 v -0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path924" />
+    <path
+       d="m 155.71431,60.400287 c 0,-1.653332 -0.21334,-5.386663 -0.8,-6.879996 -0.16,-0.373333 0,-0.853332 -0.16,-1.226665 -0.26667,-0.8 -0.42667,-1.599999 -0.74667,-2.399999 -1.17333,-2.986665 -3.46666,-8.159995 -7.30666,-8.159995 -1.44,0 -3.30666,0.746666 -4,2.133332 -0.26666,-0.266667 -0.69333,-0.533333 -1.06666,-0.533333 -1.97334,0 -1.86667,0.906666 -1.86667,2.346665 0,0.426667 -0.10667,0.853333 -0.10667,1.279999 v 11.679993 c 0,0.853333 0.16,1.706666 0.21334,2.559999 0.16,2.613332 0.26666,5.119997 0.58666,7.733329 -0.26666,0.16 -0.48,0.426666 -0.48,0.746666 0,0.48 0.37334,0.533333 0.69334,0.799999 0.42666,1.333333 -0.16,1.653333 1.33333,2.239999 0.37333,0.16 0.8,0.32 1.22667,0.32 1.43999,0 2.61333,-0.32 2.61333,-2.026666 6.34666,-0.799999 9.86666,-4.21333 9.86666,-10.613327 z m -9.81333,9.013328 c 0,-2.079998 0.0533,-4.21333 -0.0533,-6.293329 -0.0533,-0.586667 -0.16,-1.12 -0.16,-1.706666 0,-2.933331 -0.64,-5.919996 -0.64,-8.853328 0,-1.546666 -0.21334,-3.093331 -0.21334,-4.639997 0,-1.226666 -0.0533,-4.853331 1.70667,-4.853331 0.64,0 1.12,0.64 1.44,1.173333 2.24,3.679998 3.25333,10.399994 3.30666,14.613325 0,0.426666 0.16,0.906666 0.16,1.333332 0,3.039998 -0.53333,6.45333 -3.14666,8.266662 -0.53333,0.373333 -0.53333,0.586666 -1.17333,0.746666 -0.42667,0.106667 -0.85334,0.05333 -1.22667,0.213333 z m -4.42666,-6.879996 c 0,-1.333332 -0.21334,-2.613331 -0.21334,-3.946664 v -4.85333 c 0.37334,0.639999 0.37334,3.679997 0.37334,4.479997 0,1.226666 -0.0533,2.506665 -0.10667,3.733331 h 0.0533 v 0.48 z m 2.4,-4.266664 v 1.066666 l -0.0533,0.05333 h 0.0533 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.319999 -0.16,0.319999 0,-0.266666 -0.0533,-0.533333 -0.0533,-0.799999 0,-0.853333 0.26667,-2.079999 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.10666,-0.213333 0.10666,-0.479999 0.16,-0.693333 v 0.106667 l 0.0533,0.106667 c 0,0.693332 0.16,1.493332 0.16,2.186665 l -0.0533,0.05333 h 0.0533 c 0,0.106667 0.0533,0.16 0.0533,0.266667 v 0.373333 l -0.10666,0.106666 0.0533,-0.159999 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 l -0.0533,-0.16 z m -2.13334,6.77333 0.0533,0.05333 v -0.05333 c 0.0533,0.106666 0.0533,0.213333 0.16,0.319999 -0.16,-0.586666 0.0533,-1.119999 0.0533,-1.653332 l 0.0533,-2.666665 c 0.10667,0.106667 0.10667,0.32 0.10667,0.48 0,0.16 -0.0533,0.373333 0,0.533333 0.16,-0.32 0.0533,-0.426667 0.0533,-0.746666 0.0533,0.426666 0.0533,0.906666 0.0533,1.333332 0,1.013333 -0.16,2.079999 -0.16,3.093332 l -0.0533,0.05333 h -0.0533 l -0.10667,-0.16 v 0.426666 l -0.0533,-0.266666 -0.0533,0.16 -0.0533,-0.106667 z m -0.42666,-12.10666 c -0.16,-1.493332 0,-3.306665 0,-4.799997 l -0.0533,0.05333 v -0.479999 c 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 l 0.10666,-0.373333 c 0,0.586667 -0.16,1.066666 0.0533,1.653333 0.0533,-0.16 0.0533,-0.266667 0.0533,-0.426667 l 0.0533,0.05333 c 0,0.159999 0,0.319999 0.0533,0.479999 -0.0533,0.106667 -0.0533,0.16 -0.0533,0.266667 l -0.0533,-0.213333 -0.0533,0.106666 c 0,0.106667 0,0.213333 0.0533,0.32 -0.10667,0.05333 0,0.426666 0,0.533333 0,-0.106667 0.0533,-0.32 0.10667,-0.426666 0,0.159999 0,0.373333 0.0533,0.533333 -0.0533,0.16 -0.0533,0.373333 -0.0533,0.533333 l -0.10667,-0.373333 c -0.0533,0.266666 -0.0533,0.586666 -0.0533,0.853332 v 0.05333 h 0.0533 l 0.0533,-0.106667 c 0,0.05333 0,0.16 0.0533,0.213333 -0.0533,0.16 -0.0533,0.266667 -0.0533,0.426667 l -0.0533,0.106666 -0.0533,-0.266666 c -0.10667,0.479999 -0.0533,0.959999 -0.0533,1.439999 l -0.10666,-0.373333 -0.0533,0.32 v -0.32 z m 12.58666,8.426662 c -0.21334,-0.906666 0,-2.079999 -0.26667,-2.879998 0.10667,-0.533333 0.0533,-1.013333 0.0533,-1.546666 0.16,0.959999 0,1.973332 0.16,2.933331 0,-0.106666 0.0533,-0.213333 0.0533,-0.319999 0,-0.48 -0.10667,-1.013333 -0.10667,-1.546666 0,-0.05333 0,-0.266667 0.0533,-0.266667 0.0533,0.746667 0.10667,1.493333 0.10667,2.239999 0,0.213333 -0.0533,0.48 -0.0533,0.693333 l -0.0533,-0.05333 c 0.0533,0.213333 0.0533,0.479999 0.0533,0.746666 z m -0.74667,2.986665 c -0.0533,-0.373333 -0.10667,-0.746667 -0.10667,-1.12 v -0.106666 l 0.0533,-0.05333 h -0.0533 V 61.89362 c 0,-0.373333 0,-0.693333 0.0533,-1.013333 0,0.853333 0.16,1.759999 0.16,2.613332 0,0.213333 0.0533,0.693333 -0.10667,0.853333 z m -1.22667,-15.253325 c 0.16,0.373334 0.21334,0.533333 0.21334,0.96 v 1.013333 c 0,0.16 0,0.319999 -0.0533,0.479999 -0.10666,-0.266666 -0.21333,-0.639999 -0.21333,-0.959999 v -0.8 l 0.0533,0.16 z m 0.53334,4.426664 c -0.0533,-0.266666 -0.16,-0.799999 -0.16,-1.066666 v -2.559998 c 0.0533,0.106667 0.16,0.373333 0.16,0.426666 z m 1.17333,4.959998 c 0,0.426666 -0.0533,0.853332 -0.0533,1.279999 -0.10667,-0.693333 -0.16,-1.386666 -0.16,-2.079999 0,-0.213333 0,-0.8 0.21333,-0.959999 -0.0533,0.586666 0,1.173332 0,1.759999 z m -0.74667,-6.879996 c 0,0.213333 -0.0533,0.479999 -0.16,0.693333 0.0533,0.159999 0.0533,0.319999 0.0533,0.479999 l -0.10667,0.16 c -0.0533,-0.799999 -0.10667,-1.546666 -0.10667,-2.346665 0.16,0.266666 0.16,0.64 0.26667,0.959999 z m -11.62666,-4.586664 c 0,-0.693333 0.10667,-1.333333 0.10667,-2.026666 l 0.10667,-0.05333 0.10666,0.16 c -0.0533,0.639999 -0.0533,1.653332 -0.32,2.239999 z m 2.13334,5.119997 -0.0533,-0.213334 c -0.0533,0.16 0,0.266667 0,0.426667 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493333 -0.16,-1.813332 0,-0.106667 0.0533,-0.213334 0.0533,-0.32 v 0.266666 c 0,-0.32 -0.0533,-1.439999 0.10666,-1.706665 0,0.853332 0.0533,1.653332 0.0533,2.506665 z m 0.53333,13.439992 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.64 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373333 0.10667,0.746667 0.10667,1.12 z m 9.75999,-9.226662 c 0,-0.213333 0,-0.479999 0.0533,-0.639999 0,0.48 0.10666,0.959999 0.10666,1.493332 0,0.16 0,0.32 0,0.48 0,-0.32 -0.10666,-0.64 -0.10666,-0.959999 l -0.0533,0.159999 v -0.213333 h 0.0533 l -0.0533,-0.266666 v 0.106666 c 0,-0.05333 0,-0.106666 0,-0.16 z m -11.78666,5.65333 c -0.0533,-0.319999 -0.0533,-0.639999 -0.0533,-0.959999 v -0.373333 c 0.0533,0.106666 0.10667,0.266666 0.10667,0.373333 0,0.32 -0.0533,0.64 -0.0533,0.959999 z m 0.53334,4.053331 c -0.0533,-0.106666 -0.0533,-0.266666 -0.0533,-0.373333 l -0.0533,0.106667 h -0.10667 c 0,-0.16 0.0533,-0.373333 0.10667,-0.533333 h 0.16 c -0.0533,0.32 0,0.586666 0,0.853333 z m 11.14666,-4.53333 c 0.0533,0.426666 0.16,0.853333 0.26666,1.226666 l -0.10666,0.373333 c -0.0533,-0.373333 -0.16,-0.853333 -0.16,-1.226666 z m -1.22667,4.799997 c -0.10667,0.05333 -0.10667,0.16 -0.16,0.266666 v -0.639999 l 0.10667,-0.266667 c 0,0.213333 0.0533,0.426667 0.0533,0.64 z m 0.10667,-1.333333 c 0,0.373333 0.0533,0.746667 0.0533,1.12 -0.0533,0 -0.0533,0.05333 -0.0533,0.106666 -0.10667,-0.32 -0.0533,-0.799999 -0.0533,-1.119999 z m -9.44,-15.146657 c 0.10667,0.479999 0.10667,0.959999 0,1.386666 z m 10.61333,5.01333 c 0.0533,0.213333 0.10667,0.373333 0.10667,0.586666 v 0.16 l -0.10667,0.05333 c 0,-0.266667 0,-0.586667 -0.0533,-0.853333 z m -0.0533,5.066664 c 0.0533,0.373333 0.10666,0.906666 0.10666,1.279999 l -0.0533,-0.106667 -0.0533,0.106667 v -0.533333 0.05333 z m -0.10667,1.119999 c -0.10667,-0.266667 -0.0533,-0.853333 -0.0533,-1.119999 0.0533,0.213333 0.10666,0.906666 0.0533,1.119999 z m -0.26667,3.893331 -0.10666,0.32 v -0.16 c 0,-0.16 0,-0.32 0.0533,-0.426666 z m -10.77332,1.866666 c 0,-0.05333 -0.10667,-0.213334 -0.10667,-0.266667 0,-0.106667 0.0533,-0.213333 0.10667,-0.32 z m -0.85334,-3.306665 c -0.0533,-0.266667 -0.0533,-0.533333 0,-0.8 z m 11.41333,-1.279999 v 0.693333 c -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 z m -9.65333,-9.653328 v 0.106667 l 0.10667,-0.05333 v 0.05333 l -0.0533,0.213333 -0.10667,-0.05333 c 0.0533,-0.106667 0,-0.16 0,-0.266667 z m 10.61333,5.279997 v -0.693333 c 0.0533,0.213333 0.0533,0.586666 0.0533,0.8 z m -2.98667,-10.559994 0.0533,-0.213333 0.0533,0.05333 v 0.373333 z m -9.49332,0.853333 c 0.10666,0.106667 0.0533,0.426666 0.0533,0.586666 -0.0533,-0.213333 -0.0533,-0.373333 -0.0533,-0.586666 z m 0,4.85333 c 0,-0.213333 0,-0.426666 0.0533,-0.586666 0,0.213333 0.0533,0.426667 -0.0533,0.586666 z m 0.69333,15.893324 c 0,0.16 -0.0533,0.32 0,0.48 0,-0.16 0.0533,-0.32 0,-0.48 z m -0.53333,-7.946662 c 0,0.213334 0,0.48 -0.0533,0.693333 0,-0.213333 0,-0.479999 0.0533,-0.693333 z m 0.58666,1.653333 c 0,-0.106667 -0.0533,-0.106667 -0.10666,-0.16 v 0.32 z m 1.49334,-0.48 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m -1.49334,6.186663 c -0.0533,0.213333 -0.0533,0.373333 -0.0533,0.586666 0.0533,-0.213333 0.0533,-0.373333 0.0533,-0.586666 z m 1.76,-0.586666 v -0.05333 l 0.0533,-0.106666 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213333 -0.0533,0.32 z m -1.44,-0.853333 c 0,-0.16 -0.0533,-0.266667 -0.0533,-0.426667 0.10667,0.106667 0.10667,0.266667 0.0533,0.426667 z m -0.37333,-4.639997 c 0.0533,0.05333 0.10667,0 0.0533,-0.05333 -0.0533,0.05333 -0.10666,0.16 -0.10666,0.213334 h 0.10666 z m 10.82666,-2.826665 c -0.0533,-0.106667 -0.0533,-0.213334 -0.0533,-0.32 l 0.0533,-0.106667 z m 0.53333,3.733331 c 0,-0.16 -0.0533,-0.266667 0,-0.426667 0,0.16 0.0533,0.266667 0,0.426667 z m -10.71999,7.146662 -0.0533,0.106667 0.0533,0.16 z m 10.18666,-8.959994 -0.0533,0.05333 v -0.213333 l 0.0533,-0.05333 z m 0.58667,-4.906664 c -0.0533,-0.106667 -0.0533,-0.213333 0,-0.32 0.0533,0.106667 0,0.213333 0,0.32 z m -11.52,9.599994 v 0.16 l -0.0533,0.05333 0.0533,-0.16 c -0.0533,0.05333 -0.10666,0 -0.10666,-0.05333 z m 0.64,2.026666 c 0.0533,-0.106667 0.0533,-0.213334 0,-0.32 z m 1.17334,-1.6 c 0,0 -0.0533,-0.05333 -0.0533,-0.106666 0,0 0.0533,-0.05333 0.0533,-0.106667 l 0.0533,0.106667 c 0,0.05333 0,0.05333 -0.0533,0.106666 z m 9.11999,-4.53333 v 0.16 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.16 z m 0,1.439999 h -0.0533 v -0.16 h 0.0533 z m -9.22666,1.013333 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.10667,-4.053331 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.53333,-7.093329 c 0,-0.05333 0,-0.16 0.0533,-0.213334 0,0.05333 0,0.16 -0.0533,0.213334 z m -1.22667,12.906659 c 0.0533,0.106666 0.0533,0.213333 0,0.266666 z m 11.57333,-11.359994 c 0,0.106667 0.0533,0.213334 0,0.32 z m -10.13333,15.039991 c 0,0.05333 0,0.106667 -0.0533,0.16 v -0.106666 z m 0.37334,-10.50666 0.0533,-0.05333 -0.0533,-0.106667 z m -1.12,8.533328 -0.0533,-0.106666 v 0.16 z m 1.06666,3.573332 v -0.16 l 0.0533,0.16 z m 9.06666,-17.813323 v 0.16 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.106667 z m -9.54666,-6.026663 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m -1.01333,15.573324 c 0,-0.05333 0,-0.106667 -0.0533,-0.16 0,0.05333 0,0.106666 0.0533,0.16 z m 1.01333,-14.986658 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 9.92,16.426657 0.0533,-0.106667 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.37333,-4.853331 c 0,0 -0.0533,0.05333 -0.10667,0.05333 l 0.0533,-0.05333 z m -11.67999,5.546664 c 0,0 -0.0533,0.05333 -0.0533,0.05333 0,0 0.0533,0.05333 0.0533,0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,-2.933332 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 0.90667,3.093332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -0.16,1.706665 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m 0.96,-16.47999 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -1.70667,13.333326 v -0.106667 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106667 z m 1.81333,6.186663 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.53334,-3.573332 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.90667,2.133332 c 0.0533,0 0.0533,0.05333 0.0533,0.106667 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106667 z m -1.28,-8.479995 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.69333,5.97333 h -0.0533 v -0.05333 z m -0.8,-16.906656 h -0.0533 v 0.05333 z m 2.08,11.359993 0.0533,-0.05333 v 0.05333 z m -0.0533,10.18666 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106666 z M 143.50098,55.76029 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m -1.97333,-3.999997 v 0.05333 l 0.0533,-0.05333 z m 2.13333,8.959994 0.0533,0.05333 h -0.0533 z m -0.90666,7.519996 -0.0533,0.05333 h 0.0533 z m 11.14666,-8.266662 c -0.0533,0.05333 -0.0533,0.05333 -0.0533,0.106667 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106667 z m -12.58666,-9.919994 h 0.0533 v -0.05333 z m 0.53333,10.133327 c 0,0 0.0533,0.05333 0.0533,0.05333 0,0 -0.0533,0.05333 -0.0533,0.05333 z m 1.12,8.213329 h -0.0533 v -0.05333 z m 10.45333,-8.959995 h -0.0533 l 0.0533,-0.05333 z m 0,0.106667 0.0533,0.05333 h -0.0533 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path926" />
+    <path
+       d="m 171.9068,62.533619 c 0,-3.199998 -2.82667,-4.586663 -5.38667,-5.439996 1.65334,-1.653333 2.82667,-3.999998 2.82667,-6.399996 0,-1.226666 0.0533,-2.719999 -0.58667,-3.839998 -0.90666,-1.546666 -2.08,-3.039998 -4.10666,-3.039998 -1.01333,0 -1.86667,0.319999 -2.61333,0.959999 -0.21334,-1.546666 0.10666,-3.039998 -2.08,-3.039998 -1.81333,0 -1.97333,0.906666 -1.97333,2.453332 0,1.813332 0.16,3.679998 0.16,5.546663 0,3.039998 0.16,6.079996 0.16,9.173328 0,1.919999 0.21333,3.839998 0.21333,5.759997 0,0.586666 0.16,1.119999 0.16,1.706665 0,0.16 -0.0533,0.266667 -0.10667,0.426667 -0.53333,0.159999 -0.74666,0.586666 -0.74666,1.119999 0,0.426666 0.16,1.173333 0.69333,1.226666 l 0.16,-0.05333 v 0.266666 c 0,1.013333 -0.37333,2.773332 0.8,3.253331 0.37333,0.16 0.85333,0.426667 1.22666,0.426667 0.8,0 3.2,-0.106667 3.2,-1.279999 -0.32,-0.693333 0,-1.546666 -0.21333,-2.239999 3.84,0 8.21333,-2.773332 8.21333,-6.986663 z m -7.62667,-4.426664 c 2.24,0 5.06667,1.546666 5.06667,4.053331 0,2.826665 -3.52,4.319998 -5.81333,4.639998 -0.10667,-0.8 -0.26667,-1.653333 -0.26667,-2.453332 0,-1.546666 -0.10666,-4.693331 -0.32,-6.186663 0.16,-0.05333 0.32,-0.05333 0.48,-0.05333 z M 164.8668,45.25363 c 1.33333,0 1.97333,1.173332 1.97333,2.453332 0,3.199998 -1.70666,6.559996 -3.94666,8.799994 -0.21333,-0.426666 -0.53333,-6.719996 -0.53333,-7.413329 0,-1.386665 0.37333,-2.133332 1.33333,-3.093331 0.21333,-0.213333 0.85333,-0.746666 1.17333,-0.746666 z m -3.62666,14.506658 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.32 -0.16,0.32 0,-0.266667 -0.0533,-0.533333 -0.0533,-0.8 0,-0.959999 0.26667,-1.973332 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.21333,-0.373333 0.10666,-0.853332 0.21333,-1.279999 0,0.16 0,0.32 0,0.48 l 0.0533,-0.106667 c 0.0533,0.48 0.0533,0.96 0.0533,1.439999 l 0.0533,0.05333 0.0533,-0.213334 c 0.10667,0.533333 0.16,1.013333 0.16,1.546666 -0.0533,0.213333 0.0533,0.693333 0.0533,0.906666 l -0.0533,-0.05333 c 0.0533,0.106667 0.0533,0.373333 0.0533,0.533333 -0.0533,-0.213333 -0.10667,-0.426666 -0.21334,-0.586666 0.10667,-0.32 0.16,-1.599999 -0.0533,-1.813333 0,0.373334 0,0.693333 -0.0533,1.066667 l 0.0533,-0.213334 c 0.0533,0.213334 0.0533,0.426667 0.0533,0.64 l -0.10666,0.106667 0.0533,-0.16 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 v 0.853332 l -0.0533,0.05333 z m -0.32,-8.693328 c -0.0533,0.479999 0.0533,0.906666 0.0533,1.386665 l 0.0533,0.106667 v 0.8 l -0.0533,0.05333 v -0.533333 c -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 l -0.0533,0.05333 c 0,-0.373333 -0.0533,-0.906666 0.0533,-1.279999 h -0.0533 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 l -0.0533,-0.05333 c -0.16,0.16 -0.10666,0.586667 -0.10666,0.8 l -0.0533,-0.213333 v 0.426666 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493332 -0.16,-1.813332 0,-0.48 0.10667,-0.96 0.10667,-1.439999 l 0.0533,0.106666 0.0533,-0.106666 c 0,0.479999 0.16,0.853332 0.16,1.279999 v 0.266666 l -0.0533,0.05333 z m -0.64,20.159988 c 0,-0.32 -0.0533,-0.586667 -0.0533,-0.906667 v -0.479999 l 0.0533,-0.106667 v 0.213333 l 0.0533,-0.106666 v -0.106667 l 0.0533,0.05333 v 0.853333 c 0.0533,-0.05333 0.0533,-0.16 0.0533,-0.213333 0.0533,0.32 0.0533,0.586666 0.0533,0.906666 z m 1.01333,-5.279997 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.639999 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373334 0.10667,0.746667 0.10667,1.12 z m -1.28,1.813332 c 0,0.16 0.26667,1.119999 0.32,1.173333 l -0.0533,0.106666 c 0,-0.213333 -0.10667,-0.533333 -0.21334,-0.693333 v -0.266666 0.266666 c -0.0533,-0.106666 -0.10666,-0.266666 -0.21333,-0.319999 l 0.10667,-0.213334 v -0.05333 z m 0.37333,-17.546656 c 0.10667,0.48 0.10667,0.959999 0,1.386666 z m 0.32,-1.386666 v 0.106667 h -0.0533 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0,-0.106667 0.10666,-0.16 0.16,-0.16 v 0.213333 z m -0.0533,4.373331 c 0.0533,-0.106667 0,-0.16 0,-0.266667 h 0.0533 v 0.106667 l 0.10667,-0.05333 -0.0533,0.266667 z m 0.69333,6.559996 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,0.05333 0.0533,0.159999 0.0533,0.213333 v 0.106666 l -0.10667,0.05333 z m 4.58667,-4.426664 c 0.0533,-0.213333 0.21333,-0.373333 0.32,-0.533333 v 0.106666 z m -4.85333,7.146662 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m 0.26666,5.546664 0.0533,-0.106667 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213334 -0.0533,0.32 l -0.0533,-0.05333 z m -0.32,-14.399992 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.213333 l 0.0533,0.05333 z m 0.26667,13.066659 c 0,0.05333 0,0.05333 -0.0533,0.106667 0,0 -0.0533,-0.05333 -0.0533,-0.106667 0,0 0.0533,-0.05333 0.0533,-0.106667 z m 0.10667,-6.45333 -0.0533,-0.213333 c 0.0533,0.05333 0.0533,0.106667 0.10667,0.16 z m -0.26667,-4.959997 c -0.0533,-0.106666 -0.0533,-0.213333 -0.0533,-0.319999 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 z m -0.48,-5.919996 c 0,-0.106667 0,-0.16 0.0533,-0.266667 0,0.106667 0,0.16 -0.0533,0.266667 z m 0.48,15.359991 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.64,-11.14666 c 0,-0.05333 0,-0.16 0.0533,-0.213333 0,0.05333 0,0.159999 -0.0533,0.213333 z m 0.53333,7.093329 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.0533,-6.45333 v -0.16 l 0.0533,0.05333 z m -0.26667,15.946658 c 0,0.05333 0,0.106666 -0.0533,0.159999 v -0.106666 z m 0.42667,-10.559994 -0.0533,-0.106667 v 0.16 z m -0.85333,8.639995 c 0,0 -0.0533,0 -0.0533,-0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.26666,-20.319988 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m 0,0.586666 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.16,22.026654 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0,-21.439988 v -0.106666 l 0.0533,0.16 z m 0.53334,17.866656 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.64,-15.946657 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,18.07999 c 0.0533,0 0.0533,0.05333 0.0533,0.106666 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106666 z m 0,0.373333 h -0.0533 v -0.106667 z m 0.53333,-13.333326 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m 0.21334,5.013331 h -0.0533 v -0.05333 z m 0.32,-1.813333 0.0533,0.05333 h -0.0533 z m -1.06667,9.439995 h -0.0533 v -0.05333 z m 0.74667,-7.733329 h -0.0533 l 0.0533,-0.05333 z m -0.10667,10.186661 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path928" />
+  </g>
+</svg>
diff --git a/common/static/img/logo_square.jpg b/common/static/img/logo_square.jpg
index 37ed7679cdf0eb96115865560d3386e3af8970c8..073cc571293d57ac64773cb0251cd4f38f4d2839 100644
GIT binary patch
literal 32105
zcmeFY2UOEd*Do3bL{LE#P^1JYB7`En6O{)L5mBkqA_7tZ(xrt&u^_z(2ndlbQX;(*
z=}ka-@4bf>LK4FL`@HQt_j|v4&pGQ`=e}#*cNUWj$;@Q-{?F|F+htBhPG&$C?rCUg
zfT*ZIR3cO&AkfJys9W9J{y7MwqXQBKfk0<Kr>QQ2sDT#H1$em%qW!ZC0vS?q{Jm{K
zb>pvnP62%+K)@@A6{ybx?Ljnu?T-N7e>Z_&`E#Z}-yhn0+c-K|dN{lBJalvBkra~<
zlLEe{iKO{+4i=zY^o8mlzyIab-{w07`a=DWHqZ|wc~eqC@uq^JgcOg2q@t9xqJ$(s
z4}G1Thlh)zxcCcqF-vRbXEtJ1&Q9XqmM-EFVmHM>%200?ODnjI2hTGbTl*I(yxY}S
zULJdE6<#A5otru?YBqNEcYWP#^n4%aTlvDR6s&onsyxcxir!8xPBtEvJl;-@FWeQq
zRe1lLToGvh{#cxs=g%%4a1~x-orgSX&Tck5vSK&IZt?=7-K?K0KK?`FuVaD#RCxco
zNG~rhF)t}GXE$4M2?YfO@tcz3l9Hl84^elY7ao@0qA%R}{x-uOHttq#_AVaw&M$a=
z&uIC~+0#RX7x?ABEXm2`@6-NM+JAR+QB-rYvGlO{1E?yzzd0y+Q$bWhO8;*h{7WYt
zfFx^656gerO<wV)q{)An%-K!f+1XKr_n$pmUU7hY@xRjlyZLuUs`C81{F?*+=D@!>
z@NW+Mn*;ym!2ka^@DC_v^8&zMUI5Snoos@hKD2Z8aCWzIcHxl{y9tuNqoqUh8&iTn
zf}B()AZiW)O-=xBo&ufXJf#Xc^Bcq5RFIJ3JOu@vmXMT^22kLiV+8&jb72uqn_`r_
zGy3GzW5JXQki(N7Am%f`U=TGGFX$9A6*V)}NfQVRU{qSFKg*xPfETJ$)HJlG>CVtI
zoCP|RUI3k<qNYAYLrqKjdpT78!1o{;X4;F_ByOL+^vIHq*Of)`byVsZzB`5WtdDzf
z{8G={0_YjoE?;54DsWx!hLEtdjI5lzf}+|V>KdANweIQZ8yFfHKQXbgws~%AXYb(d
z;rY_b+sF6K+js8+KLiCw$9#;9i~p36n3kTAnU(!D=UY*6NoiSmMP*e(V^ecWYg>Cq
zU;n`1(D2CU*zDZ=!s62M%IezA?%w_Z{*Z7){7n~trT;<}@cI|Z{%>?K19Y9Dp`oUs
z`%M?sDX-szGt<yslQ@0x_9Hq=*Gs&Tug|dDiApW3r{|M;jAMP~*2}=gFFh-;^P99k
zDf^!zEZ~2OvcD7d54y%dYM@hp7MfG1Xn=Lo(9+QYg^r%?ccDK^|7SV-*K+>P!t}c^
z|Gk_5lK>Bt8W>Cm{4t+nIQRGO{-+x!lYmLWb20`xM@<E6CTeC76m(2Z3%&vR|FBY@
z{Q4JIL+`)98v6bP*3kbiu!ezufi(>NKdm9yXSVEJf!GyZ9+97|AgWt5dyP(K&CG88
zaqCG!IOtT3ramFT*Yjh%HuXjnCyV9J^h(u~spMk_M~j7I6719HXmp&NH`lqhs7rpn
zOzZt`Fpx{3PP_fSoHB*yt8WqeHHSnzJ6Y?#JLb&18Ps*r%y%&AsN>!_e;DZeZZM~V
zn?H2lm7*G0>MxqU&Zf8+JZ&tikGs8=UBNe0g&T=Ac(Ud3C@(ATP)7J{NG@DH^w5p}
zu3~Xy`kqV@25v|^op3$fwR?)$ubBG{8IMUQ4Rd}pEEYDDdd-R~thyGV&h@qFnU%KA
zh(TUzw%$q);eL%cwv*)eg!mch-R!}3D0}EiziIvC!oKyj*JO2sp9xX6^6QGxops$v
z3{(<a)%YuT@%_{7?esHq$;S)+R_fMvzRh*jS(-VfE}WL}Ro~Z`j#f<$R<jbTlrHI*
z)3Q9eZ>i4mkm|pw(wz+bS5NQ%<;m4~@5F~@4KK;Qw9ZWArc=8HZkn2UL)=%knHMTO
z3QJB9dLH=tSDWUJT=^GDt-Ru{t8oF!jf0bQeb29O<VITXj6y(pjmjLyHskc!T)mIV
zTI?(HWPc`y`n1p1b6f9|9<1z7f8gdV9Tv?qso!H~+{pg=;`9~YaFpVn@QrUdj|SqU
zIF}ZVK0Q)rrAg_+r7YcvFS90p$#~^^DH5g3fEV5azrU&<m#AO$vGMfk`=2KuSE)CI
zXn23_WJA{1mgVf*GCF3!I#y&DZlt~X@2b#~;eW=^{{zenzVYSMU+a0L^Be|)R8Fc>
zTCd~<PTx{p{({lKZ8s`a3YelVJd<x#z4y7;@1i{Qiu-;6_3dFy4HO~n7{SHGH0iky
z{e{P5mgu}_g%z5vv(2ZclG>);SAYMQqeB|UE85-pTs|=vE<YFc>;tU<l|Cbg`U?y3
z2F7grzl(!^n+N)zmED~?0j*!YoLNrcurryP4d6ZX^QQ}lb+0wF#zqz^QCut_@(ZWP
zeui=#zYiWxuYu3Ad#=B+vc5Ok@}~%420jj2&Zf3_0AcOgQnL!EMLNG!0*;g!-OWep
zETI2b4Ts)fVBScA#VEI;BDiXO1(mvKT%s5H+5A_({Uq4fE^#s(`>cFOv{h5>Fsk;v
zqfv(<W3B%zY1Yr$du0W=j!C5eQb#InovmssB{3}JPC1R7f#m>_b<?s?_*?PB{*56$
zL%+n5N9vv&?^qt)0+#XLQo$$xgMJQJTbt^b+}fJ73YHPs0wnN6zgD^R^*$p@Pfur?
z7l&Ri3Y)LSd0D*zZNXpTTAT7OHC1I)>ECLYjfk{{uIqbo2afmew7w(XBD{8;tA1nk
z%tYzT_@c%8JJ2BPm(2r=T)5V?D4U?*qR5U8m)l%tWbjwS*dI5Re|VWR-Z_j#AE5<8
z=j1r=7<+n)l8=?Zdfv-FrtPQmR@zBDwIUF*MPB;wfEV##M;MO@gfge^(7@B-B5X$*
z%sr=`Lh;UzroUzr{`nAs*|2c#``jBHvM6+-%GU>R*Q1*(C;OG}1>eA&`w1Lv(C;cd
zm>IT6u2T=yl{x`=QVH(oGcm0cuM;zTJ?QM9QssvaJ)yJPum|=g>hDAB%6t))ES@SO
z#KJ9jF4H?t&lhb|v6oeR!Ahuxvvaj6wj<-<?dvGf(E_)vj@<?rFJ3U{1XMMXa|~Z!
z`#D$=1~n;>^?hqOun%p@9@Mom*Xx{jc3pE45iYOliT;|8xnNg*^>ohlWQB>#OB$5j
zlt8lox>17#OXZHhl&O#t`|&~wb^}v+4C&TiSZ<5U=6(CQ(l?r*)*wX)U+zj0vS&Y<
zt_E*musNGBjd4@c(ecduZ#3Z&r3SuKI>p6h-^;X>q?uc<G<?mK*|fy%WP+u7GqrB}
z0DH{*bgH^HrX^zR(yZ<eQEnBZxd&NgJuVy@Wv)VTYcm*_n7}f1>hrNzGi>avN4*88
zdQ)YE(>aUY&h<uKhPNvdw~j>@AtxZSX!eg_k{!I5DSi%K;P9}Dgq4b}gPy;=JD7|<
z7&(5OR<k#&+0?^`ct-HcB;UoQOB4-W+cV&AAtY-(;YD1O)0oMrZ_#9DUGeO^QPJL_
zt?$utPwSqz2FoLLEzJ&SsnMqCtnO$?Bio@-*|OeHDet^)e-yd4UZU;^jLzEOF6SxZ
z&s2xO6bFJ8h2b6PmV;txGkNM#pQnmsx0I_F?OQo}e%ZBKV*N)k!c>uGOZ?o!X%WjO
zAX=K8=IJn~gNiRbXK7vVcYDJQ-3eo>^E~9|K1}m4ixW^i|Ni*d7Q!3R2`wRs)rS{m
zZc23GYu9eCaoDwO2NOTdxDXuahJ+gvMxnKsJ=JH%6B>Nr17E#GBls5f2%G^fZrzl^
zE#sqb6N#;Fto{7Pb<$2iI+Z^v%Ac=z&(1q?71VIQJ>3RY8*V%SQDdL!o+qCsOy?qu
zaN))B{o9*M<>kFKk-4WHtX?xr@{#0aIXd0UV<mF8ofJ@qV*4tL-q_mNTV3!Y7(MAf
z0ofVGFE0UHbLaY}h~QDD$iqMSyE{I-Dwg5|(eX$7Qav_Wa3-^cv{Y@y%tGnC!4E2;
zIYcey@Jo2<XKApaGc@-Iq>ghx$93632dAoBylp>Vt_qEvfN;yQ;upT}+dRn~Io9nb
zTh5AFOf79|4T`)-E2b^54wzG5yFkU?X7J%5Nbs`<rbTy5m%?B>XPW<PxdB!@9B*qy
zeI>~(MJCRtZKkxw?Nn5YPorY-&3Y=xZUY2!nZP=};dS|{&!ER{k<Z20y^XD5TCn9J
zmv1hTw+iz*g*I@`wnh9LfpO)M^EZdMUuITs7A~K5^gFUFggpvuw|nT8Yp^G9jOa;d
zvXxo?+LuvjN{LV1_CoR<*JR?n2|cdg2)!P6(p-_{UP7SI6HuqFkQo&-h0+x`R2Ola
z1f9vOlfY4O>E2PGSbSM5<V*{O)AGe;@eg~$n>4-QghxE=3huk;3G7Cf97&GEv9aSH
z(N2Pn;+Y^F<K@qPTD_Q(2(=2Pm|@}5uK{*_p~yQRnG5e!84Sss=l_h}f70GyeX#pk
z8nOFw;ULe&6L%CS!%))aFFH={D*PzHjuQ1X;4pWdCS!Ba1J7f>tyV21IOnbOw|d%p
zUF@R4efM4nYgx3htf%1&5I#n71AA5a1Y|!R1~!3>F1N%B<Y7YEQ?>a}ITecwW$i1`
zyKPFUx7v|Vm)zx@5iLu&;^QOLmBa7G6?1mq3XhI%VuDXVFHS%asse<wIDLmYhN!1Q
zGJXsFPA?b&Z$kLm`M>2c#4#ip@yvXem&cSscHdAK+VW384A_SJ5%|S7gdc-GPVxiI
z{ci^c+kZOTOJU2H+1Nnpjv@z8C7$o|swT1Fh+DQs(?QNlalF1WjeOGTs8L9S10@`5
zg9<ssa#CnlP!|q7{}>RvsO`5H>I5q>ZR=H{8d3c<^KvqLkep>S2|fcX63mV`M>wa-
zN>(lol6fM-k(4#C^(u<zy+MRSGxKc5#FglyhZ4JmQ1OAKN;k{{;ZP%)V`)_w<fl19
zv}eYV(|XSFjYqJb8WE%i$T8&gxaDSU?j5=hnE4`Ao1bx!2~Md!(3-pRH^0oM-(V$5
zk+=Zf-mxV_6__H#2$Kt@Yln`!{RhS$)maC!U!QAJ)sKysu%Ws_t~MyF{?TMz5&Y0y
z=id0Zhu;Wd>D`mB()0e9VyMyAwBVf{vy;D@s{bphcwzbj_=1?Sk&C0Fwx%TqBKJ=u
zz0Xxwg7~i#u{5T-T~a+zoB0DE#?;3tObZBA&t%2TS={!-8tDY|gWLe<#cWl_Uws<~
z93(=%$49S<=iJ5-?`hmW<6F(}n6erb?o!66a~9XU7pe^!OZ=I#eBIZ>rOb>PPC#cG
zzt{!knDLOz=K`Pkrg0l+W^<xZU-Eh`Bh*sP2KGx|Jl$Bj{6qQhY>NB=xfZ^c!ryH_
zh%VN0Yu+BLE89H*1*&cf{5XCPQJyR4<FR@IVmi9?Unuq+;URyNS+uUqw%A$UqcDn0
zl1Py*pN^2oPq&}GF>COd_3lgY(yZZY(V@w6d7A1hhSz@^MRY}E!SV>F%~7yi+bbfY
z7MVcK9a*oZTwW_2TXVOM;q=eEeg6dHBh@-bPtKi!jMq!A<Z_2|VV(yB-zZ3Tf3t&n
zi*AY^dJ*_cv*!LW-sKkeQ8%Qpu&}h>>0(FwDa|VlugN_*DqW^&Y0`5UOL2LzG)a+w
z{bBPPyn(wl$t+m?q({eJw>Bn2&T@&hOj0R>+MfT|U%CUIfeJL~2(9F?gwK4QKe|ft
zqA;*!t>f!<&R0JDl9ibBY3AJM?RNZ9oyLxOesWfMu4#0GohMzmH1*<*tGAj+0hhsg
zu+$mphCbLNEcFNvHPbOVUP5N0>P3+XCm?G=;3LBMg02rTDr4a>6*w^~*mM(gJ;Q;M
z@lEH8O<08t`6WTTd2IMqjcFEPxG~yCZp>i7=;ih};{KJ>Tm$pxlmgUICfKue*X*#Y
z6mEMkGeX%No@*k|QQ&6f-V{|x*?+;#F>&<>u%qZV*1qkGnG%6hq8f3nnRM^L+w$-!
zG>#DuY%sLS{E@<O)>g_kK6GcIh%;)=`~kE<Dd7S#0DA{D6Ybk@dk%Zm8-Y$->u6bF
zg8K-loq*EcA-yPlt|hP%-3ZodLb{<dX>IE<G}&MK1Vq!kL*EMsx~R-$YQtWPsKnaC
zRu?Ge9r)JH_odE^_+3oHPl6-Mk^2?wHne4h5)6Pog4))d|6mXO%`W=$9m|ZJ$uHj&
z3_@JWV%}}EN8ME2tr14;n<FQNXUoZ~*%VDI-3e$!cKrmTk?%XodjfI_?Af)AO-5Df
zE*_=t@wJX+7UbqWDSbm1d|vQ=IgR^XtL=r4$cxUb`+?+3ly-3xJmO62jgFS#zP)v>
zutuf<hNFn)vHN2>gYn4@PRRaj6SJ=^Z0g?e7sZR`1&{MmK4EfCKu_fb`%sP%=T?Vv
zi;SoB0@PKK9mQ^gKQ;Tm6z-&dJl$~u;#<I#dslf;Y>v=<n@Dru0X(L#Wq+VBuVPt5
z(z}KO#cAXi>Bv<Uk%5ABqi?<UC#Fh?K^8VU=|2h)Is~0LaZp=ol0m<i=v<F|@TIe;
zAJz=h?+{`P*zZkuFt!9w)p>Tc@rTK(>pLqbfve{D{8X3;Awnx^vq1C(n<@N5H~aOs
zxw%3c`ZK|lf)kJ}IwTIpiDa@vGjEpV4ad!jKeLE3Z+iLQ)7oj=z2$F_e#5(tal;qQ
z&fe_}&6}lp%GDgyGU3_n-HfaT7B55*m5KNuGaWGYu_BlV-Sk&6#tDWvdNHVIw-(k#
zB@$~6&u+R<dY!?YIlGnIf}_t=lEz?YHT)J<1Ur+9C)J%Lyxb<G@aUg_>NahmBvB;b
zk+3DRn(&-}id&)tJ$6(I$>%diFo_+Lg&&V~Gky-@JXM&M*J`uz{sf791cH#8MV9Lw
zX($)5si|yHugx>-ysi|fYQQz|bzWv3-^>4VWvg(O{q@ra_d?%Yp^FJ+d^0eIHNyfM
zSu}$J*};5BzMV4E(K5(Up`9S$1rB<R5+>b8Ui;PfAkPa{5ZLnchA`p7<G7yvf#bIX
zSKNI4G(8z?I|Dm4ni+?-e*K6A(LLSFA2zt)-YKz=NWOLg3W!DU^dheyY`^bhzfCoT
zpe~N93`8}^3Qvt!TvDWZRo$r33T`nz#H?6gFF|MWiEkz0Q*?bFBP2PTG@gSuI`C^y
z<1R#&wxRRY(y2od<_K-@xi6qzMYgn#1olHVO>-|14sj3q4TdtN^H-UUlfGR#%!rmb
zt<ZDqd52y_E|B^)%;NW1xCnz)ylJ%%!&O)<n%kERzw>)h_OXR&Q35lOxYTt+BV5=e
zeEfL%!4nIE@83U{k8eRw{&b4|t7tli=G`*wNt2;&-D>Hb7G&rTLr;^UN;qW1`BKce
z5FTPq*B||^4ob17KgPthj#~)T`b#yI8$CqiMQuJ-VmrDGzCYY?aPR=*4E}&n5HWmv
zv+|X)W?|a|7R|cbn69xTM<e<9f^Pr>SJSMeV|FZeDObl)7LbARi;60j8sdp9@jf{w
z+DkQ(Vc(tV>Azc3r#!eV&!;@ELhGJ!!Nr@m?j6A?v(xemM?%ZhMe+}(Lr?<+flkev
z=Je)-1rAAjHq9qI1GjEL$0#U^+P((4ds5-c+FvGEqGmlFN8YZV3BEV05ZEjd*;ag~
z+5F6JZ0NyTTi!T*Nn|m|sTB7t(slVpM|mZ6B=WOAq;Wk5SN^VmtnF1O8MlJ+6VLvB
z09eDg17Qur?17<=S($d=yT%T_$Q<@0-Gu7~@&b%5{^w!eOps{%%rSwmV4ASgAn;FO
z%*5c>p%q}+q(s-20^XWW4SyB2OiLjYhRYmXBnv{~m6j**IuBL4!fFw-bH`MUHF|UF
z=K61|`o(QSg;Z=VBlW`N<vAj<e~V0(>-M52AovOBVh{b;N)=papSNY=4NR2K`e;Z7
zbqTIGRQ!T%Zq0o7e2Lg7zYD3dn;jqhGR5ugksD-Rgq4EPBt!P0&=%8mR&3J=NG}jG
z8bJ_<Xgy!#fCe43S2&FoMAaN#xqP$0zpU+x?_Kx|j2a;Z10#Q?i0>eixR2H9iTPpc
zGd5wd3(hQ!lvBMKZ}A2WJLvZ*szVeu0!fSTZgyzgCu1`6r<FV-3t+{e&V9HiS14WR
zksgx%ICv82mpTi`Xp`g@5xb-45n9%E@3Spc;3gF9YKjtWAjK&>AI>#FSMlWZz4COS
zj_(^kg4f}DN9xTbJ#{+LiO^W|INbwE=W#Z|*-=9deo6NPgxV3ObeW59*ylFSV#c-`
z;xFX>2>Yh<Jm<#Pn%J|r2?(RhwCb6If_Mr8S4LR`$;3N&N#eVOrlrTH!;&C+FpmFv
z9g^^5F20W<?`_*+1m+uGuku{zLf5n_JM)1LKk$ctp3g+S*6}mDXg*5>(>@}J!2Pp{
z=%G!PM=&%P0a%rwe^yxuPJkh8e*%iyu0H|c&sUv*dUIiwGXLyji!v<=35;Iwc`{7;
zNP#8bpe<+pX}pUYA7<<-mcOr2b#BD*zASnhtBXgZg;Dmd9)d?(Zl_YF6%h`j^mFF-
zMwq9)D?$#s5RE%fm@4mJNhQ>s5|V0f2&)2tC!RE+6}Bea{LWGL)!O212eSsCaP(QY
z+{L^;b3zoqk3OrBeWt?GGc>16c`lsyw=st1-a7&P)}xE|I{wdPf9uhKe1O~(G!k`_
zas!uw#sMVF$22M?=DjqWk`5Y#y)*UMinA-;;PY%RfY+P+oGuRB)Xs)87rr0Y7<yPJ
z$iI3>$4Gwww?CtLunERJJ^?Xu(c#mo2KAjNBYhi~*IXS!r9G5em^g|ep^S;0tc@Mm
zn6xhc5MpO6Hj#(mB$7v$`&kbOenXjbqM2MMOp@wZW(S$)=`}Xl2fLaF-_P*zMZ)*s
z6o$lVtnFFq<7wtGFgd=G^<a5pelBF1olJuZ3@YWTT3S~xkt%bqQV-=TC)0mKx76tX
z$|S87i-*6)R$@lWDR(}jOpZ8dI3<3)S%rX2Bcf9n+{_;l(sAMqhN>ciWOe)!$Uf!L
z(7}y*_DOy=H%IVmM+z%Z2moPGRiks_9HXcnZF5y!((E#rB!jd?@C+0<B1~oSYXSrY
zq-*ar*b3S)o0;5;n6M3>Br?MHAryKApW?afU}^FRD8Kiy8ERPREtsU@|G*)s(GUx;
zUm{kzP}*sdqPew+Qb6M#Z*!{8VaH7TFOD9n&mPN;KaeNY!M7^i54c_QwkkGZzvLni
z_Gx5AoJGz&mRYq^^m?CZWrSN{j*`*fr&)b3eg=xBveVq8QEQ#Vpt4he?0T2UE_`(7
zmvXn+>kX+_N>mn5Ccs%B++&@;XPe)|I$*C6JXuYSBHjk5Ox1*P)Tr^0(i;ny1}04b
zpni<QG;b65F>jws5n5YguJfI>&-y{h)9$)S28HZl#;(qHFJy+rR+;W9Ng9{F2_sJL
zK~Dyb52~uli^~0~%Z=y4w3P~ffV^aOotv0GT;_&U$jS<&e-EoOSh40KBoP^>O$v7)
zEo0%;U)$4pYiskXV<m2mb$5~<KqJY%!*NDa);_{t;;vWKq;!^(!EFS@wt{C;<FA*&
zlZPju*GGU~5B31@>#x3Rs(tZ>2(BNIg6~R$PO-|DMn%wokDlumAh#D+Q|6nIpf{$m
zrRQbhINmQd`nRfPb1`9kj~HqpfG>(w`BADySy9nsLk%rj;MG1OBT9L=b=ge1P}0){
zKkPv|yiW`*S9vQWqAF@Giatt~BCGB^(^?F9+DK~nqjjyJnu!S^>5=fF6#tlI;Z`7E
zm3R~Vn7Y4fw!&hT`Y6uC?N&-ifs#3)bk5&Gw!8#l!{LJXyv=vK*G86rsRYiD_0U)j
zaapWdUy5tIo>^~9%%&}IWd8&d1iQe~D+<2V%MW&REgh4hWFphkxf{!2_mXCNn(WB8
zuda-B($+nC>;B^t0A~ig)*y>%+82Mi>GCIfxM`gW5xH;h@x7s5z?qfXn8Fi~(f4V6
zB6e&#3Gilgb{-7ihA%u*E#0JaPt=N1kf^M{!8&=IJeDcXZat_~vI!b7fhr|aY7sXB
z7@25{O2`U?Fz^RHbB-rZ%R6p{dtagg(57$rvAQE6V)h`V^sB&eZirhK2}d`@oqmk@
zbwH%R6K!Pd3|$<0_VzS~^pcegWMGW@w$TLBhGC>Y^+<xY2edZ6f5bP2Xw>(g^=)tV
zDn8LX&3NR?-Cs|s{xm_&a~fv{1(v)XXMCvv^lw4T!?b_oCQu)x0w@1NDr{`N2~dbE
zCaz!tHHZ>BFYXMVi+DFBx)Zj~5Rb3wejjoTEE)6^tM$<~k}K*8=3B%)U{eu<L@VCl
zzYPk2cgGs)duC5zD4iBiPmE~m4-BjM6lL$xkTT5f@Z!Bx>!*<uozc0k@@vO=&Yf?1
zR!2HFJ$Pfh1f@2!BjoQ;O6ojN1;lND3Cd=j*b`C5vF;j_cTjtkspQTif6&#tX5eoV
zRy&w(R0xXkK=&>7;sG}BYTf-Y^@X_Ew$=nTFLfU7XbpqbLQFAg=95W#Kh!?v8RO~n
zGz(Ma_0lvnU=|ux0)~VElJPjgs23@3FY!#NnbJ1U5pe{+BK~4|GRF7P2ygg7cP>e!
zTYsuwdmDypRZHD;Ya1}F$5tkblP`A8Cz*||!9b;>MMJX(I$+lmP@Lnf%6G`nKfckK
z_i+!BxL|GN^{60|KtSIGCxV-j<y@VFcdkdV%(PP-U5}v9U6Q2896PM|4#m&uu=O`7
z@^`x0qG-Gf`voEVkH30YZs@<U5c7YBH#p8Sj3yIPx;2CGA5}i46q2jJudz4-Q7%WU
z9hbO4=u-=V@2S?oxrz$FC=TDmbX6zcSz1~Z8`+GO4r0MvxsaBIE?_~*5P|bbYhOG%
zkEL6?aso2Vnp8&<mxa;CDcI{_$v=v&zMr0CKWAD@zC|7Cf8L+l05Y>$K7OF;C2iQE
zuZ>+nSyEVrVwzGe_aoHVWT!{#Jt|g%(v85~<?GAfQKlv|E|&7Mj()KGg;09q^VY$<
zJxBNLPnDST+*ycPbVllEiGLUSUVkVlqalGms+mI@LTf;q@M#957;S$*kOLNX<`^Rr
zaoIcVa0b>mUiL<<-#q`oe#VhwJXYRYn-TFTqQfY?LZR)f`4thC9RvT*s?LS)<=D@O
zirYh@w~+71TEMOepa9MWM)8ck@LbhOPkIQyiO8K<{qQOG5|?uFX|?c!WmOiSTPm`9
zif72Guhh>8t`Tg=Ahs<!=WPU?-W7;#9k|9_fvLv?qM67BJK8NTxliE?3abW6*%4o@
zx>=sG*}h!)8hln_gGvUk+X^EHW;eqEU`*gShy$DAV18n*!=op8Ap%u})IT52_(bJE
zC64ZLZnoSdvfC*53tpAEev~qXhH?(y{FZ6>`Lgh|8WGax=_GE^&ofk_{h?rEEB(or
ziq}n1?Hr-Gd_p_{ip(aggk(U`lo8gyT`&KNpdPlh7g-F@(Z0**zIgYo53T_9Zs6z@
zBbA|w@$hCCVoQ!*M8;N&T+5VN|13Qy?HgwG9g6{%u3rDXx$M00;8OV5P@iCmPv+Ac
zPS+gUmsUR*sQ8IS*9I)ou<Uh!;;z>(1#ccnLtoc%2W-*Cbi6+_*n3iN<uf%`i73Yn
z!L6+r*S3zFxthb~Y1~8fxZrGAE-kuAmti$g`=@eK07<8&eD9&>h{eoBg?c`D75LD5
z?T~0AExF!WU16VYHqp_tc>NOS%N5L<6VUUX@%dR)tNjeFpf0^Jg)jb0oR{S3@=9lI
zpSFj__^^g@o9T8;4WtJXm~#gaYxUgg7v@2U!s@*h2yUFh0E9F6rh#cTI6MK5kRjF$
z{!c!wJr{F~heF$Cy0H`4yX2mqm=6{lfepm2%*^jiUWyv=2$iv|4)zS_;Z9;TX0-ds
zzW5mAN3n&_hZ9hPTBgK=1gSFn!+RpjQ_aF$a0zWyV~)2Wp!!7-9I2ILQ3Dj-q;M`f
zsUCe5A@glQuFKhS*6EMJd5)5GfSLE_ahUH*%b6X-EJTF3fQ3zOI<X`z7_t|$55#;Z
z41;NBl$~a_ndg)86}S3TvCm_{;6OBSc%j6w{IZ0OvX$G9*9)q_#yrr&ek#K7CWg#f
z{NN%%yWY1<A%KWKcLEx=n{z(ShJ_K?qC7okYn_d5&2il22^%$Kuz1q%AQ@2`8QQ3P
zzH&X7RFmTMqu;rIN0meFU172IFb?_!L%5C(y}kq8Xm!doQ$74us#Gg)y|5wh!U%T?
z!DJaes7GONzMMdkxO)Pc7!pZ30jYt9YIG^|Lb(YwT=Ob)%BHZ<iTVCb=C%YfBJpsV
zAf#`30<sCajjjATQch8CJ;0oDS?JopCLk44_62)BPM&}uWNGN<N!76}JP`t+wN13u
z54)Tz!3P3_6cpI#7%UsM#4z2iH3Y+Gu0coAc6B@fwd)|<Nh+&_aPZX{g^ZrXx0Q9q
z35zrtmLDT-zN=SKJsdFDevQ3Sx;@%ICry#*b(*iN3+Uc$kdCBm+Qc$I%2apLUyXZD
zhFaUo>=W7)2fjTF-$bF0z*27#(+!h-9MMOp1R`KW(W)M&&BpQ^rzk^QE8d46TG;h~
zLRD%Dj|E)EnMuK9c=VL<;)STVYj+vc{AbQ^dM!r{a5KsHnGPK|Uxdq9TpKRnudc02
z*b=k-xaA`d6(S{Zz`umO1D#0*1OBBQ(>X0d;9O3AMt_Zzunt>_wVF)xNTWs<o7b6V
z^3R$qlNcoBUdPJ!cj6NFr@ImdlJ7TO&Ni=!A|8-9$(->s5kEvZ8mc}b4YOZn>kSNq
zN={6?YQBQ<<)>#qyhjWUhRiw)#+pRuh)!isJkGxA5+=V<0zDZ#E+o9hQKp*>mxxUw
zUOSkMNt#!vvnaLkRoL}OlZhv0Dw$)_ZY|B`SDpqaT_xdbdgkaFNhns+r1cAh(=Awt
z$3-f#=rNOXroXIKa*dKPLhVpdbs7idTiVNuHPU4c1VRV~(-fv9M1;DoC*0M>1s`wM
z+sVP|VHt2{!-0X+)NiEODBWND+x-6@GZ&0<$jSsCxuB)1DOp#6RDl&6K7!Hs0EIf`
zGa=7FL`Kjsw@8EFD05$3{a$pn6@}CL)OwSgR-VQ7`;N=!@-lMocK8Z$zG5;2-|o@P
zEb!dq?a>R=s7!vm<oZ!<fN0yVQaoJhmU46FJ$j}~#p77z;l#)BbC8CX^jAOWZYePy
z?9oncP9BU;o455yhgE(~0W^sUWaq(+xkHgVp$22Cz2bYQVJmzc>&!IXOQ>3KI2W~m
zEm)q=H;N#u5CUY>b(l;MX5qZC*u}5=&`lH+1AChP-MhoM>R##P?8Sv{vzMpK7>m=N
zzzH&ww5&h9bDL#VS^FuYo_lRa#%Xl4<3y^76OhSK@spkHpo0W$EjL_?uR7I@FTSqr
zWF40!g()!2kw9Lx_`ts8&yY*u5hW4T+}r!ARcUo%B|Cy2M6554O-)<_@ArOyKGdSy
z!Fb}Bnx4pvwuI%=nnp`;=KhG`y3Ivxl_IY?d4y9|4{RFH&-iRQM)F^NVA&}DSyawM
z^0Btxi#p8$AU3g2*9DjeKy)IW%rZBbdYo+?34mTF?)KK%T0qzxmCp?*q5|g!5MU=B
zDa|=0QqbM*Q9iYW=JN1nYh?h#8x5gx21=fR_~5=p@a}k{YymQDZ4(uV2}C%%`{!x8
z&b=-fpIGy9d9kMTxcHQte9tG>HzP}RB=O<%>PKMuh<@fLK4{=6)T{nMbl!Y7TYdJ`
z<NFQ|@7BF@d_fOR9Nz9}-F{~XyIOYvjy=B>5F~TfkR%e;r}PyF)le^gzbL%bYS6X~
z>yF=m&nbAEfW}(uiG%Y?+R2YnU#g7lDqMpmL}S0M;fj}qy{kO=YU8JpRBD1Wyr1;@
znICuY5&8$DNt4$p{*cE(sg-51vx$ONtfM6jK8814xQziM7~wiotL%<f<9N@Ts~h8(
z8hK=|Wd*s%Qpzp8a@R&4Z*)u1s0oXE40{8{6(Hr&oU5*m{RmSIi-2^7^1|5{Mr%!Z
z8HZy2kR|9SW}dd^M^7kGut`P~Z|Xx%z9X~zvp8wtlT{^y*H_AJFy8fMIF!1JYpm*E
zlgO>g?e@dU$EN5d5&#8yfTcrW82?f;Mi+p15VAWyF+E!C*B#mR>I5{M)r?dwoq9V(
zFRNASNmCn!L#}JI^SBe3uK;nOUzf=&s6UE!;y2QvK_z(Y4<()J=O5EsDaEEI)OEAp
zUCVqSg?<3XF^q56{<!RV^rV8)`<h%&mdJAxq8llWXk|^&R(0gG8Z9MEAINlDc811m
zVTj1UZ7lf`xT(?XI3Y$;G_JSQuT!Ni<a%cm;gST|9hN#ss0Ac^?2gMNLgx>6Igzr&
zk7p^X-dWH*1Ckm{CP{ChMc;gqa;%%)@y&f^6$|kn;|7zo5D%bqNILJBxpU{(mO9#_
zf1;0wpFcd6-=Yc6h`0qmdi$&9a?ZL1^$uOro^V^0N^{=DjEa|0n9yKr1%C2dF)gsq
zFd8<-L=f%?g%;H@t*D+2!nfzHEzPzd_hlb(HLlhf_sl?cO&d+#C(~`L&zTGR*pkeM
zVV{3B6^Vc3V`NQ;p{;vXv)}#)-_~bRrKp_l*-f_HXcESGPg!{>EhiZ7Y}b5@2hutG
zfL$x7Ob7RyL$M;XszcdDMDzE@4f*8dMEdS)rk>(w-+u-<;KQd3l}u9OJ$LHTb6Iy$
z-^IUB5vAiVlt^I644RLCaN9IaYh)gLhOBdg+GiLfIAYowu>=S5Q3yB6H{lrs;v<~Y
zY;E7f>Px*K&yW#Q53kG2;OH|WF1=0DQgYWmpYxwcQQQ}PR;<Zd<Ur9f-bl4b;eb5$
zE1@_|;Ft)4s%Re{(h~x5Zh$K;j`+5Jp7g%Y_`QF56=0SK4=>V@FI~d>11EeocRQS0
z==GPXVN13|eYbP+h3B{ydG>;mjciFm71lQ~gT#gI^_BwB-F-t!-CnLhs<5#^*Wd(X
zhnm75M>5gQcaVb?4>zs{lo>|T4sWGH*R$2oMMB_u_9d7Rhc|;4vYhO=(=>e^d4Uk$
z8dtHmpD(<$H0{zRm1o;QH&XWte#Bi>RasXVjo;OXvro>5))Yuc4u2A)9Ie$ZGZ;BD
z|M*1>rF#mCV{|-Z7k1IVhA#(BMS#CJ>GHUp9I8|>V0P!)Bd6mGE;$c5dXCcX#DoT_
zZCJ1PCOb~;HUaR~Ji=Yi=R`T!jX6m!26j1lzdCwMJ#Y$XME&g|<ml0_(tNc>-$nF?
zlK9=N4Kd!Ta#yls!|X50IV#LLcD5&68XbhB?o;+py(wE&``O4fbRqlGmm)AeswhwC
z0vvIypKTb&%jDbzKlZ(1EygNj(Ab4)RAE}+B?5M?#=!G_Ow>$OX>3|7$NTWR<8bPg
z+gC8t(*!}(rtVjS1rD&r8OVNaFO+Ym-VrFTj4c*ejJ)Qhpj;AJXz*OQs3<0;Vt5g=
zIXh#G6WmmvGxhVJch2eL&SyL=g>Pa4ASia~DMtPnjqv}5U?Qx}Vy;dGmTLRuzN|9p
z*ea}f$S=Qjm0m7K*+XV^Zbgmj&0}rDf?8BQlN7y)w}@$oub}NQQt$u;3k2f~V=d>=
zbWp2obEXdc6VTP;R!lRL06`?0I@FyGR*0O+P>j(&!&BAJ<-AzQbxWjxYU_3@aIB-D
z_z}bj=(_)P59|m4!F!LkNoseaExeU%aRi9E-ob^$QqqI%n3gdCVu{oFp0~<MBHXZt
zZ**SYoZdWbNR}`wZw?Zd(SbH9j)v8BC!UY}5K~6-;K7u0skpfQ8m5|4%72VMFS-2q
zr6=i>rgKGH)6I7NTj%ycsXsPhmiNFk)#fOe>n^M9P5~Y@JcY7AvJOJa$l_V+PkP2h
zW-59x30KPx@`<}Wuf0V5aN7055$*AfW7dIdLxqV4isaq2#U#xY>1pXpP!Z@%9vi9d
zytmsn*d0cX*1`9KkK?e{l<9o3#u3+dlHct)R#urSYBPs!d1rjmBKTZ~wX%``P)w`#
zo*wo5tA%A;ghjDkPr8p@0cty19mz~E<hY_NRREhV8&&NHvFCV7&pzGx((FLu{pPPr
zFv}Rf5tf=0kbX~8x5@<Ee|V*r_zn=Fk+nh?!n|(J#j(}Cc}iss5PL2j9s&HJ0+S@6
zwPjNI5D=2S&Vn)_#8BwPz?4hVm%o$J@0Zc7FUvyn{YQZXv6b6ntuf4Zp^G1he(086
z))UZZZdZ$aQtJ>jb!AqjLts^WfGl$S(NY`80BpQjqv1a+`I_M~`<43Z12i5YE>s7Z
z1Z~`W1NsuyUJ3l(_js~l)g%SuYBwyae(^E3MxuzFdEmv<T74Y6jZ8l`srv&d-B)9u
zF58(ejOS>JfF%l^4`8C<d>U~N4;~#NYD34e8!n60=|O`X=dD5KvrJ>}RDh_$Mt-pF
z$Ic6E3acL{s~GfrPQ8J(r57(HD1q}5G31MZGoeFyiGm-L)twc<kO`H%6{afDnm+&?
zc&CnuFibi}_Bu}bM81PFUtc3Sy_P~b*sI#qE`EU>+4J9P{zcX9J=qX${1vk@7Zv;D
z-uE`;3x30G54&CU7q>eTUfK>Dw&RK;D!GBwqW1uk!DN==CPmT3@;4|uS;hA$3mS<F
z_9367e#o79bMebo2hA(%<Rz@L^roinkTgNYN!WYK>4nvkFDYzHoKY1Md3llyk#1*z
zXt)=2CUv?vD+XsY=UE<1<~lWQhZKBbtL@#wS62NYLRFgjod#pUn=hR=sPv5n(Ku~b
zcw!vJCWlP7L$i6Ke^xOi(rh7vwqfE9J%!cF=n<tK&-wEC6=4~db&hw2=0*o2^FOTM
zkDHX)1Sa=U!|s1g+^7CY;Xy#lcMK})3eR$p;k)IxdB6?Rzwkt(_F=<vHX|ncmtmZR
zg+>}zc*0xijxo}IF&$q1XVXC^+SZwI2z=qVJEEM-)OJ*}E@05#WI2o-^g02(IYRS2
z()-WGML^w-^k6TpOL_zFLT3Ff36bu<Gy>2Dn$BFc-%Cr*UgX`p^~Lhml`GtXIClrd
zH}B3XXYf#I?7gD9r7&Y;HH-Z)C4k#sPe@st04K74`rb<7U&tZ4uAN8JImgWHgbf&6
zD(fBJ*<SJ&(@UDY8FahfcN+*JGPbmaBOnDU@tdfeyD!p{cg6g5)0t*f-()Fz6fo4a
zl~ncCIL&U$&?Pu|ynCWMr8fcA(4+9y-5-|DDN!-;7e<;~hM*q}>dxJS<HWADPn|AT
zcJ3IT_8@_OnaL%K_=9aB?)+t=1xqHykkfhr3N$Y<>2=)XTju6^e+;27-+Cs*L@uQU
zyv*>UWB4+ITQMKqo!fLg;b(YjYN{o219CToHV3^_%z8x9!_zm`oO|)8p_MHP|61P~
z5>gS0o*V~XqsyAEJt~6-<P?ZquTI`7Fs_M~u(cY9X}_LYfH3f~C|Os{FV3dF{V<gQ
zvXK#xXJW2n<;k^JIL`loOE*<7%3y6P*-AAV#fWy4;_qfgT^e3d8hZHH?H<RzOj2-&
z`*;q%tESW4nX3WE|FCg~J?ESJGvjM_FKwm01K*N(Y_##HHnyi}5nk;GL;xkg0Pr<D
zuaHJ)o~<=4Au>fmH^Y8*oq$RqGx?&k1FC1&UWR6HN$CgA$V#5I5N^p$nYgor)i?po
z<h#zdsGf@l`!?*j??*#-FkPa2O)kcW8^dHsX(%x~H^b~b^$p(A_XK$nc{-2w*VE{G
zq?)pAYAwvV-s~QSh!dch@)=+Z2LM)5R~00<H*=H$j;3`sqlEokTTM#rvH$Q+1Z)Hy
zaNs`2L}#zK$C<Q#)-T#0F)|2@^g(Eyx7QusR}te%iM_T;M9U(_QM9@(J+FMT;<MG3
z*2eBq2K!}0i?v^5Bh)JQo{7Ekn9xs&C1Z#2uCSlFo2@u)3QKltzDB=JU}x_^k*B$w
zU4!ub)~*jPx%I0*GPai-W;c#tLe7gD0O?Q`sZCd4fy`h{BG{M=CUj4;pq`3zk%fk>
z_dLbv?ccObxb+!3U%cZ_j#C433co$Tbei)H=zej~GdHWb(8SA}cD}(hP#_ANb@|y{
z_nx0DR-eOjXQ53BNba?<FY&HL!_wwzYIW+{tQ+S(5VjX}7CXa95>pk&3`(XU+JtR!
z^)K(4O7CQ9`}DWeKRT_*dAEssWH}hWVmZjJzc>n{+$78yHu>50FHJ&FQHRUqi}S|i
z{V02mT=r?W12?$3JP1{@E`7<0augPZQj;MUAX`%)s^^ZgmD%&+Mv5E?M=CeV2poE7
zN9{z)*Yzdu0JNBZZk1|0Pfpi+*NvVAX9DDUj+b~d(^<O$-6-eucGoMLbv~o8{UG=r
z!?0nz?ynb)qQ-NCD=#i*cSNv!^<Lk**5bY9JG`z-X2mo<zMenRbETzH>uK%xsM>Q*
zoKbHMrU*o%g+4V9u67}kJW^f65q_ICwL%0cK{UdCtf*myuIJ1webeRuBCkV%&a+Hm
zU>1bnjEd&0W&mcfxc^{0HhNd-QP8qeo$&|xqlTL^-{!s4qMDJBl(uQygD_pa>QX`&
z>5eG}TpA)%@LKTVPv2bgh#6g?&0$jdCMeejO};qBL}x*E8Q4@-vvqR)s+q}=FJ|9s
zo*FIm$XbGp4*n6b;9dgurM%Al*|kUi>+{z~hIRP|fROI%a3dJLg%5d^1$n@RpUI~M
zPYDFj>i_ZZ+qau6j3AH)NdMy_n27)F9a}OBM>AUr`*EzYCd;m4k{;Y#bpqwTkjyoJ
z8t0Fsu#&rs*2T#zq`9Uk8A4PVnI2aUf1Tk}Q5`I%Jw5sAdvo|k6mx{n-A!05aVsc?
z05`3{ZIxfT?PKXGkpEL>P#`E+5$*CLm=qcvQPfc2w3xG_Fz5^qMpSF+CY(;lnp#(k
zt&7TPv8k+z<#{hLxDXZaE%cE8H7pDDJ#aJL24|v4P}+H}#q;ZWg>7-oQy^l$h4PLT
zlRINx$<GwEd$;i-_A^Dm)ah(X`TFdVM5FE3`=abx0)|4maku$XK%5K1o3Q5Tk@N;s
zsqNd6o`~r)Tbuhj?oC$_SKm$6d_y*SO835Z8>h0fId}J^!P6$U>L9ButH*EN+RqtP
zi`Am<NUnJfEb5&*UfJJ@NWsqJ;1vzi=eN%w?)+MQzt(TmYaIYd@_ND>>y#zNZmwl`
z<+B^pJp$ic#-qw!@!sSRyP*qG(48y!*A%!rtDl~L+)C=C5bT6noGh^)2<))r+mwWb
z_8Z?weqQEsFUUI3N-}IjX9D6P#|Q}-mhn;6%qF1j6dESNAyRe*`f)ziRS5%(8!wgZ
zR9`X1mN7<WYkam8v&LYe!X<NaOfoXL4L7qBj#<6?@%03EoZgIcV9les+|0URPZfra
zt>L@x>MN6%$~Dgm!h}UqtS#3|q+DTd&{xFkZX!$pgr+vfFX<m*GypB$d~t>4ZBhF7
z_oEMVV;Qpgd|#|RDZhFwcMSLpW)dWxOx~UYoFZb2&NjF5xdY+X>{@=E3Q1@=J9Ey*
zm0ZJ=9LTx?#_2oJ>Gor7&!3vUTOK?%)z~EatNGlGi}G`_@xrVIw;0kHK-8yd%(xuI
zz!!5YkCz4Ws!WSiKHG5WAf$3(laEX^4{YbH`(-n6&;0_8n!~fROog5wxr_LQ$o8(>
z;lT_0!^3xO^}dh&g0k-5M{P9XOWUq1P=?)O<yUtdpQ@ZuS<eloccRetxR#b#cn7m~
zGct{e+O+pPQ0=4jS;|Q>UzPRs-)uKejLoSI<hmo$SKm{&l7z#gy(>6IsV!$s#wA1b
zh?&Q6p6Wx;=fKqo3qc$K*+6{|!1{rbV{r(BAe-FT0dF<zw8G|706!yofXvN%#4N0G
zGsNg?&GZuT^?R>Wx&<uX9utM3Fjo_f<52Hye5Q@P&_Fo2>r^0zyx$26;rpWxJFC~2
zKY2Gt%4|B1s={6%Xn&{F-<74HwVb<@s0~2v=Q|}vNFf}$vD)h>FJDYX=^b~uSfM)l
z4vjcL25B(u;FaC*(t@)hl>cSoyN#Kcy`z{`$MC$`Tc=p-x3u;#-`iK^XI75@O(wPA
z?i*}em7*oycI*Kdx_9j&s$04FqL60xLs8{9C!o7NbhEoi&qKP+RXDz96w-2YUOu2t
zmvrby4N$iSxcti-k80RSvh@WP8jn%T(>*ze3At$5`U^y6X4j2T%+upi9(cs4ZA;YG
zZfU^Vb=dT^^~mT=(6{VFcd=;gts<2P8NqXy*x#P~7d=bVjri{{_>iH|1HNGRr*y9u
z!Fe+m{a?v!K+jSJvnc&3TqLXbl<5{W3#*j8RYX$6oCdtf(MimC+p@6e@tzKE>s8F-
z4VN$eT9{h|btzMXC9b)-_geaJ9($)JjajNfk1y%!x1IQORn?f1^tFifs$d9^y0MR8
zTJ^d*Rq4)}nVjJyeR%2n-TAQ7zpn^`Jg;MA?h*O+-*4tZMNu<(UM9W>2(I?yTmNr5
z_6r}pcKarqKH(=;(G7KgjTGQEF?pNx#7@FA`Xb!vjzZxiZAJlb@#~a99@97s9~FWR
zA>`sGK@~Vk2*;Dv+x2=kp8qVT)fKkkdCGYqxRR<Eb_;O&K<dEeaM%?Y)Q1=iWP9pr
z)+CtIKWpdJL#7g~>#qHLLv<Z2DICHd9ky)=8wkX=ya#ah$7AcbiT`l@le$awPiSub
zPoR1E|6ynj8K7_@l#`nF&g<%(=X&e3FBwIKY_6fGdhSaYe=~M?kZ)uih8uDC6dW8Y
z@bc$2J%M-G=zHZ3EAeAtX|NWZm6S%tCJgVU0DZNboiaRCWRH)w@5yNS7WpYu=>mxL
z-aN?vJP<KkEjLk3myP$Wwt3wlSEtClp~*K^gGiG09*Z?ADb1_F=J%M&cd{`ii<x^>
z?@`L6R?>or)D5by2pnIv>wq0?^|9f8`mf`m-?{tZb7cC<!F;ibosOZ?4}W6Y7}iu7
z?A%ROhZPqaVlVS+viW_p6<pD3oizhDP3p*qv)$)aFtstJIy8$Te~N;AAcPpk#Hw=_
z!F)~7<=(FMzTsb(^HP@fm4PI2OuO!IL$FZC&rmLU;6{x+PcW#jzeERS_)$G;Tw6kA
z@B!Tba9tFWhM>hABV<t}2a*`?Muy9=q1<1%H~i2){Q3x}9a>LMoy#UJVn3SPxM}KI
zk1@*!7&A;w+io6M#MK3@wAa_ukL~FKrz{`UDJ2>!MlM*GO!}^7lcj*jNnOU#1t4pK
zddH8#FrWQB>4)#PaE7(W!hN2xB8m?*7O84{+esjKqx`&ZiHlH3C%a)}=ar_wq74yE
z(k+H#z~~PsSmE8C=?SdSNEfv!2yx$tm)(s<$$v!35Zq_?{A2(zJ=$Qsx2fGW8O38M
zb*VM&S>5vWWZcbN>=Hu5^{kccvUA%KgxGbQBy%Yj*@L9RTE(Fl?NcLi1&ScxpI0gO
zbBQU(VK2J2H>zh6BAkTy02h>F`w6IMSddwKqW*{yXv74a*6~4R_Z#5QWR}Y_mV|no
ztiSu+z@I}1aqQ!gG0ELqmyd;+r<ZPVQnlL+FqR(L@ZET7@c#D98<==tzOamyAxHWe
zq&LjLEmI|dY^@(v*WiDK=Wq?O4jb2-;Um)$M*XtvBR}n{n3bhxM5?L>YaGpR6`x(a
zwO(wg0$*@AW(zYO%)S~qicTKN6gJ>e*b38OGOYaNnXB@$Z4M6kuw@0^Y^onVU_B05
zEys@)Bs+h5lKg9;DMrKJVBjV+3N!y0n};6FHdp08P9d}ADNO9zu?g|{wsS%_(4<uz
zz~9B(IFSw2_orj*naW|(`13NM0FHepaHGp7Z#-+2tm5yKQ$W|X811wGzE$iD-69$7
zRnqQD5MHk9I3C5Ao8HH6HN`-XMGO>d&68Pwj+Uv*1IpmJ8#REde#lhm(_TRD{Y(zJ
z!ulD!^6+jgNgYAEW3;ie!@K^2a?LPqBt|yLzntFFLAPW6yXB!ZZ1$;d=tURGb+OOi
zcYU)xL{32G1s=<P$iG?)##yx_VmA(Vh8ro8*q^Y`L$-eNG_LWyZC*X$oq<mR@?1%)
zut8+}H~YWbG28p|j+ySCcg(_Xv6~ya+@`spj)Fj$Z3#vggaEEJSlTo}uek%JGq_=(
z8!ftNWK;60T&X5A)Hc$GOMmzmv?y?Mbq+Op2*i2bUTThWgV%8~T1?4A3Unn{#a)P_
zHIY?NHMKgYbx!j3u*?tL%e01xhjC$E`^V>YYdmhoczlC;uA!ZQi&|HW0+4-R?ivM>
zGD5Our=_S}bYAGq{UX8ho*s=0W}YKhUyY3~hdkg9%9?;Gwj}7Cj@<RuY&01hr1Tdl
z%{x1v-WrWoc<#t_sDQ_GV%WUL=TV#4I|1GQS9RYR)nuEl8$pN^L8M3zN|Pqi1ZhD<
zno5x-MIzEWNUs4AP+9~86qF)LFVX}l2@rY_lqO9G0cipWHGw3a*O~Lp{$`&&bJkgV
z&a88OWW6i-kv!$TpXV;ub>-_S4_SYoGHh!H5_5B+yXsN!CZ(A8F6EgJW3>|d^1_a5
zO|H7|qjoRf(QYTU0;bz?KhnM(sT|M|Y^%b;$px2?azV58bI50p{6m|cfmzp$Wt`ap
zh%ng&?`AdtcU?uHS2z}OO<m7~)JdbW&2q#Hub^D$m$CMo=i&=W(?NHVO_EjwKEXy2
zQyU9$aR7Z{J7xmHEPp{bRc2*=4omxEOGC9!=cf{Vbd7kjPu9U#YN~Qs=9Ao6Q*{$^
zGhYd2&w|;t#iKhyxlyd+C^<r(P_<-_(2j1^80uT?!7hMZZ8xA%?3j<o+1x3#?A>+A
z%y1SC(tOv9zQAEz8!TD=Nuc_q-O2C<{4j+Mh4Wo}v-kZ1&ZG*XcK4x;L5-GhP0Fxv
z*0&em49IeRHpjYEF$!J;VeOs-w_5(BilC5nD(@OysX=LknL%wMB%1rc3DIWf^2jW`
zH6(19pOZ0ysY;C<u8$I2F__^wNc>2Chm>GubN+D0?VZa(f?ZLUPn3j!V_^FDEgC0B
zRVwfN;Gs}O5^L3gACi!dzYBSGwEv0HhHDv6ZboD*k|UfSTJP^93y1l2L?3RfLt|k$
zJ!$5tTYSGjUBhQ@g<Yd5_j^ft+Q*l;vlU=G&yv5Va8$jpO-q1bet}{)!_F@ZaQbat
z=~Hc#%HuTU)7`Ro*5@dcA(X1C(33diY>Xybfu|>bq6RnGm;Kits@EoLhX%rz{Kkny
zz_MgVDKp2?*0Wfs{kVJKkwca=fUeTmz#0%PGMWJ_pB(YnBWKJ*$!S(Wnz;)$*D>a5
z7LT8URw{ym!0x7^KTvWrJ!KfzDp4Zjb5|T<pFrWABPqG1BW}s5t+zcillGCAq3w(c
zlpX-7ew-EK>|^h=fmz?5nVuOs?_d7}&7IOAma;UhrnMrqub|2J+Bh-Mj^(wju3%J-
z&wR-mEWcQ|1`2)T6#zGcf9Qr|Ua0cq688W|oStJ<v40Zm_K1A;n?TDqQsqA}p$X5H
zRqiSk-2MDBlm?Ur6Qy)>Bh&ICdevFR>j#P@D{QM@E$1xWjF&g^6%&5*6cm~UdkJd*
zZ+6e0djb%?knB46Ajhjdq2c|=w_jez7>#?N854JwsJv;J(LL10>`5h@n}b`&uHkjn
zb+|_5M;WBj0c2b^tPa*SkedwU@*jS>Lu(Z}kd$GBy0>|rN{KDG=V7cnoJF+N0$|0=
z8<F24ucCpyo2f5XZY8BD;u&tKbuFa~L)WqUP&r#|A@eMPU>ouhA!X6c3ryhe@VI5v
z>`{G<YU#L^KR#jn)l*a6R2sO>eD(Iv+=7T9^)tcsw8@8^({IJbs#ss#<V^^o0})+=
zMM2Gvys2W2Zb!&w(8njLOIS#c5eJ!=3a`|@pzzG0MvfZLIgqbmqL^a{!>Y_F$+`{a
z3aPhzXD=FFkY30$AI|+Kou~@INKf^2r4W!?60l7*k!^!&ZK(WVn#LmM7j2=fcJ;`!
z-ZbG?VQB!iFZU#&0fsY(X)(>8G4L%~Jya|!K4tX!=E}P}yfrkR$BbAX_e3>PX}K+Z
zU>mnK5mfYmFHl6svFpnBEc)q>;BC;;bu-6rB#auyB|lG9yw@!dtu3H6CdQ8_cOD=`
zfaCV@H<}EU;d%k<1z99MG14d3we(KH>N<yPgUsh!ea+{w{ygSh9x?Zp{O6-b%)$~8
zg7R+lx<Z$Hqt?;@GqR9dxb%fgL%I%x6Zs@CZZ2Co%EKSRGMC;@lQ=cA@@45|a5ROQ
z)qJU8ljax}_JI7>;flIwlhtW<NSNi#<fVpQsDz%blmx<D37l>^6jl$@3*#oIMkt4Q
zH$7_+eLLh;q@%TIZg|?uu@wGPg|Tn%nf0;xEA11}?NzQv{!Z`BA&K2kN90nFc}$qp
zA~R1=@^FU@dtK_W(U{0RsftA|rvNgDPge;<r3-RIl`u~QE?jzZtU?m1!O2Uext?VW
zEVa&Cbo&IFufqihoM0_9eWayZDSco=PNqO2hC+PCq!0&HKoBtHUv@i6uVjP0$)4!5
zQYD;CNWKi7tlypb=v*y5b(dg{{RILYF&0S%Ic&O;3~>Khg5>`+qOTO}M1n@bj6-S2
zJ1<g2c=~;Q<WV?&f%qG-YkRNMLvRD%78T<3HwH#`)B_d!IE0T~xo=fD0sSBx41g?(
z;8oGaV`zZ?8_xI+PJbMd+vYnu(_)J~4Q<Z~Svv~J|0MR2(>x|g`@~D8$N;`od|aaZ
zFA%NEM$y`O>{8)3TN@4qUfKuBC%y$%Wo}Dfs%I!_ZS@tZ;rl!^mPLrTw78z`s%(;j
zdSd<yBosam#)X}yv8*|Sb3rMZ+bt06#?U#7j1{xMES`^pOFK6Lyw#c>1RT-tZ?gqS
zU&rsi4oQDkn&v^C8w`3du!?Skyd+C3m(G3()2$#xTt6~JYtapenm|pjLJ*e3u!LtD
z%cIsq_4B33<E>oBa2t3>o!rImVxltzA7|Vr{rt|sz+rYL-=DuHVl!9+f{L_PR-xfE
zg{&3B&kFCv`z~GAmHIi<4IbTPw}B*P?TAcmR?HqH4IJ4|sF&bxby+4la@wevszT0t
ze*=&s&jfK@;oX9rIj+{#8AzjA(tRWrbD6@er{Hz5EVzFIXMQ|vjn7T21CBSB`6)}A
z;d*yEHr+(c#x@~8PChXFeOSxZSD8SeTHe%Pp)3obVCy~!4#%rPuTu>kxug@5U9wDj
z##eHlxuYtJ)-CjnZ%G&Y#FT}iHu3<}%@6+K--!Iu|CwDVN5izj)Co_Titn&md0&cS
z(F;0z3^_pMP9Pt>KUu<9bn?-uO45IH2Pn-s0S*~odW;VI6<3d{HcHhu-nIl{4m;16
zJo%!~)749S2*8ty{@P>+IcAS|72SQe>#M1AD^yw}!XmkCqJ6^8O$tuioCm-Yi?L8;
z_(HKDv{uuJPhkd1rg8=3Tuj-w4`9!n;%8f%*3OBk^*A@&5WBfH{>?iaQ32P0;S3>E
zu1Hn#OaDecovhF|m+mQCJIh$B4JZa61>m);Dr62+F8l<^3W<}BBHdXCa?k%1*ZHA~
zH?^5P==Lq~5^E>H!*|$ZjjLGj#*W$YW1m=nFuD|$@#uw5>D@0@q~Qxy>yQJkv_bL<
zRqm<JQX;VaA4X&eV$cR`bn+(zxWd^8b_bTY&nb&DE|Obp_wVq^QPoJY&JIZ4e6gb_
zu>}Hlj@|mESDE(Yo;_}#QY~#T4uJuSkMEhx6{bwVYX@O<iXA*jkUL56hPt<t)2eZx
zS0-V&vmuiGSN1`dwV(m0mnzM-${0%Ay$XVyVy3U!J!kjxS8{MreSE4NoL}{`1Emg#
zY(NO2xoEFsbrt9tcLgz$TCiWBtF8zAEj|FUI&4@zu-p$%ioM}4(JXXK)N<if_H%;<
z7NuYuiPqJpP2aX|A)R*f#XFrGKF;<vKAFAT>693$&-~F~0tFO!z8#|}=Wz``zhF)?
zcU{@Bgxz>xsUu8tpF`J12Q9X2Wp~MBhZ&r;L}8$yb3y8KbLfgSOW*aQIwARx*zy6@
zln*O@MltuEM}K+j1^Hg}L0r!LD`#=3NyfA9Kg-J`b2#+hYnPeT(Brldz~L6ge5@J7
zVL(ghhF&rev0@;!$ypjCf^Y8|e;Ijwc~5|4OWr(5|6NI+KGP6?m!40_Il8h!xD1>F
zW$QI9ficUMEsRq~6_r860knfwlevLe%!w>n@cq81si!}X5j6yDJnU}sG3^^nlIvm^
zwm-fDGY{Q*B7&;2l$bB-G=1TrBtpS<l%U6~tLW;GyTn=okTIOxA?H&B*JU6qYdsy)
zcDw_@B@24sx0bPIYeQ&Hp*GDoCx31pOOa&B*>&1=xWqgDC36B+b7rg5K3A?zH!dpe
z_TD^@yq7rn)x<nEama@=eL&RtKs$n2B0o0li+T9B-yWIs^bdU}^B&a4!?uKB1e8Yf
z>s}e!E%dG4bXT3dXEJ6X_U7deypQ{P+<c>FPELL^>3f*$oW-SK?`;~jdTk+oBFW-z
z3HS3kF0=RCb<6spa)kaO8JcHCQ90oE;b{CkkSr|YvaLQf;wCbE7Vn(u@1EGceM5DC
zBf?1j6k~i__##_Tfq6PfjhqVD6~HN&<<?5Uc<6r-_BU@Gt5~k9e->JMOSbsDwu-qg
zf4@FWL=3M3-HmCnvXI=|0kDnSfhQB?2|q143+lq0VCny6B}P@T)aIK`?rNqYq_JLR
zX_vQwF(v(TuyOn`0%J}(Th!h3?hA}$J1Mo-?Ed&5#Odhf0MojI_l3mcsEY?xQ_`{g
zw6z2W+=eD0-1dBi6n6*1c`;5|&gT+W`4T?fOP;FSLYlMWJ`|dDv5@XOdaZu*x|m+Y
z42hYKpoU|`z}k||tfEfWHU1ooWJo9?FORqLEx$rKt~@WGmklVcdI*SMh+fs#Hx6{F
zKc&TD^!T?WPJ22<ANb;Dii|N8ci^-iQw&z!X5C)n5wXDa#Z9<ng7d7%ty!N?<Y3+0
zSf17T@=*=f!*24f;I3>qw@A5l{>92-ind=k(~Kd`vAX=s?W3>>cP5?AyToFWpAaf@
zkE>D^Sx#s;=8hPFo$~+WH&a2sYgQ8|L7V@s^U&~iB<cMV+bmh1LwnFlp|(Qi0_@xN
z!3#n(oG&8{!>9j+FH%wYwUixIRe+Q?t2g8)t$Jh@nMtXX7dbjD@p~f}tg@$$72!7y
z=MNkR#<-@@g`&qTMsAo24k^72^t8j^Vf8($cRRO)%iX`q$44d9p4Al<njBa7kxSdc
z5RB`=7{pQd*A-zNx!R+7wW$kK?9&RMP4Jt7w<~Y*oU-<sNXIPdAvJ->^5gY0OGWH%
z&X40Smfu1Ay|&k1v`DExll&L%sTz2CNwy^)DG;slo-<>-Rzt%jmN&B95>(>qn{M!?
zq?VUozQ%qi6+EBZ!r-;&<{RvIGVlu`y@?J);h@Add_~%+_D-Bv)#3{!o5KmvSNp-V
z5ja4qOnP?p7~V!gq_382c~nYn@lUKVKGM>w`OtpSgdHa(|8w}uUu6k3fc}3TkXuCc
zRha{cXh%5AIL5Qyh32)p?3v27Q-{1XB>nKx;SShobTLe~^7}>m`tuLLMK6PgNhkdd
zuWrI_PEITZvn%gqh_aT3N{{FA<oeK9tC`1itWIq2X(#F8oOERtCHlt67AA;ZJL2R^
zLO~n%WGvfk-?PnHgt97rt<fJ+d{|Y!e)Vm9P!`>ovXXc|S|ECi#b!bx&fE0CxY7<X
z37&$%C}s2Mz4dn(;xC!OId$xnmj7fqA@mxgoOU0U*rFPywkYQJ?Su?cDAHdTl6TnN
zOOp6TfrP2wKW}U~@to(a9Iw;68)HEGn}n>#zr0<<0%Mg_sOaAhKes<}oapEqhsM~&
zdy)p`?!)#cKLFQ$Skv{w`U(L>O-H3+ojBmkcO!<oO*+3tX}2$|*}90EnC0eXLoat@
z-RvZk$8%7bsX%eCDf!x|lMi+zmf&0w9jKc5q<vk53WE;3mGxlPl81WmCd;#>TyfER
zQKBWM=&CB6D_qP<bicpO+6#~?(RyR*ngU#giX;_s3fu`AlUcovJcFyxveCSu?H^#X
zV{c2+nLI7heR!vSFNV!ztV|h^^)5RiB>BxnW9z!b)E!^|c^`n<<bOx)mij-WZ~pFk
z{$jCKa8|4Afy;u(#ITAH9Wbt8PYs#Avp-bd^#(mXcRpFC&&-TdApSU`WxL0QrU9?;
zsil@mWwqx|P<NRed$%(=f=sQHG875mrME;wg91K$%iQt{M=eBX_wSh;UYX<wWOnn5
zy(%bdaqq!nae4wrR)Ev$v%*TipYohWp{Fe6Fiv!1*2wM3amvdo<EmH4bA&=Z*1O@d
z<j2<nv^e)Hz_*X6M{?hX%fSlmm0p^)b;yMDXhoQJ-n019^ZBmjEt6lM%J!RV<~2c%
zQr%I7b8bd%4PwY3!5!@I&864a$s9Ea*Ld;s`j<VrERSkd?)vKLexH^W0)Q13f52t_
zRZs9=<rn^Lwg1?`|2I~>l^LDOLb~}@D?{~u$K?{<Pm*d>A68bG!pnwDExui#0+Kv`
z5o9tU97ptvx#MQ3;rVpXCnts3m@>I@G+B{mi}&~8=;-TdUd`NC4I+LQ1Z$rTvo>kI
zR6z{bb$Y*c&iwEr%bcs(;)FSujMsOQfWEf&gH1~lBUoX(0$Vof713nry~x*w=RicD
z`5PDi_vD5D{(A^O{{3e%(f?!{NC%l$CZ!yFc!Y{)xK0tHLmncR%3WHZ1+%VSHqtaI
z{LgNhL-Pgm1*<n<Dp^Zsy2eah)GhpXclSxdg?)thAZ5=a;84pkXee3=o=0!T4ZIU~
zv;0c!r)8J8_cSE;0F^n>8W1n>?N6tU>TS;}A7yJREH;S20>4v*CBXClWn!p*{;uD&
zm4CJ{f7t2&%_sFgR=fQ#eddp`P^GzAKDwVk+xS#JMDu>&j@+Sd&WnbSn3za2WX?tE
z@KM1hpVee<h5=NPzBV+41UnKi%iUxK5`ASD*)N+Y1q$aZugo8Le5zM4pRGRk9lTd$
zlF|s&uk+$Z{-)YW^^X*bet-VI(k!)2J|Ij+FUj}aE1m7Y)k+)k>7}X>^XR0c%d1BE
z?BspLU$gSif{yH2&B_vR29Moam~9ne62IQAz4aqaM3xEE1JT?3+q9{!7RG)T$q6*M
z$>sPEL_L;fWcl4<UVFvj7H=<;qE>rsxU8IYQKuJ?rQYD^AOu69g7>CNlOy_oIS7M7
z9scA>kU#Z6f3F~QH&Qxi@;=V}3iDf8u|}60+lKSrmt=BF%Ecmt2-wa^DHq(afyH71
zgYa0vvQv+eq$yhnm9Aipc-$rnu!hqH$Gbp%qRJPC%ycOT3K`a~F)ZN~8*OIEu-nqt
z&12?0Hs3t)3#5K9{~AiVudok6Obo23y=jRG7FT@>7gzu~!_jXeRd2QDp5rkakBe<F
zcROHMoMGE39wv8G^ry`Tt?UecoE$_!e%jVIjVjC-EzipMIoZ%n)jubjq}jAD&fUeR
z^V_&Se^aCO?146?>?K+CS9wg-)`Z$JSMGtJK+N1rWIEkx6{^0Tlf@@b7aR}zY|f7F
zqMSL1EoJ`0?(IG6Zl96dB%P74(}f0J{!a3ftOK9kBfMroG&cG{SD{HE1|)e1&=Oeb
z-q<z3=ba+WPlGAJGZb%ll1bX4SEn@izBU9Q-{lslwEGYQX-|JeM@<M)--lDUnbE&M
zyI6l>5n)nou__VCg;$KLtg~>xB&TYxHE7Hru*_5C*q+B+P?uzhzCUU^Ue|N>n(USq
z`IKNa__3PXKuaM0UXZU5KOc^an(?QOG*2xa`r?g}liA}uns92PTTbi9G`!m16EqXv
za3K|{jHR$>WmR>Co13@rEMJq4H*ieVpo7F}kBoq?@#o`@_QMF94`QsIztcn&$k-A_
z|1_6y@J&$IOej`55y@uI5EN$+@8)p)lU;{;?yX(JI-32bIz7Jf6*gRQokO5U$X;@!
zdO(o>^&z-=d!M#xO)&Z1w3vzP2l0NbYvAGwNEksYT95Fm+&QE3DT|W^YQO(MDI8?5
zqQO6e9fqtSX{vQS*@k_uguP_LDy<I&cOZ(?ny~|t02jEqGbLe@zvH4ngkeo7l?uy`
zkNV*nT@FDVocqH`0NI`}&*(wruKGAXtV8`tTGr7EM(>N%E5nKhQNKVr0lz@6Az0H(
zE!Xgq*Au_bPu=u6`>5R%5;gfWI}jUxq#O?-U^VgFLA8tI<ffFDw}&|GMV0ImT-vu}
zl`2;bfPF?tNmH8ckTO1O)y>?O!;W2^D%MPO$aG%iS{#UQ62(^xPeUlGb*Q5?#T`pf
zg#(^>QS@25I|*FGT-%nwm8C%^tifB$er=;6ZQC-(YZE|&sBZoODM7niv(d^9l?cqS
zn)1-~ciwr}Pl7_Zj~?Ce903oX{Z1L-C|gmeI~&z!x}!^qrh?Ot<Gi}2>iHf7IdvVp
z?Z#&hn4L(!l@s4TC2ruUBl@+qA8Aj{bAQkPI7YTlO~9_86Yp78H_;`)@d{T<Gg#|~
z5*4K<bjF9rBNaR&RUw@yw*46di!KIw!lGil^LC$5CBx|fv<H_VRSttpcWB7iZ@*aS
zZ{VMbCf)I|;h%UwaKlU7<rhxYOoq#t-LxiE3HXQki)(Doui4C^&QbWFf=1LEjDVTj
z=P~gNk@MgyZ%`{>!YIs`KrA}{DeE&sjE&w29^P;kG^C@IE<%E&=9!fmAtnGo;9bX{
ztg2vd468{bD|yE-!X1>ym>UOUS->~la`9!Js`BVw69kJ7y!N!-xf~?0hfT9oSHn}X
z7u?t~{N-kcQW!yhvz<~1V-N%S=t{HC;^u`#i!7r3@L&>$u=k4RIIq1=UlkwzolAL`
zN6o007QxQV?^_6a&r9u*R9_9AD@b7iW|6b&Al>$K3v<7x>Fwwd%6?5m-ouJuR2?&E
zw}G?(q`pT?@gU-KI1KweBX)i9GtUznz0&R#0F4<9>1c+;q1ZfB?c`h(@!_fD6Y4gP
zD#7<ZEwRiQcs>g5DK{TbmwBwliuKI)eV|(FD^=UqkobzX8$kSw@E-k$RFB0&pZN{b
zt(|pKE@R=gH8sx&RAbjJp;%8owIpmWP9~mc#^q$QUHNqWKF$|$u;g$WabQ*!F>)x2
z_*r&2;>L=Ob_oxNR0CH&QZ%S(7<IQD-87N(?yk<`dz?rQ!omOS8#<m)XU^DPpbK+&
z($#1{Hi3OlnC-*@g_rQDxT~Tnwk6b2(v$D8WGQ{6Rm^O;0r<ELi~G?U*QuDcHcfMo
z@2-)`;NoyyOKa)6miCxA3!W9z8KHWm=6!En#!Qg(^0s6}B6CaYDQuPHTFV{Y=L8AE
zc=KI8KVo`r;o=40JOw?1(uG--6VO_pS|?s{>r|@NU(DB<%R4w!D8hpmPiIEC7dRq@
zn;M7HIJ_^nS|F6Dp~l2449aPq$gvII*anQva-NU^M!&4`*f-$nop(+G#a^6Bkc}1H
zl6F5-;wz?3sl!8h)E=D=FIvOhU`Q-nHtAJUN<WNHDucO@^Pnr=XLbQkPpB})Z?tPH
z+zbitbOAb-U*R3cI2Z{JaOY6J-;dDBPosKT)lLsiT)v&qXUg&ksIVFxLOK9pr8qci
z9{zQzOCXM%L1C9PWUX&g687yC*f%ET_`J;J4?DZXUeIC~`f`$b=aJh`-H^c@Bw>xC
zD6cWpEt8S!8fd$LxWmL0=p=BM#|$SV0UL4!$Ukj{t)XoO#Ix^zfvTpUdnYTgi;ASD
z(?#S^HSb@b6VN5SObW{W7ii>cq*~3Nq7vr+B*{+wV|!YVqE?rCxI)o|U3Wq4Ff{<#
zknZoeW`{Wu$&6=CI6pe75j}iazR=(eE6-!Q?GM8@zP85qE5`P7+H=|`nIOC`J5!AV
z;REC<W#ktKj{ycXX#r22d>izZ>ykQ$wf31l%8*%4O`hEm{R%}kNV}6?vhG8|wdi4I
z8?EPFnr#mnkC&W^T=zYc$9mpfvkS;ej}lB_I)%{?IFLVq1&W?qdD$Jt(_;MgitZMn
zk?YL{*l418<3fn}=fGV0FaV2TyN`%dEPYBO55_T`Sn(R9Z%Ur|0DohB?6e6&Nl!^-
z^X)Q234}c6yvu6hv1P@Ut49vq05Dj5S|t4dD|GQQtxw<PwhiCG_B1smY!jsfCoDUz
ztDhqbVNJNbp1zO$Jo5bh4H2fROWKVMA4IwGn4NKmMENSNZ@R^6bmH1<IYzrCVv-=w
z%U0Fw3%TI;cT^Td%HGfMr8W)5GY2HwXisuMW5U501IBMb+L{SBM|b*PIR&h5g#aA@
zRYv+<13uElMnEf#pr<P4EhW9f)h=xMDnzzssVoE^z4=)~ttp<keih}+pl~Lb<4`5&
zdpBb)2|yU>k_(PC;KMNu<b5Do<pU-N{3nu38{eI<X55?Z_jv7l;}uBfd)7*}qi1i^
zQ|v_QrR!kH>Jm@Zn#!Mlo9Wx>W&d)`c#M_*KyXG$ZDT@}ZovcEc4TESBa;!V<MKn0
zrgs&zLa)MjD1Ty6T_Q_O6#Kq{yf($!!pMoc+_$`<9+~X6Zn<ml6vgDB>hH!|-7C-%
z$-?%|K`$rZ!$#tsTG?lJzj*{d@6&r0^5!49uM{38wNa1s8sYcznBO^9<)Z4~&dfv{
z0Hs9c{{ms_^|zQ2O=FJz4uFYaaulEGGau^>0!K@vjF;DDJu~8YYZ>_751j<2>{Q9i
zBqnW)6|@dY3R;8jfAw0b&&1@$@QdN*lgq1KojiCh+md-=gz*LO!~2E>nI8R+Nnd<g
zhq?gXq2fu%N&Zlc9WL6v+q4V_Bw9_y5L`?MIhr>fDc|0_C;zA|5fCo+vk$+va6Hi!
zHs`jG8=phR4~zE9SR8Nj^%A>X#?p%>wT6~4PQ)D`{t~`&%pZ<HNdb}SQB|*i;;>cm
zo4VGkOb-0+G1`Va&alB~#Cw;%z~gKBNXzLNbS~ETM9+^+QmGd`Np!+-GP6rvz3H4>
z0Ud`x*UrFUV0Bx^fhi|RDUaEAG(R+4ogr9#&*wsJG03#P9j_VEiG(yG&`SxUTxZT`
zs30o<p{GN1Hk`|9byPHIK;cWHi0oiuSZi6bValZX-U)*{c<5uwc~^7plNoU5)DgMM
zP0#3)%GoxBsyFs3v<Myx+8gg@C=7@tw?T#B*a)dFss_^WNQm`?x}Tp%KG9$E&49gR
zcM#3-Nmi3HCO2aV*q>%a^OpgK?+5LP5MT=&8-+Ir9FX3L@WXb%XKk$N#2l7ncy(DD
z9n=ei5QE&<h<Q9fkPh>JZx;jpbyJ2wF7EcY9dHoUC@ZLW3Op;9vQI64A{}>q=g<Hz
zQi2hE(RcFvTsU=)z;%tyM*fJ$B|+ULUy(`{FdM!Um6?y&HhS&M+&qCFt+y~m=;(!)
zKU&8`EVe5(%<Vsq8~R2Xh>E7_@HOr?<C!sYnaLG`wxTJ)pbKI?&#$eQtI3TVkh(x2
zYV5n|Ik|W&y85*GP(CtNrk<h2BT|j~I)XSpyb1Yz9!Q~n4|o$4P7O#Je%BmVzp|Dw
z5f^IJ=UQX;nhh?45>{xHix}aK=doks045`G<GFhNYebu46Fd&Z5EoMgflZ2@3rT!M
zjMXdLZn@@Zfd~*V1cQ$?Z-~BnZ*8~SxseNhKKb>Exi@U8E8Mlc;1!3a?MNA8{7Q=*
zMhe;;0)z~m`QWg-#eLd*Oe)FV+CDZCg#;@s^zq0V1(L<nY^bX#8KV_`GF$IB(kBaa
zWEM|6-kh%klZZ1Bz>R?KzY%z>p>u`7&3Tt(5r-fFcsOy;7E&PWW<8Fx-a=|<baq>X
z{eYb=*#KPW2Jie@N1-1R^}je=G}y&#DssVxV4k`PquVE&-#0i*(rmWMyw0gn6cDgO
z%}ZknCYU^!tPe{53c%RWlLs6LO%|Ju$qKz(PS-(w3y(raz{wHAs2!>Az{N06>8<bi
z{teKi#iG}y`-vmqo{YIu$*21p_j>#Ih*gFJHH?REN0#t-PBU|gP$vJwq;=x`JSTCT
zbqSuHUm(W%R_#s+ECl?y+UE58FUg~yXh-^IlQlrmJt)9cY!K)pM!mBVJPt-9k~-j>
zqF*4}Rn+c^*Dp{+CP@PQb^9PH>AFIF%M&=uWqXP~c+KEA@Gs6A@H_W>6seYjMMv;A
zJw^N4y04>8%Ig4a<ob8c2bSQTQ+sbz$eQNh2cH9CpMIPLxQDMJ*uSg(0@a;v+tK8}
zd141?nbat|aPcnOFj|`1dH#%te}xXiOVh#HIl6NhekzSpEtq7vhMc++n@|?ilGZqM
zV`?LgKnL9FCaC>mQA%X`DpG#5N&!v}TgwXZ$_e5Uw2sP9tI?sxD-47Q;eJ$O;EcLO
zZYjZX3Io089-;b8HLl*(Hgu*Q`~6QDf4u&SF2f&{kbl?nAJvZkUBCaAy2U@KrvAIe
ZLH)leC;mIeLH+-6<?dg(KGeUa{s*dci825H

literal 42490
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

diff --git a/common/static/img/logo_square.svg b/common/static/img/logo_square.svg
index ccc0ff8d..76251fed 100644
--- a/common/static/img/logo_square.svg
+++ b/common/static/img/logo_square.svg
@@ -1 +1,193 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><title>logo</title><path d="M2.46,18h.32l.55,1,.18.38h0a5.66,5.66,0,0,1,0-.6V18h.29v1.75H3.45l-.54-1-.18-.38h0c0,.19,0,.4,0,.59v.77h-.3Z" style="fill:#606c76"/><path d="M5.45,18h.31v1.75H5.45Z" style="fill:#606c76"/><path d="M7.34,18.88a.82.82,0,0,1,.8-.91.67.67,0,0,1,.5.22l-.16.2a.47.47,0,0,0-.34-.15c-.28,0-.48.24-.48.63s.18.64.48.64a.49.49,0,0,0,.37-.18l.17.2a.73.73,0,0,1-.56.25A.8.8,0,0,1,7.34,18.88Z" style="fill:#606c76"/><path d="M10.2,18h1.05v.26h-.74v.45h.63V19h-.63v.52h.77v.26H10.2Z" style="fill:#606c76"/><path d="M12.86,18h.47a.78.78,0,0,1,.85.87.78.78,0,0,1-.84.88h-.48Zm.45,1.5c.35,0,.55-.2.55-.63s-.2-.62-.55-.62h-.14V19.5Z" style="fill:#606c76"/><path d="M15.76,18h.56c.36,0,.62.11.62.43a.39.39,0,0,1-.24.38h0a.41.41,0,0,1,.34.42c0,.35-.29.51-.67.51h-.61Zm.53.72c.25,0,.35-.09.35-.24s-.11-.24-.34-.24h-.23v.48Zm0,.79c.26,0,.4-.09.4-.29s-.14-.26-.4-.26h-.26v.55Z" style="fill:#606c76"/><path d="M18.53,19.57a.2.2,0,1,1,.4,0,.2.2,0,1,1-.4,0Z" style="fill:#606c76"/><path d="M20.46,18.87c0-.57.31-.9.77-.9s.77.34.77.9-.32.91-.77.91S20.46,19.43,20.46,18.87Zm1.22,0c0-.39-.18-.63-.45-.63s-.45.24-.45.63.17.64.45.64S21.68,19.26,21.68,18.87Z" style="fill:#606c76"/><path d="M23.58,18h.59c.36,0,.64.13.64.52s-.28.55-.64.55h-.28v.68h-.31Zm.56.82c.24,0,.37-.1.37-.3s-.13-.27-.37-.27h-.25v.57Zm0,.16.23-.18.54.95h-.35Z" style="fill:#606c76"/><path d="M26.3,18.88a.83.83,0,0,1,.83-.91.72.72,0,0,1,.52.22l-.16.2a.5.5,0,0,0-.36-.15c-.31,0-.51.24-.51.63s.18.64.53.64a.44.44,0,0,0,.25-.07v-.38h-.33V18.8h.61v.77a.8.8,0,0,1-.56.21A.81.81,0,0,1,26.3,18.88Z" style="fill:#606c76"/><path d="M6.06,17H6l-3.61-.76a.18.18,0,0,1-.14-.18V10.78a.16.16,0,0,1,.07-.14.19.19,0,0,1,.15,0l3.6.76a.18.18,0,0,1,.14.18v5.27a.18.18,0,0,1-.06.14A.19.19,0,0,1,6.06,17ZM2.64,15.91l3.24.68V11.68L2.64,11Z" style="fill:#00a1cc"/><path d="M6.06,17A.15.15,0,0,1,6,17a.19.19,0,0,1-.07-.14V11.54A.18.18,0,0,1,6,11.36l3.6-.76a.2.2,0,0,1,.15,0,.19.19,0,0,1,.07.14v5.27a.18.18,0,0,1-.15.18L6.1,17Zm.18-5.31v4.91l3.24-.68V11Zm3.43,4.37Z" style="fill:#00a1cc"/><path d="M24.69,17a3.4,3.4,0,1,1,3.4-3.4A3.41,3.41,0,0,1,24.69,17Zm0-6.44a3,3,0,1,0,3,3A3,3,0,0,0,24.69,10.58Z" style="fill:#00a1cc"/><path d="M24.69,14.93A1.31,1.31,0,1,1,26,13.62,1.31,1.31,0,0,1,24.69,14.93Zm0-2.26a1,1,0,1,0,.95.95A.95.95,0,0,0,24.69,12.67Z" style="fill:#00a1cc"/><path d="M17.73,16H11.42a.18.18,0,0,1-.18-.18V11.39a.18.18,0,0,1,.18-.18h6.31a.18.18,0,0,1,.18.18v4.46A.18.18,0,0,1,17.73,16Zm-6.13-.36h6v-4.1h-6Z" style="fill:#00a1cc"/><path d="M19.94,16l-.08,0L17.65,14.9a.18.18,0,0,1-.1-.16V12.5a.18.18,0,0,1,.1-.16l2.21-1.11a.17.17,0,0,1,.17,0,.19.19,0,0,1,.09.15v4.46A.19.19,0,0,1,20,16Zm-2-1.4,1.85.92V11.69l-1.85.92Z" style="fill:#00a1cc"/></svg>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="220.10335"
+   height="220.69331"
+   viewBox="0 0 220.10334 220.69331"
+   data-svgdocument=""
+   id="_3tPmdtZQ0LGlljQGEkT-3"
+   class="fl-svgdocument"
+   x="0"
+   y="0"
+   version="1.1"
+   sodipodi:docname="logo-square.svg"
+   inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview21"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     units="px"
+     fit-margin-top="42"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="42"
+     inkscape:zoom="1.4707031"
+     inkscape:cx="282.51793"
+     inkscape:cy="94.172644"
+     inkscape:window-width="1792"
+     inkscape:window-height="1067"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="_3tPmdtZQ0LGlljQGEkT-3"
+     inkscape:document-units="mm"
+     inkscape:snap-global="false" />
+  <defs
+     id="_SabXoCmWUJmxvMwSINRGH"
+     transform="matrix(1.1728515134752755, 0, 0, 1.1728515134752755, -34.46172819025348, -40.976997984526726)">
+    <rect
+       x="86.353256"
+       y="25.158035"
+       width="217.58301"
+       height="60.515274"
+       id="rect4965" />
+  </defs>
+  <g
+     id="_WTrHMt3eZfxcQKrYJirRK"
+     transform="matrix(0.95294163,0,0,0.95294163,-45.741197,-148.14988)">
+    <path
+       id="_1MYZsbAYvxe5D-50H3pho"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.519653,192.79657)"
+       data-type="polygon"
+       d="M 82.1,74 75,81.1 v 0 L 82.1,74 74,65.9 v 0 z" />
+    <path
+       id="_oL3QJlwkbW1Nmdh3qAlDC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.45702,193.17499)"
+       data-type="rect"
+       data-x="64.5"
+       data-y="51.5"
+       data-width="0"
+       data-height="9.8"
+       d="M 64.5,51.5" />
+    <path
+       id="_sK1htog-8Op55rgRI5hDg"
+       d="m 61.1,52.9 v 0 0 l -4.9,4.9 c 2.5,1.2 4.3,3.8 4.3,6.9 0,4.2 -3.4,7.6 -7.6,7.6 -3,0 -5.6,-1.8 -6.9,-4.3 L 40.1,73.9 40,74 61,95 69.1,86.9 v 0 c -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 l 7.1,-7.1 -8.1,-8 v 0 l -0.6,-0.6 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 0,-1.5 -0.6,-2.8 -1.5,-3.9 -1,-0.9 -2.4,-1.5 -3.9,-1.5 -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 68,59.9 v 0 z"
+       fill="#083b66"
+       transform="matrix(1.2602785,0,0,1.2602785,27.51993,192.73364)"
+       style="fill:#ffb380" />
+    <path
+       id="_OMWHsyuaoclMfPE9tvM7V"
+       d="m 46.5,49.5 c -4.2,0 -7.6,-3.4 -7.6,-7.6 0,-3 1.7,-5.6 4.2,-6.8 l -5.9,-5.9 v 0 l -21,21 22.2,22.2 9.5,-9.5 c -0.3,0.6 -0.4,1.3 -0.4,2 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-2.9 -2.4,-5.3 -5.3,-5.3 -0.7,0 -1.4,0.1 -2,0.4 l 8.6,-8.6 -6,-6 c -1.3,2.3 -3.9,4.1 -6.9,4.1 z"
+       fill="#8dcaff"
+       transform="matrix(1.2602785,0,0,1.2602785,27.583487,192.79709)" />
+    <path
+       id="_jAk8zpl6O80_21e1JT4hk"
+       d="m 83.3,27.4 -8.7,-8.7 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 C 80.9,9.6 78.3,7 75,7 c -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 61,5.2 l -21,21 -0.7,0.8 -0.4,0.4 9.5,9.5 c -0.6,-0.2 -1.2,-0.3 -1.8,-0.3 -2.9,0 -5.3,2.4 -5.3,5.3 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-0.5 -0.1,-0.9 -0.2,-1.4 0,-0.2 -0.1,-0.3 -0.1,-0.5 l 0.5,0.5 9.2,9.2 0.4,-0.4 0.8,-0.8 6.8,-6.8 c 0,0 0,0 0,0 -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 0,0 0,0 0,0 z"
+       fill="#c0d9b4"
+       transform="matrix(1.2602785,0,0,1.2602785,27.709267,192.98645)" />
+    <path
+       id="_GDSYpBtYoW2GJpRZoPCot"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_NldltLD5fZQlQrgFsakaC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_TbX_b0jxU8J0Cb3YWVDy6"
+       d="m 51.6,40.5 h 0.3 L 51.5,40 c 0,0.1 0,0.3 0.1,0.5 z"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.708767,192.98587)" />
+  </g>
+  <g
+     aria-label="NeoDB"
+     transform="matrix(1.5308318,0,0,1.7424549,-43.057047,-3.2641996)"
+     id="text4963"
+     style="font-size:40px;line-height:1.25;white-space:pre;shape-inside:url(#rect4965);fill:#083b66">
+    <path
+       d="m 107.58017,70.426948 c 0,-1.866665 -0.85333,-6.026663 -1.28,-7.786662 -0.10667,-0.426666 0,-0.853333 -0.10667,-1.279999 -0.90666,-3.786665 -1.44,-7.733329 -1.92,-11.573327 -0.15999,-1.546665 -0.58666,-3.039998 -0.58666,-4.586664 0,-0.266666 -0.10667,-1.919998 0.42667,-1.919998 0.74666,0 0.90666,1.226666 1.59999,1.226666 0.26667,0 0.64,-0.106667 0.64,-0.426667 0,-1.013333 -1.92,-1.866665 -2.82666,-1.866665 -3.09333,0 -2.93333,3.626664 -2.93333,5.919996 0,2.079999 0.32,4.373331 0.58666,6.45333 0.16,1.279999 0.10667,2.559998 0.32,3.839997 0.16,1.12 0.64,2.719999 0.64,3.839998 0,0.906666 0.16,1.706666 0.26667,2.559998 -0.48,-0.746666 -0.69333,-1.493332 -1.12,-2.239998 -0.64,-1.066666 -1.28,-2.186666 -1.813332,-3.306665 -0.373333,-0.746666 -0.8,-1.439999 -1.226666,-2.186665 -0.426667,-0.746667 -0.693333,-1.546666 -1.119999,-2.293332 -1.919999,-3.253332 -3.626665,-6.666663 -5.493331,-9.919994 -0.213333,-0.373333 -0.693332,-0.426667 -1.066666,-0.426667 -0.693332,0 -2.933331,0.533333 -3.039998,1.333333 -0.16,1.333332 -0.16,2.719998 -0.16,4.053331 v 4.53333 c 0,1.013333 0.106667,1.973332 0.106667,2.933332 0,1.386666 0.16,2.719998 0.16,4.053331 0,0.799999 0.16,1.546666 0.16,2.346665 0,1.333333 0.05333,2.826665 0.266666,4.159998 0.106667,0.533333 0.213333,1.279999 0.213333,1.866665 0,2.399999 -0.266666,4.053331 2.826665,4.053331 0.8,0 3.093332,-0.426666 3.093332,-1.439999 0,-0.48 -0.16,-0.959999 -0.16,-1.493333 0,-2.666665 -0.213333,-5.759996 -0.64,-8.426661 -0.159999,-1.119999 -0.426666,-2.559999 -0.426666,-3.679998 0,-2.559998 -0.693333,-4.746664 -0.693333,-7.413329 1.279999,2.933332 9.599998,20.853321 11.786658,20.853321 0.8,0 3.52,-0.799999 3.52,-1.759999 z M 90.246847,53.573625 c 0.266666,0.106666 0.266666,1.333332 0.266666,1.546666 0,1.119999 -0.16,2.239998 -0.16,3.359998 -0.16,-1.493333 -0.16,-2.986665 -0.16,-4.479998 0,-0.16 0,-0.266666 0.05333,-0.426666 z m 0.266666,18.453322 0.05333,-0.106666 c 0,-0.373334 -0.106666,-0.746667 -0.106666,-1.12 v -0.48 c 0.05333,0.266667 0.106666,0.906667 0.32,1.12 0,-0.64 -0.16,-1.226666 -0.16,-1.866666 0,-0.213333 0,-0.639999 0.213333,-0.799999 0,1.119999 0.05333,2.293332 0.05333,3.413331 l -0.16,-0.16 v 0.106667 h -0.213333 z m -0.266666,-7.199996 c -0.05333,-0.266666 -0.05333,-0.479999 -0.05333,-0.746666 v -0.693333 c 0.213334,-0.266666 0.16,-2.453332 0.16,-2.719998 0.106667,0.426666 0.16,1.813332 0.16,2.346665 0,0.266667 0,1.653333 -0.266666,1.813332 z m -1.28,-15.83999 c 0.106667,-0.64 0.05333,-1.333333 0.05333,-1.973332 0.213333,1.706665 0,3.413331 0.426666,5.066663 l -0.106666,0.05333 -0.213334,-1.066666 c 0,0.533333 0.05333,1.119999 0.05333,1.653332 l -0.05333,-0.05333 v -0.533333 c -0.05333,-0.106667 -0.16,-0.32 -0.16,-0.426667 0,-0.32 0.05333,-0.639999 0.05333,-0.959999 l 0.106666,0.32 c -0.05333,-0.693333 0,-1.386666 -0.16,-2.079999 z m 0.266667,12.373326 c 0.05333,-0.373333 -0.05333,-2.239999 -0.16,-2.559999 0.16,-0.266666 0.16,-1.493332 0.16,-1.813332 0.106667,0.213333 0.106667,1.813332 0.106667,2.133332 0,0.426667 0,1.866666 -0.106667,2.239999 z m 14.613326,-0.373333 c -0.0533,0 -0.10667,-0.693333 -0.10667,-0.746667 -0.0533,0.106667 -0.0533,0.266667 -0.0533,0.373334 0,0.106666 0.0533,0.213333 0.0533,0.319999 l -0.0533,0.05333 h 0.0533 c 0,0.32 0.0533,0.639999 -0.0533,0.959999 0,-0.533333 -0.0533,-1.066666 -0.0533,-1.599999 v -0.64 0.266667 c 0.0533,-0.213333 0.0533,-0.426666 0.0533,-0.64 0.16,0.05333 0.16,1.173333 0.16,1.386666 z m -0.10667,-3.466665 c 0,0.213333 -0.0533,0.373333 0,0.586666 v -0.319999 c 0.0533,0.16 0.10667,0.373333 0.10667,0.586666 0,0.213333 -0.0533,0.48 -0.16,0.693333 0,-0.213333 0,-0.426667 -0.0533,-0.64 v 0.266667 -1.973332 c 0,0.05333 0.10666,0.213333 0.10666,0.266666 z m -13.546657,1.493333 c -0.106666,-0.373334 -0.16,-0.853333 -0.106666,-1.226666 l -0.05333,0.16 c 0.106667,-0.586667 -0.05333,-1.12 -0.05333,-1.706666 0,-0.213333 0,-0.426667 0.106667,-0.64 0.05333,1.12 0.05333,2.293332 0.106666,3.413332 z m 0.16,12.586659 c -0.05333,0.16 -0.05333,0.32 -0.05333,0.479999 l -0.16,-0.05333 c 0,-0.266666 -0.05333,-1.119999 0.16,-1.333332 0,0.159999 -0.05333,0.426666 0.05333,0.533333 0.05333,0.266666 0.106667,0.586666 0.106667,0.853332 v 0.05333 L 90.353513,72.08028 Z M 89.926847,57.733622 c 0,0.16 0,0.266667 0.05333,0.426667 -0.16,0.799999 -0.05333,1.759999 -0.05333,2.506665 -0.106667,-0.48 -0.106667,-0.96 -0.106667,-1.493333 0,-0.479999 0,-0.959999 0.106667,-1.439999 z m 0.05333,4.053331 c 0,-0.426666 0.106667,-0.799999 0,-1.279999 l 0.16,0.16 v 0.959999 c 0,0.373333 0,0.8 -0.05333,1.173333 -0.05333,-0.32 -0.106667,-0.693333 -0.106667,-1.013333 z m 0.586666,-11.093326 c 0,0.266666 -0.05333,0.479999 -0.106666,0.746666 -0.05333,-0.05333 -0.05333,-0.16 -0.05333,-0.266667 V 50.58696 c 0,-0.266667 0,-0.48 0.05333,-0.746666 0.05333,0.213333 0.05333,0.479999 0.05333,0.693333 h 0.05333 z m -0.05333,17.173323 c 0,0.213333 0,0.426666 -0.05333,0.639999 -0.05333,-0.213333 -0.05333,-0.479999 -0.05333,-0.693333 V 67.22695 c 0,-0.106667 0,-0.213333 0.05333,-0.32 0.05333,0.32 0.106666,0.64 0.106666,0.96 z m 13.706657,-3.679998 0.16,0.32 c 0,0.213333 -0.10667,0.373333 -0.21333,0.533333 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.16 0,-0.266667 0.0533,-0.426667 z m -0.32,-0.16 c 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 l 0.0533,-0.106667 -0.0533,0.05333 c 0,-0.106666 -0.0533,-0.159999 -0.0533,-0.266666 v -0.64 l 0.10666,-0.05333 c 0,0.48 0,0.906666 0,1.386666 z M 89.180181,54.213624 c -0.05333,-0.16 -0.106667,-0.319999 -0.106667,-0.533333 0,-0.266666 0.05333,-0.586666 0.05333,-0.906666 0.05333,0.16 0.05333,0.373333 0.05333,0.533333 z M 102.78017,51.22696 c -0.0533,0.373333 0.21334,0.853332 0.21334,1.226665 v 0.05333 c -0.16,-0.373333 -0.21334,-0.8 -0.37334,-1.226666 z m -11.359991,8.799994 c -0.05333,-0.32 -0.05333,-0.639999 -0.05333,-0.959999 v -0.373333 c 0.05333,0.213333 0.106667,0.586666 0.106667,0.799999 0,0.16 -0.05333,0.373333 -0.05333,0.533333 z m -2.239998,-4.319997 -0.106667,0.266666 v -0.266666 c 0,-0.213333 0,-0.373333 0.05333,-0.533333 0.05333,0.106666 0.05333,0.213333 0.05333,0.32 l 0.106666,-0.106667 v 0.426666 l -0.05333,-0.05333 -0.05333,0.213333 z m 0.213333,-1.226666 c 0,-0.05333 -0.106667,-0.586666 0,-0.48 -0.05333,-0.266666 0,-0.693333 -0.106667,-0.906666 l 0.106667,0.05333 c 0,0.32 0.05333,0.586667 0.05333,0.906666 0,0.16 0,0.32 -0.05333,0.426667 z m 0.906666,-6.399996 c 0,0.32 0,0.639999 0,0.959999 l -0.106667,-0.48 c 0,-0.106666 0.05333,-0.319999 0.106667,-0.479999 z m 12.74666,5.599996 c 0,0.213334 -0.0533,0.373333 -0.0533,0.586667 -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 v -0.32 c 0.0533,0.106667 0.10667,0.266666 0.10667,0.426666 z m 1.06667,8.693329 0.0533,-0.05333 v -0.26667 l 0.10666,0.32 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 l -0.10666,-0.213334 v 0.32 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0,-0.16 0,-0.373333 0.0533,-0.533333 z m -13.81333,6.613329 c 0,0.266666 -0.05333,0.906666 0.05333,1.226666 l -0.05333,0.106666 c -0.106667,-0.106666 -0.05333,-1.226665 0,-1.333332 z m 0.32,-2.399999 c -0.106667,0 -0.16,0.106667 -0.16,0.213334 0.05333,-0.266667 -0.05333,-0.586667 0,-0.746667 l 0.106666,0.05333 v 0.213333 h 0.05333 z m 12.53333,-8.746661 c 0.0533,0.266666 0.0533,0.586666 0,0.853333 -0.0533,-0.32 -0.0533,-0.533333 0,-0.853333 z m -0.53334,-7.253329 c 0.0533,-0.213333 0.0533,-0.426667 0.0533,-0.64 l 0.0533,0.05333 v 0.693333 z m -13.386656,6.346663 c 0,-0.16 0,-0.373333 -0.106667,-0.533333 l 0.106667,-0.266667 c 0,0.16 0,0.373333 0,0.533333 z m 1.333332,-4.853331 c -0.05333,-0.16 -0.106666,-0.319999 -0.106666,-0.479999 0,0.106666 0.05333,0.16 0.16,0.16 0,0.106666 -0.05333,0.159999 -0.05333,0.319999 0,0 0.05333,-0.05333 0.05333,-0.05333 v 0.05333 z m -0.319999,-4.479997 c -0.106667,-0.32 -0.106667,-0.693333 -0.106667,-1.066666 z m 0.16,11.839993 c 0.159999,0.05333 0.05333,0.373333 0,0.48 z m 0.213333,-6.45333 c 0,0.106667 -0.05333,0.266667 -0.05333,0.373334 v -0.48 z m -0.213333,5.493331 c 0,0.05333 0.159999,0.426666 0,0.426666 z m 0.159999,6.666662 c 0.05333,0.05333 0.05333,0.16 0.05333,0.266667 l -0.05333,0.05333 z m 13.866654,-2.773331 0.0533,0.05333 c 0,0.213333 0,0.426666 -0.0533,0.639999 z M 90.353513,53.200292 c 0,0.106666 0,0.16 -0.05333,0.266666 0,-0.106666 -0.05333,-0.266666 -0.05333,-0.373333 z m 0.16,2.879998 v -0.586666 c 0.05333,0.213333 0.05333,0.373333 0.05333,0.586666 z M 104.9135,70.160282 c 0,0.05333 0.0533,0.106666 0.10667,0.159999 l -0.10667,0.05333 h -0.0533 l -0.0533,-0.05333 z m -0.69333,-9.066662 c 0,0.16 0.10667,0.32 -0.0533,0.48 0,-0.16 0,-0.32 0.0533,-0.48 z m -13.973323,4.159998 c 0,-0.106667 0,-0.213333 0.05333,-0.32 l 0.05333,0.16 c 0,0.106667 -0.05333,0.16 -0.106666,0.16 z m 0.319999,-2.933332 c 0,0.16 0,0.373333 -0.05333,0.533333 v -0.533333 z m 0,-10.18666 v 0.106666 l 0.05333,0.106667 -0.05333,0.05333 v -0.16 l -0.106666,0.05333 v -0.05333 z m 14.079994,11.466659 c 0,-0.106666 0,-0.159999 0.0533,-0.266666 v 0.32 z m -2.13333,-11.893326 c 0,-0.106666 0,-0.16 0.0533,-0.266666 0,0.106666 0,0.16 0.0533,0.266666 z M 90.46018,69.840282 c 0,-0.106667 0,-0.213333 0.05333,-0.32 v 0.213333 z m 0.32,-1.333333 c 0,0.05333 0.05333,0.106667 0.05333,0.106667 0,0.05333 -0.05333,0.106666 -0.05333,0.106666 l -0.05333,-0.05333 c 0,-0.05333 0,-0.106666 0.05333,-0.16 z m -1.386666,-3.466664 c -0.05333,-0.106667 -0.05333,-0.266667 -0.05333,-0.373333 0.05333,0.106666 0.05333,0.266666 0.05333,0.373333 z m 14.933326,0.586666 0.0533,-0.106667 v 0.266667 z m -0.21333,-5.119997 c -0.0533,-0.16 -0.0533,-0.266667 0,-0.373333 z m 0,4.746664 h 0.0533 v 0.16 l -0.0533,-0.05333 z m -13.386664,6.293329 c 0,0.05333 0,0.16 0.05333,0.213334 0,-0.05333 0,-0.16 -0.05333,-0.213334 z m -0.213333,-14.773324 0.05333,0.106666 c 0,0.05333 0,0.05333 -0.05333,0.106667 z m -1.333332,-2.186665 c -0.05333,-0.05333 -0.05333,-0.16 0,-0.266667 z m 15.199989,11.946659 c -0.0533,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.0533,-2.613332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 0,-0.05333 0.0533,-0.05333 0.0533,-0.05333 z m -0.21333,-3.199998 -0.0533,-0.05333 0.0533,-0.106667 z m 0.16,5.33333 c -0.0533,-0.05333 -0.0533,-0.16 0,-0.213333 z m -1.44,-13.173325 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0.0533,0.05333 0.0533,0.106667 0.0533,0.16 z m -13.599989,0 c 0.05333,0.05333 0.05333,0.106666 0.05333,0.16 -0.05333,-0.05333 -0.05333,-0.106667 -0.05333,-0.16 z m 14.773329,8.373328 -0.0533,-0.106666 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m -13.599997,7.679996 v -0.106667 h 0.05333 z m -1.119999,-4.906664 v -0.106667 z m 1.173332,1.973332 v 0.106667 z m 13.119994,-8.533328 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -14.613326,-0.16 v -0.05333 h 0.05333 v 0.05333 z m 1.493332,-0.693333 c 0,0 -0.05333,-0.05333 -0.05333,-0.05333 0,0 0.05333,-0.05333 0.05333,-0.05333 z m 13.599994,7.413329 v -0.05333 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106666 z M 89.446847,63.653619 h -0.05333 v -0.05333 z m 15.253323,-0.533333 h -0.0533 l 0.0533,-0.05333 z m -13.91999,4.959997 0.05333,0.05333 h -0.05333 z m 13.38666,-7.146663 0.0533,-0.05333 v 0.05333 z M 91.366846,71.653614 h 0.05333 l -0.05333,0.05333 z M 104.06017,61.57362 v -0.05333 h 0.0533 z m 0.0533,-1.546666 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -14.719996,-5.33333 0.05333,0.05333 h -0.05333 z m 15.626656,15.626657 v -0.05333 l 0.0533,0.05333 z m -2.02666,-15.306657 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z m -12.479997,6.826663 0.05333,-0.05333 v 0.05333 z M 103.63351,60.93362 c 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 1.33333,8.906662 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path920" />
+    <path
+       d="m 126.07266,60.666954 c 0,-0.266667 -0.16,-0.586666 -0.48,-0.586666 -0.58667,0 -0.64,0.693332 -0.69333,1.119999 -0.42667,2.506665 -2.77334,7.306662 -5.70667,7.306662 -1.76,0 -1.97333,-0.479999 -3.14666,-1.706665 -0.32,-0.96 -0.96,-1.919999 -0.96,-2.986665 v -0.106667 c 0.37333,-0.106667 0.69333,-0.373333 1.01333,-0.586666 1.97333,-1.12 3.94667,-2.613332 3.94667,-5.119997 0,-1.706666 -1.44,-3.413331 -3.2,-3.413331 -4.16,0 -5.6,2.773331 -6.29333,6.293329 -0.10667,0.533333 -0.32,1.173333 -0.32,1.706666 0,1.706665 0.53333,3.253331 1.01333,4.85333 0.16,0.64 2.08,2.506665 2.66667,2.879998 0.8,0.533333 2.02666,0.8 2.98666,0.8 0.64,0 2.82667,-0.266667 3.30667,-0.746666 3.30666,-1.066666 5.86666,-6.45333 5.86666,-9.706661 z m -9.28,-4.213331 c 0.74667,0 1.33334,0.746666 1.33334,1.439999 0,1.973332 -1.65334,3.306665 -3.25333,4.159998 -0.0533,-0.213333 -0.10667,-0.96 -0.10667,-1.173333 v -0.64 c 0,-1.226665 0.42667,-3.786664 2.02666,-3.786664 z m -3.67999,4.266664 c 0,-0.16 0,-0.32 -0.0533,-0.48 v 0.48 l 0.0533,0.05333 v 0.266667 c -0.0533,-0.106667 -0.0533,-0.213333 -0.0533,-0.32 v 0.586667 l 0.0533,-0.106667 c 0.10666,0.213333 0.16,0.426666 0.16,0.64 0,0.159999 -0.0533,0.319999 -0.0533,0.426666 -0.0533,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.106667 0.0533,-0.426667 -0.10667,-0.48 -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 -0.10667,-0.586666 -0.16,-1.279999 -0.21333,-1.973332 -0.0533,0.426666 -0.0533,0.959999 -0.10667,1.013332 0,-0.16 -0.0533,-0.319999 -0.10667,-0.479999 l 0.0533,-0.106667 c -0.0533,0 -0.10667,-0.05333 -0.10667,-0.16 v -0.05333 h 0.16 v -0.213333 c 0,-0.106667 0,-0.266667 -0.0533,-0.373333 l -0.0533,0.213333 v -0.266667 l -0.0533,-0.106666 v -0.05333 l 0.16,-0.05333 c 0,-0.106667 0,-0.213333 -0.0533,-0.32 l 0.0533,-0.106666 c 0,0 0.0533,0.106666 0.0533,0.213333 0,-0.32 0.0533,-0.64 0.32,-0.906666 v 0.479999 l 0.0533,-0.05333 v 0.213333 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0.0533,0.213333 0.10666,0.426666 0.10666,0.639999 0,0.32 0,0.906666 -0.10666,1.226666 z m 1.54666,7.626662 c -0.0533,0.16 -0.0533,0.32 -0.10666,0.48 l -0.16,-0.16 c 0,-0.373333 0.0533,-0.853333 0.16,-1.173333 0.0533,0.213334 0.10666,0.586667 0.10666,0.8 L 114.766,68.02695 v -0.213334 c 0.16,0.16 0.32,0.32 0.42667,0.48 -0.0533,0.16 -0.0533,0.373333 -0.21334,0.533333 0,0.106667 0.0533,0.213333 0.0533,0.266667 l -0.10667,-0.05333 v -0.479999 c -0.0533,0.159999 -0.0533,0.319999 -0.0533,0.479999 l -0.10666,-0.10667 c 0.0533,-0.106667 0.0533,-0.16 0.0533,-0.266667 l -0.0533,-0.05333 -0.0533,0.16 c 0,-0.16 0,-0.32 -0.0533,-0.426667 z m -1.76,-1.439999 c -0.0533,-0.32 -0.0533,-0.693333 -0.0533,-1.013333 v -1.866665 l -0.0533,0.106666 v -0.106666 l -0.0533,-0.106667 h 0.10667 v -0.639999 l 0.0533,-0.106667 c 0,0.64 0.0533,1.226666 0.0533,1.866666 v 1.013332 c 0,0.266667 0,0.586667 -0.0533,0.853333 z m 1.17334,1.066666 c 0,-0.106666 0,-0.16 0.0533,-0.266666 v -0.48 l -0.0533,0.05333 c 0,-0.266666 0.0533,-0.479999 0.0533,-0.746666 0.0533,0.05333 0.16,0.32 0.16,0.373333 v 0.64 c 0,0.213333 -0.0533,0.48 -0.10667,0.693333 z M 113.006,59.546955 c 0.0533,-0.16 0,-0.32 -0.10667,-0.48 v 0.213333 c 0,0.32 0,0.64 0.0533,0.959999 0.0533,-0.106666 0.10666,-0.159999 0.10666,-0.266666 l 0.0533,0.16 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,-0.16 -0.10667,-0.373333 -0.10667,-0.533333 z m -0.32,3.519997 c -0.0533,-0.159999 -0.10667,-1.013332 0,-1.173332 z m 0.90667,-5.49333 c 0,0.16 0,0.32 -0.0533,0.426667 l -0.0533,0.05333 v -0.373333 z m -1.49334,6.346663 c 0.0533,0.32 0.0533,0.586667 0.0533,0.906666 l -0.0533,-0.106666 z m 0.69334,-3.999997 c 0,-0.16 0,-0.32 0,-0.426667 0,-0.05333 0,-0.266666 -0.0533,-0.426666 0,0.266666 0,0.533333 0.0533,0.853333 z M 112.206,61.41362 c -0.0533,-0.16 -0.0533,-0.32 -0.0533,-0.48 l 0.0533,-0.106666 z m 0.32,-0.64 c 0.0533,0.106667 0.0533,0.266667 0,0.373334 l -0.0533,-0.213334 z m 0.10667,1.12 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 v 0.32 z m -0.74667,0.426666 c 0,0.106667 0.0533,0.213333 0,0.32 z m 2.08,4.586664 -0.0533,-0.106666 0.0533,-0.05333 z m 0,0.373333 -0.0533,-0.106666 0.0533,-0.05333 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path922" />
+    <path
+       d="m 134.04765,67.86695 c 1.17334,-0.64 1.92,-1.866666 2.4,-3.093332 1.38667,-0.533333 4.69333,-2.399998 4.69333,-4.106664 0,-0.213333 -0.21333,-0.426667 -0.42666,-0.426667 -0.32,0 -0.58667,0.16 -0.74667,0.426667 -0.8,1.226666 -1.92,2.239999 -3.30667,2.773332 0,-2.079999 -0.63999,-4.159998 -2.13333,-5.65333 -0.48,-0.48 -2.50666,-1.706666 -3.2,-1.706666 -0.26666,0 -0.58666,0 -0.85333,0 -0.96,0 -1.97333,0.05333 -2.77333,0.586666 -0.42667,0.266667 -0.53333,0.586667 -0.85333,0.906666 -1.44,1.599999 -2.18667,2.133332 -2.18667,4.533331 0,3.946664 3.09333,6.45333 6.82666,6.45333 0.32,0 2.24,-0.533333 2.56,-0.693333 z m -4.95999,-9.386661 c 0,-0.373334 0.26666,-1.44 0.8,-1.44 2.29333,0 2.93333,4.906664 3.14666,6.506663 -1.97333,-0.746666 -2.88,-1.706665 -3.52,-3.733331 -0.10666,-0.426666 -0.42666,-0.906666 -0.42666,-1.333332 z m 2.55999,8.159995 c -1.65333,0 -1.91999,-2.826665 -2.34666,-3.946665 1.22667,1.546666 1.86666,1.706666 3.68,2.293332 -0.21334,0.746667 -0.32,1.653333 -1.33334,1.653333 z m 2.98667,-2.986665 c 0,-0.426667 -0.10667,-0.8 -0.10667,-1.226666 v -0.106667 c 0.10667,-0.106666 0.0533,-0.799999 0,-0.906666 h -0.0533 v 0.05333 l -0.0533,0.05333 c 0.0533,-0.16 0.10666,-0.373333 0.16,-0.533333 h 0.10666 c 0.0533,0.373333 0.10667,0.746666 0.10667,1.066666 0,0.213333 -0.0533,0.426666 -0.10667,0.586666 0,0.373333 0.10667,0.693333 -0.0533,1.013333 z m -7.2,0.106666 c 0.21334,0.213334 0.16,0.64 0.16,0.906667 v 0.853332 c -0.10666,-0.373333 -0.16,-0.799999 -0.16,-1.226666 0.10667,-0.106666 -0.0533,-0.319999 -0.0533,-0.426666 0,-0.05333 0.0533,-0.106667 0.0533,-0.106667 z m 7.25333,1.066666 c 0,-0.213333 0.0533,-1.333332 0.10667,-1.439999 0,0.05333 0.0533,0.106667 0.0533,0.16 0,-0.05333 0,-0.05333 0.0533,-0.106666 v 0.213333 c 0,0.373333 -0.0533,0.693333 -0.10667,1.013333 z m 0.26667,-0.693333 c 0,-0.266666 0.0533,-0.533333 -0.0533,-0.799999 0.10666,-0.426666 0.0533,-0.959999 0.0533,-1.386666 0.0533,0.213333 0.10667,0.426667 0.10667,0.64 0,0.373333 0,1.386666 -0.10667,1.546665 z m -0.16,-0.799999 v -0.05333 l 0.0533,0.05333 z m -0.21333,-2.079999 0.0533,0.05333 v -0.05333 z m 0.0533,1.439999 v -0.106666 z m -7.14666,-0.639999 v -0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path924" />
+    <path
+       d="m 155.71431,60.400287 c 0,-1.653332 -0.21334,-5.386663 -0.8,-6.879996 -0.16,-0.373333 0,-0.853332 -0.16,-1.226665 -0.26667,-0.8 -0.42667,-1.599999 -0.74667,-2.399999 -1.17333,-2.986665 -3.46666,-8.159995 -7.30666,-8.159995 -1.44,0 -3.30666,0.746666 -4,2.133332 -0.26666,-0.266667 -0.69333,-0.533333 -1.06666,-0.533333 -1.97334,0 -1.86667,0.906666 -1.86667,2.346665 0,0.426667 -0.10667,0.853333 -0.10667,1.279999 v 11.679993 c 0,0.853333 0.16,1.706666 0.21334,2.559999 0.16,2.613332 0.26666,5.119997 0.58666,7.733329 -0.26666,0.16 -0.48,0.426666 -0.48,0.746666 0,0.48 0.37334,0.533333 0.69334,0.799999 0.42666,1.333333 -0.16,1.653333 1.33333,2.239999 0.37333,0.16 0.8,0.32 1.22667,0.32 1.43999,0 2.61333,-0.32 2.61333,-2.026666 6.34666,-0.799999 9.86666,-4.21333 9.86666,-10.613327 z m -9.81333,9.013328 c 0,-2.079998 0.0533,-4.21333 -0.0533,-6.293329 -0.0533,-0.586667 -0.16,-1.12 -0.16,-1.706666 0,-2.933331 -0.64,-5.919996 -0.64,-8.853328 0,-1.546666 -0.21334,-3.093331 -0.21334,-4.639997 0,-1.226666 -0.0533,-4.853331 1.70667,-4.853331 0.64,0 1.12,0.64 1.44,1.173333 2.24,3.679998 3.25333,10.399994 3.30666,14.613325 0,0.426666 0.16,0.906666 0.16,1.333332 0,3.039998 -0.53333,6.45333 -3.14666,8.266662 -0.53333,0.373333 -0.53333,0.586666 -1.17333,0.746666 -0.42667,0.106667 -0.85334,0.05333 -1.22667,0.213333 z m -4.42666,-6.879996 c 0,-1.333332 -0.21334,-2.613331 -0.21334,-3.946664 v -4.85333 c 0.37334,0.639999 0.37334,3.679997 0.37334,4.479997 0,1.226666 -0.0533,2.506665 -0.10667,3.733331 h 0.0533 v 0.48 z m 2.4,-4.266664 v 1.066666 l -0.0533,0.05333 h 0.0533 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.319999 -0.16,0.319999 0,-0.266666 -0.0533,-0.533333 -0.0533,-0.799999 0,-0.853333 0.26667,-2.079999 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.10666,-0.213333 0.10666,-0.479999 0.16,-0.693333 v 0.106667 l 0.0533,0.106667 c 0,0.693332 0.16,1.493332 0.16,2.186665 l -0.0533,0.05333 h 0.0533 c 0,0.106667 0.0533,0.16 0.0533,0.266667 v 0.373333 l -0.10666,0.106666 0.0533,-0.159999 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 l -0.0533,-0.16 z m -2.13334,6.77333 0.0533,0.05333 v -0.05333 c 0.0533,0.106666 0.0533,0.213333 0.16,0.319999 -0.16,-0.586666 0.0533,-1.119999 0.0533,-1.653332 l 0.0533,-2.666665 c 0.10667,0.106667 0.10667,0.32 0.10667,0.48 0,0.16 -0.0533,0.373333 0,0.533333 0.16,-0.32 0.0533,-0.426667 0.0533,-0.746666 0.0533,0.426666 0.0533,0.906666 0.0533,1.333332 0,1.013333 -0.16,2.079999 -0.16,3.093332 l -0.0533,0.05333 h -0.0533 l -0.10667,-0.16 v 0.426666 l -0.0533,-0.266666 -0.0533,0.16 -0.0533,-0.106667 z m -0.42666,-12.10666 c -0.16,-1.493332 0,-3.306665 0,-4.799997 l -0.0533,0.05333 v -0.479999 c 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 l 0.10666,-0.373333 c 0,0.586667 -0.16,1.066666 0.0533,1.653333 0.0533,-0.16 0.0533,-0.266667 0.0533,-0.426667 l 0.0533,0.05333 c 0,0.159999 0,0.319999 0.0533,0.479999 -0.0533,0.106667 -0.0533,0.16 -0.0533,0.266667 l -0.0533,-0.213333 -0.0533,0.106666 c 0,0.106667 0,0.213333 0.0533,0.32 -0.10667,0.05333 0,0.426666 0,0.533333 0,-0.106667 0.0533,-0.32 0.10667,-0.426666 0,0.159999 0,0.373333 0.0533,0.533333 -0.0533,0.16 -0.0533,0.373333 -0.0533,0.533333 l -0.10667,-0.373333 c -0.0533,0.266666 -0.0533,0.586666 -0.0533,0.853332 v 0.05333 h 0.0533 l 0.0533,-0.106667 c 0,0.05333 0,0.16 0.0533,0.213333 -0.0533,0.16 -0.0533,0.266667 -0.0533,0.426667 l -0.0533,0.106666 -0.0533,-0.266666 c -0.10667,0.479999 -0.0533,0.959999 -0.0533,1.439999 l -0.10666,-0.373333 -0.0533,0.32 v -0.32 z m 12.58666,8.426662 c -0.21334,-0.906666 0,-2.079999 -0.26667,-2.879998 0.10667,-0.533333 0.0533,-1.013333 0.0533,-1.546666 0.16,0.959999 0,1.973332 0.16,2.933331 0,-0.106666 0.0533,-0.213333 0.0533,-0.319999 0,-0.48 -0.10667,-1.013333 -0.10667,-1.546666 0,-0.05333 0,-0.266667 0.0533,-0.266667 0.0533,0.746667 0.10667,1.493333 0.10667,2.239999 0,0.213333 -0.0533,0.48 -0.0533,0.693333 l -0.0533,-0.05333 c 0.0533,0.213333 0.0533,0.479999 0.0533,0.746666 z m -0.74667,2.986665 c -0.0533,-0.373333 -0.10667,-0.746667 -0.10667,-1.12 v -0.106666 l 0.0533,-0.05333 h -0.0533 V 61.89362 c 0,-0.373333 0,-0.693333 0.0533,-1.013333 0,0.853333 0.16,1.759999 0.16,2.613332 0,0.213333 0.0533,0.693333 -0.10667,0.853333 z m -1.22667,-15.253325 c 0.16,0.373334 0.21334,0.533333 0.21334,0.96 v 1.013333 c 0,0.16 0,0.319999 -0.0533,0.479999 -0.10666,-0.266666 -0.21333,-0.639999 -0.21333,-0.959999 v -0.8 l 0.0533,0.16 z m 0.53334,4.426664 c -0.0533,-0.266666 -0.16,-0.799999 -0.16,-1.066666 v -2.559998 c 0.0533,0.106667 0.16,0.373333 0.16,0.426666 z m 1.17333,4.959998 c 0,0.426666 -0.0533,0.853332 -0.0533,1.279999 -0.10667,-0.693333 -0.16,-1.386666 -0.16,-2.079999 0,-0.213333 0,-0.8 0.21333,-0.959999 -0.0533,0.586666 0,1.173332 0,1.759999 z m -0.74667,-6.879996 c 0,0.213333 -0.0533,0.479999 -0.16,0.693333 0.0533,0.159999 0.0533,0.319999 0.0533,0.479999 l -0.10667,0.16 c -0.0533,-0.799999 -0.10667,-1.546666 -0.10667,-2.346665 0.16,0.266666 0.16,0.64 0.26667,0.959999 z m -11.62666,-4.586664 c 0,-0.693333 0.10667,-1.333333 0.10667,-2.026666 l 0.10667,-0.05333 0.10666,0.16 c -0.0533,0.639999 -0.0533,1.653332 -0.32,2.239999 z m 2.13334,5.119997 -0.0533,-0.213334 c -0.0533,0.16 0,0.266667 0,0.426667 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493333 -0.16,-1.813332 0,-0.106667 0.0533,-0.213334 0.0533,-0.32 v 0.266666 c 0,-0.32 -0.0533,-1.439999 0.10666,-1.706665 0,0.853332 0.0533,1.653332 0.0533,2.506665 z m 0.53333,13.439992 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.64 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373333 0.10667,0.746667 0.10667,1.12 z m 9.75999,-9.226662 c 0,-0.213333 0,-0.479999 0.0533,-0.639999 0,0.48 0.10666,0.959999 0.10666,1.493332 0,0.16 0,0.32 0,0.48 0,-0.32 -0.10666,-0.64 -0.10666,-0.959999 l -0.0533,0.159999 v -0.213333 h 0.0533 l -0.0533,-0.266666 v 0.106666 c 0,-0.05333 0,-0.106666 0,-0.16 z m -11.78666,5.65333 c -0.0533,-0.319999 -0.0533,-0.639999 -0.0533,-0.959999 v -0.373333 c 0.0533,0.106666 0.10667,0.266666 0.10667,0.373333 0,0.32 -0.0533,0.64 -0.0533,0.959999 z m 0.53334,4.053331 c -0.0533,-0.106666 -0.0533,-0.266666 -0.0533,-0.373333 l -0.0533,0.106667 h -0.10667 c 0,-0.16 0.0533,-0.373333 0.10667,-0.533333 h 0.16 c -0.0533,0.32 0,0.586666 0,0.853333 z m 11.14666,-4.53333 c 0.0533,0.426666 0.16,0.853333 0.26666,1.226666 l -0.10666,0.373333 c -0.0533,-0.373333 -0.16,-0.853333 -0.16,-1.226666 z m -1.22667,4.799997 c -0.10667,0.05333 -0.10667,0.16 -0.16,0.266666 v -0.639999 l 0.10667,-0.266667 c 0,0.213333 0.0533,0.426667 0.0533,0.64 z m 0.10667,-1.333333 c 0,0.373333 0.0533,0.746667 0.0533,1.12 -0.0533,0 -0.0533,0.05333 -0.0533,0.106666 -0.10667,-0.32 -0.0533,-0.799999 -0.0533,-1.119999 z m -9.44,-15.146657 c 0.10667,0.479999 0.10667,0.959999 0,1.386666 z m 10.61333,5.01333 c 0.0533,0.213333 0.10667,0.373333 0.10667,0.586666 v 0.16 l -0.10667,0.05333 c 0,-0.266667 0,-0.586667 -0.0533,-0.853333 z m -0.0533,5.066664 c 0.0533,0.373333 0.10666,0.906666 0.10666,1.279999 l -0.0533,-0.106667 -0.0533,0.106667 v -0.533333 0.05333 z m -0.10667,1.119999 c -0.10667,-0.266667 -0.0533,-0.853333 -0.0533,-1.119999 0.0533,0.213333 0.10666,0.906666 0.0533,1.119999 z m -0.26667,3.893331 -0.10666,0.32 v -0.16 c 0,-0.16 0,-0.32 0.0533,-0.426666 z m -10.77332,1.866666 c 0,-0.05333 -0.10667,-0.213334 -0.10667,-0.266667 0,-0.106667 0.0533,-0.213333 0.10667,-0.32 z m -0.85334,-3.306665 c -0.0533,-0.266667 -0.0533,-0.533333 0,-0.8 z m 11.41333,-1.279999 v 0.693333 c -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 z m -9.65333,-9.653328 v 0.106667 l 0.10667,-0.05333 v 0.05333 l -0.0533,0.213333 -0.10667,-0.05333 c 0.0533,-0.106667 0,-0.16 0,-0.266667 z m 10.61333,5.279997 v -0.693333 c 0.0533,0.213333 0.0533,0.586666 0.0533,0.8 z m -2.98667,-10.559994 0.0533,-0.213333 0.0533,0.05333 v 0.373333 z m -9.49332,0.853333 c 0.10666,0.106667 0.0533,0.426666 0.0533,0.586666 -0.0533,-0.213333 -0.0533,-0.373333 -0.0533,-0.586666 z m 0,4.85333 c 0,-0.213333 0,-0.426666 0.0533,-0.586666 0,0.213333 0.0533,0.426667 -0.0533,0.586666 z m 0.69333,15.893324 c 0,0.16 -0.0533,0.32 0,0.48 0,-0.16 0.0533,-0.32 0,-0.48 z m -0.53333,-7.946662 c 0,0.213334 0,0.48 -0.0533,0.693333 0,-0.213333 0,-0.479999 0.0533,-0.693333 z m 0.58666,1.653333 c 0,-0.106667 -0.0533,-0.106667 -0.10666,-0.16 v 0.32 z m 1.49334,-0.48 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m -1.49334,6.186663 c -0.0533,0.213333 -0.0533,0.373333 -0.0533,0.586666 0.0533,-0.213333 0.0533,-0.373333 0.0533,-0.586666 z m 1.76,-0.586666 v -0.05333 l 0.0533,-0.106666 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213333 -0.0533,0.32 z m -1.44,-0.853333 c 0,-0.16 -0.0533,-0.266667 -0.0533,-0.426667 0.10667,0.106667 0.10667,0.266667 0.0533,0.426667 z m -0.37333,-4.639997 c 0.0533,0.05333 0.10667,0 0.0533,-0.05333 -0.0533,0.05333 -0.10666,0.16 -0.10666,0.213334 h 0.10666 z m 10.82666,-2.826665 c -0.0533,-0.106667 -0.0533,-0.213334 -0.0533,-0.32 l 0.0533,-0.106667 z m 0.53333,3.733331 c 0,-0.16 -0.0533,-0.266667 0,-0.426667 0,0.16 0.0533,0.266667 0,0.426667 z m -10.71999,7.146662 -0.0533,0.106667 0.0533,0.16 z m 10.18666,-8.959994 -0.0533,0.05333 v -0.213333 l 0.0533,-0.05333 z m 0.58667,-4.906664 c -0.0533,-0.106667 -0.0533,-0.213333 0,-0.32 0.0533,0.106667 0,0.213333 0,0.32 z m -11.52,9.599994 v 0.16 l -0.0533,0.05333 0.0533,-0.16 c -0.0533,0.05333 -0.10666,0 -0.10666,-0.05333 z m 0.64,2.026666 c 0.0533,-0.106667 0.0533,-0.213334 0,-0.32 z m 1.17334,-1.6 c 0,0 -0.0533,-0.05333 -0.0533,-0.106666 0,0 0.0533,-0.05333 0.0533,-0.106667 l 0.0533,0.106667 c 0,0.05333 0,0.05333 -0.0533,0.106666 z m 9.11999,-4.53333 v 0.16 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.16 z m 0,1.439999 h -0.0533 v -0.16 h 0.0533 z m -9.22666,1.013333 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.10667,-4.053331 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.53333,-7.093329 c 0,-0.05333 0,-0.16 0.0533,-0.213334 0,0.05333 0,0.16 -0.0533,0.213334 z m -1.22667,12.906659 c 0.0533,0.106666 0.0533,0.213333 0,0.266666 z m 11.57333,-11.359994 c 0,0.106667 0.0533,0.213334 0,0.32 z m -10.13333,15.039991 c 0,0.05333 0,0.106667 -0.0533,0.16 v -0.106666 z m 0.37334,-10.50666 0.0533,-0.05333 -0.0533,-0.106667 z m -1.12,8.533328 -0.0533,-0.106666 v 0.16 z m 1.06666,3.573332 v -0.16 l 0.0533,0.16 z m 9.06666,-17.813323 v 0.16 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.106667 z m -9.54666,-6.026663 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m -1.01333,15.573324 c 0,-0.05333 0,-0.106667 -0.0533,-0.16 0,0.05333 0,0.106666 0.0533,0.16 z m 1.01333,-14.986658 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 9.92,16.426657 0.0533,-0.106667 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.37333,-4.853331 c 0,0 -0.0533,0.05333 -0.10667,0.05333 l 0.0533,-0.05333 z m -11.67999,5.546664 c 0,0 -0.0533,0.05333 -0.0533,0.05333 0,0 0.0533,0.05333 0.0533,0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,-2.933332 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 0.90667,3.093332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -0.16,1.706665 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m 0.96,-16.47999 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -1.70667,13.333326 v -0.106667 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106667 z m 1.81333,6.186663 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.53334,-3.573332 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.90667,2.133332 c 0.0533,0 0.0533,0.05333 0.0533,0.106667 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106667 z m -1.28,-8.479995 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.69333,5.97333 h -0.0533 v -0.05333 z m -0.8,-16.906656 h -0.0533 v 0.05333 z m 2.08,11.359993 0.0533,-0.05333 v 0.05333 z m -0.0533,10.18666 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106666 z M 143.50098,55.76029 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m -1.97333,-3.999997 v 0.05333 l 0.0533,-0.05333 z m 2.13333,8.959994 0.0533,0.05333 h -0.0533 z m -0.90666,7.519996 -0.0533,0.05333 h 0.0533 z m 11.14666,-8.266662 c -0.0533,0.05333 -0.0533,0.05333 -0.0533,0.106667 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106667 z m -12.58666,-9.919994 h 0.0533 v -0.05333 z m 0.53333,10.133327 c 0,0 0.0533,0.05333 0.0533,0.05333 0,0 -0.0533,0.05333 -0.0533,0.05333 z m 1.12,8.213329 h -0.0533 v -0.05333 z m 10.45333,-8.959995 h -0.0533 l 0.0533,-0.05333 z m 0,0.106667 0.0533,0.05333 h -0.0533 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path926" />
+    <path
+       d="m 171.9068,62.533619 c 0,-3.199998 -2.82667,-4.586663 -5.38667,-5.439996 1.65334,-1.653333 2.82667,-3.999998 2.82667,-6.399996 0,-1.226666 0.0533,-2.719999 -0.58667,-3.839998 -0.90666,-1.546666 -2.08,-3.039998 -4.10666,-3.039998 -1.01333,0 -1.86667,0.319999 -2.61333,0.959999 -0.21334,-1.546666 0.10666,-3.039998 -2.08,-3.039998 -1.81333,0 -1.97333,0.906666 -1.97333,2.453332 0,1.813332 0.16,3.679998 0.16,5.546663 0,3.039998 0.16,6.079996 0.16,9.173328 0,1.919999 0.21333,3.839998 0.21333,5.759997 0,0.586666 0.16,1.119999 0.16,1.706665 0,0.16 -0.0533,0.266667 -0.10667,0.426667 -0.53333,0.159999 -0.74666,0.586666 -0.74666,1.119999 0,0.426666 0.16,1.173333 0.69333,1.226666 l 0.16,-0.05333 v 0.266666 c 0,1.013333 -0.37333,2.773332 0.8,3.253331 0.37333,0.16 0.85333,0.426667 1.22666,0.426667 0.8,0 3.2,-0.106667 3.2,-1.279999 -0.32,-0.693333 0,-1.546666 -0.21333,-2.239999 3.84,0 8.21333,-2.773332 8.21333,-6.986663 z m -7.62667,-4.426664 c 2.24,0 5.06667,1.546666 5.06667,4.053331 0,2.826665 -3.52,4.319998 -5.81333,4.639998 -0.10667,-0.8 -0.26667,-1.653333 -0.26667,-2.453332 0,-1.546666 -0.10666,-4.693331 -0.32,-6.186663 0.16,-0.05333 0.32,-0.05333 0.48,-0.05333 z M 164.8668,45.25363 c 1.33333,0 1.97333,1.173332 1.97333,2.453332 0,3.199998 -1.70666,6.559996 -3.94666,8.799994 -0.21333,-0.426666 -0.53333,-6.719996 -0.53333,-7.413329 0,-1.386665 0.37333,-2.133332 1.33333,-3.093331 0.21333,-0.213333 0.85333,-0.746666 1.17333,-0.746666 z m -3.62666,14.506658 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.32 -0.16,0.32 0,-0.266667 -0.0533,-0.533333 -0.0533,-0.8 0,-0.959999 0.26667,-1.973332 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.21333,-0.373333 0.10666,-0.853332 0.21333,-1.279999 0,0.16 0,0.32 0,0.48 l 0.0533,-0.106667 c 0.0533,0.48 0.0533,0.96 0.0533,1.439999 l 0.0533,0.05333 0.0533,-0.213334 c 0.10667,0.533333 0.16,1.013333 0.16,1.546666 -0.0533,0.213333 0.0533,0.693333 0.0533,0.906666 l -0.0533,-0.05333 c 0.0533,0.106667 0.0533,0.373333 0.0533,0.533333 -0.0533,-0.213333 -0.10667,-0.426666 -0.21334,-0.586666 0.10667,-0.32 0.16,-1.599999 -0.0533,-1.813333 0,0.373334 0,0.693333 -0.0533,1.066667 l 0.0533,-0.213334 c 0.0533,0.213334 0.0533,0.426667 0.0533,0.64 l -0.10666,0.106667 0.0533,-0.16 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 v 0.853332 l -0.0533,0.05333 z m -0.32,-8.693328 c -0.0533,0.479999 0.0533,0.906666 0.0533,1.386665 l 0.0533,0.106667 v 0.8 l -0.0533,0.05333 v -0.533333 c -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 l -0.0533,0.05333 c 0,-0.373333 -0.0533,-0.906666 0.0533,-1.279999 h -0.0533 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 l -0.0533,-0.05333 c -0.16,0.16 -0.10666,0.586667 -0.10666,0.8 l -0.0533,-0.213333 v 0.426666 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493332 -0.16,-1.813332 0,-0.48 0.10667,-0.96 0.10667,-1.439999 l 0.0533,0.106666 0.0533,-0.106666 c 0,0.479999 0.16,0.853332 0.16,1.279999 v 0.266666 l -0.0533,0.05333 z m -0.64,20.159988 c 0,-0.32 -0.0533,-0.586667 -0.0533,-0.906667 v -0.479999 l 0.0533,-0.106667 v 0.213333 l 0.0533,-0.106666 v -0.106667 l 0.0533,0.05333 v 0.853333 c 0.0533,-0.05333 0.0533,-0.16 0.0533,-0.213333 0.0533,0.32 0.0533,0.586666 0.0533,0.906666 z m 1.01333,-5.279997 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.639999 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373334 0.10667,0.746667 0.10667,1.12 z m -1.28,1.813332 c 0,0.16 0.26667,1.119999 0.32,1.173333 l -0.0533,0.106666 c 0,-0.213333 -0.10667,-0.533333 -0.21334,-0.693333 v -0.266666 0.266666 c -0.0533,-0.106666 -0.10666,-0.266666 -0.21333,-0.319999 l 0.10667,-0.213334 v -0.05333 z m 0.37333,-17.546656 c 0.10667,0.48 0.10667,0.959999 0,1.386666 z m 0.32,-1.386666 v 0.106667 h -0.0533 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0,-0.106667 0.10666,-0.16 0.16,-0.16 v 0.213333 z m -0.0533,4.373331 c 0.0533,-0.106667 0,-0.16 0,-0.266667 h 0.0533 v 0.106667 l 0.10667,-0.05333 -0.0533,0.266667 z m 0.69333,6.559996 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,0.05333 0.0533,0.159999 0.0533,0.213333 v 0.106666 l -0.10667,0.05333 z m 4.58667,-4.426664 c 0.0533,-0.213333 0.21333,-0.373333 0.32,-0.533333 v 0.106666 z m -4.85333,7.146662 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m 0.26666,5.546664 0.0533,-0.106667 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213334 -0.0533,0.32 l -0.0533,-0.05333 z m -0.32,-14.399992 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.213333 l 0.0533,0.05333 z m 0.26667,13.066659 c 0,0.05333 0,0.05333 -0.0533,0.106667 0,0 -0.0533,-0.05333 -0.0533,-0.106667 0,0 0.0533,-0.05333 0.0533,-0.106667 z m 0.10667,-6.45333 -0.0533,-0.213333 c 0.0533,0.05333 0.0533,0.106667 0.10667,0.16 z M 161.1335,55.28029 c -0.0533,-0.106666 -0.0533,-0.213333 -0.0533,-0.319999 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 z m -0.48,-5.919996 c 0,-0.106667 0,-0.16 0.0533,-0.266667 0,0.106667 0,0.16 -0.0533,0.266667 z m 0.48,15.359991 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.64,-11.14666 c 0,-0.05333 0,-0.16 0.0533,-0.213333 0,0.05333 0,0.159999 -0.0533,0.213333 z m 0.53333,7.093329 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.0533,-6.45333 v -0.16 l 0.0533,0.05333 z m -0.26667,15.946658 c 0,0.05333 0,0.106666 -0.0533,0.159999 v -0.106666 z m 0.42667,-10.559994 -0.0533,-0.106667 v 0.16 z m -0.85333,8.639995 c 0,0 -0.0533,0 -0.0533,-0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.26666,-20.319988 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m 0,0.586666 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.16,22.026654 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0,-21.439988 v -0.106666 l 0.0533,0.16 z m 0.53334,17.866656 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.64,-15.946657 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,18.07999 c 0.0533,0 0.0533,0.05333 0.0533,0.106666 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106666 z m 0,0.373333 h -0.0533 v -0.106667 z m 0.53333,-13.333326 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m 0.21334,5.013331 h -0.0533 v -0.05333 z m 0.32,-1.813333 0.0533,0.05333 h -0.0533 z m -1.06667,9.439995 h -0.0533 v -0.05333 z m 0.74667,-7.733329 h -0.0533 l 0.0533,-0.05333 z m -0.10667,10.186661 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path928" />
+  </g>
+  <g
+     aria-label="neodb.social"
+     id="text2391"
+     style="font-size:40px;line-height:1.25;fill:#b3b3b3">
+    <path
+       d="m 32.766354,166.81331 q 2.856,0 4.248,1.488 1.2,1.272 1.2,3.48 v 6.888 h -1.824 v -7.152 q 0,-1.536 -1.008,-2.376 -0.984,-0.864 -2.616,-0.864 h -4.776 q -1.632,0 -2.664,0.888 -1.008,0.888 -1.008,2.352 v 7.152 h -1.8 v -11.856 h 1.8 v 1.92 q 0.456,-0.96 1.32,-1.44 0.888,-0.48 2.352,-0.48 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5924" />
+    <path
+       d="m 46.123104,178.66931 q -5.328,0 -5.328,-4.44 v -2.832 q 0,-1.92 1.344,-3.216 1.464,-1.416 3.936,-1.416 h 5.232 q 2.568,0 4.008,1.248 1.344,1.152 1.344,2.928 v 1.728 h -14.04 v 1.344 q 0,1.704 0.984,2.496 0.864,0.696 2.472,0.696 h 5.928 q 1.416,0 2.112,-0.576 0.72,-0.6 0.72,-1.512 v -0.24 h 1.824 v 0.24 q 0,1.368 -0.912,2.328 -1.176,1.224 -3.528,1.224 z m 5.376,-10.416 h -5.712 q -1.392,0 -2.28,0.816 -0.888,0.816 -0.888,2.136 h 12.24 q 0,-1.296 -0.912,-2.112 -0.888,-0.84 -2.448,-0.84 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5926" />
+    <path
+       d="m 64.309479,166.76531 h 5.016 q 2.88,0 4.248,1.416 1.176,1.2 1.176,3.264 v 2.592 q 0,2.064 -1.2,3.264 -1.392,1.392 -4.224,1.392 h -5.016 q -5.448,0 -5.448,-4.656 v -2.592 q 0,-2.16 1.248,-3.36 1.368,-1.32 4.2,-1.32 z m -3.6,7.272 q 0,3.192 3.768,3.192 h 4.68 q 1.8,0 2.736,-0.72 1.032,-0.792 1.032,-2.472 v -2.616 q 0,-1.536 -0.984,-2.352 -0.96,-0.816 -2.784,-0.816 h -4.68 q -1.92,0 -2.856,0.744 -0.912,0.744 -0.912,2.448 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5928" />
+    <path
+       d="m 76.910604,174.03731 v -2.592 q 0,-2.136 1.272,-3.36 1.392,-1.32 4.032,-1.32 h 4.896 q 1.776,0 2.616,0.408 0.504,0.24 0.984,0.936 v -6.096 h 1.824 v 16.656 h -1.824 v -1.152 q -0.48,0.6 -1.08,0.84 -0.816,0.336 -2.52,0.336 h -4.896 q -5.304,0 -5.304,-4.656 z m 9.96,-5.784 h -4.032 q -2.136,0 -3.12,0.816 -0.984,0.792 -0.984,2.376 v 2.592 q 0,3.192 4.104,3.192 h 4.032 q 1.92,0 2.904,-0.888 1.008,-0.888 1.008,-2.304 v -2.592 q 0,-1.44 -0.936,-2.28 -1.032,-0.912 -2.976,-0.912 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5930" />
+    <path
+       d="m 96.877479,177.37331 v 1.296 h -1.8 v -16.656 h 1.8 v 6.096 q 0.432,-0.624 1.152,-0.936 0.936,-0.408 2.472001,-0.408 h 4.896 q 2.592,0 3.936,1.248 1.368,1.248 1.368,3.432 v 2.592 q 0,2.256 -1.296,3.456 -1.296,1.2 -4.008,1.2 h -4.896 q -1.464001,0 -2.328001,-0.384 -0.576,-0.264 -1.296,-0.936 z m 3.864001,-0.144 h 4.032 q 2.16,0 3.12,-0.744 0.984,-0.768 0.984,-2.448 v -2.592 q 0,-1.56 -1.008,-2.376 -1.008,-0.816 -3.096,-0.816 h -4.032 q -1.920001,0 -2.928001,0.888 -0.984,0.864 -0.984,2.304 v 2.592 q 0,1.44 0.96,2.328 0.984,0.864 2.952001,0.864 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5932" />
+    <path
+       d="m 113.31635,176.79731 h 1.968 v 1.872 h -1.968 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5934" />
+    <path
+       d="m 117.6221,175.74131 v -0.84 h 1.824 v 0.624 q 0,0.864 0.552,1.296 0.552,0.408 2.04,0.408 h 6.792 q 2.712,0 2.712,-1.632 v -0.504 q 0,-0.768 -0.648,-1.224 -0.624,-0.456 -1.728,-0.456 l -7.224,-0.144 q -2.136,0 -3.144,-0.624 -1.152,-0.72 -1.152,-2.304 v -0.552 q 0,-1.368 1.08,-2.184 1.104,-0.84 3.264,-0.84 h 6.648 q 2.664,0 3.864,0.984 0.912,0.744 0.912,1.968 v 0.816 h -1.824 v -0.6 q 0,-0.792 -0.432,-1.152 -0.624,-0.528 -2.256,-0.528 h -7.08 q -2.376,0 -2.376,1.584 v 0.504 q 0,0.768 0.6,1.128 0.6,0.336 1.776,0.336 l 7.248,0.144 q 2.088,0 3.168,0.768 1.104,0.768 1.104,2.376 v 0.672 q 0,1.344 -1.104,2.136 -1.104,0.768 -3.36,0.768 h -7.08 q -2.328,0 -3.336,-0.888 -0.84,-0.744 -0.84,-2.04 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5936" />
+    <path
+       d="m 141.09073,166.76531 h 5.016 q 2.88,0 4.248,1.416 1.176,1.2 1.176,3.264 v 2.592 q 0,2.064 -1.2,3.264 -1.392,1.392 -4.224,1.392 h -5.016 q -5.448,0 -5.448,-4.656 v -2.592 q 0,-2.16 1.248,-3.36 1.368,-1.32 4.2,-1.32 z m -3.6,7.272 q 0,3.192 3.768,3.192 h 4.68 q 1.8,0 2.736,-0.72 1.032,-0.792 1.032,-2.472 v -2.616 q 0,-1.536 -0.984,-2.352 -0.96,-0.816 -2.784,-0.816 h -4.68 q -1.92,0 -2.856,0.744 -0.912,0.744 -0.912,2.448 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5938" />
+    <path
+       d="m 167.63585,174.46931 h 1.824 q 0,1.872 -1.08,2.928 -1.32,1.272 -4.2,1.272 h -5.592 q -2.52,0 -3.768,-1.152 -1.248,-1.152 -1.248,-3.24 v -2.952 q 0,-2.04 1.248,-3.24 1.368,-1.32 3.912,-1.32 h 5.448 q 2.592,0 3.936,1.104 1.344,1.08 1.344,3.096 h -1.824 q 0,-1.296 -0.768,-1.968 -0.84,-0.744 -2.688,-0.744 h -5.448 q -1.608,0 -2.472,0.768 -0.864,0.744 -0.864,2.304 v 2.952 q 0,1.368 0.96,2.184 0.888,0.744 2.232,0.744 h 5.592 q 3.456,0 3.456,-2.736 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5940" />
+    <path
+       d="m 174.07235,166.81331 v 11.856 h -1.8 v -11.856 z m 0,-4.8 v 1.728 h -1.8 v -1.728 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5942" />
+    <path
+       d="m 180.75448,171.42131 h 7.752 q 0.984,0 1.56,0.288 0.6,0.264 0.744,0.696 v -1.68 q 0,-1.104 -0.696,-1.728 -0.792,-0.72 -2.376,-0.72 h -5.28 q -1.536,0 -2.328,0.456 -0.768,0.456 -0.768,1.224 v 0.288 h -1.872 v -0.288 q 0,-1.464 1.176,-2.304 1.176,-0.84 3.312,-0.84 h 5.952 q 2.448,0 3.624,1.128 1.08,1.032 1.08,3.048 v 7.68 h -1.8 v -0.936 q -0.384,0.432 -0.912,0.696 -0.528,0.24 -1.344,0.24 h -7.92 q -3.816,0 -3.816,-3.024 v -1.44 q 0,-1.272 0.912,-1.992 1.008,-0.792 3,-0.792 z m 7.272,1.464 h -6.648 q -1.608,0 -2.184,0.408 -0.552,0.408 -0.552,1.32 v 0.864 q 0,0.84 0.6,1.296 0.624,0.432 2.136,0.432 h 6.648 q 1.464,0 2.088,-0.504 0.648,-0.504 0.648,-1.656 0,-2.16 -2.736,-2.16 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5944" />
+    <path
+       d="m 198.05623,162.01331 v 16.656 h -1.824 v -16.656 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5946" />
+  </g>
+</svg>
diff --git a/common/static/js/create_update_review.js b/common/static/js/create_update_review.js
index 9dad74dc..60ab409d 100644
--- a/common/static/js/create_update_review.js
+++ b/common/static/js/create_update_review.js
@@ -1,9 +1,9 @@
 $(document).ready( function() {
     
     $(".markdownx-preview").hide();
-    $(".markdownx textarea").attr("placeholder", "拖拽图片至编辑框即可插入哦~");
+    $(".markdownx textarea").attr("placeholder", "从剪贴板粘贴或者拖拽文件至编辑框即可插入图片");
 
-    $(".review-form__preview-button").click(function() {
+    $(".review-form__preview-button").on('click', function() {
         if ($(".markdownx-preview").is(":visible")) {
             $(".review-form__preview-button").text("预览");
             $(".markdownx-preview").hide();
diff --git a/common/static/js/detail.js b/common/static/js/detail.js
index 01feac46..fe61e7c9 100644
--- a/common/static/js/detail.js
+++ b/common/static/js/detail.js
@@ -7,7 +7,7 @@ $(document).ready( function() {
 
     // pop up new rating modal
     $("#addMarkPanel button").each(function() {
-        $(this).click(function(e) {
+        $(this).on('click', function(e) {
             e.preventDefault();
             let title = $(this).text().trim();
             $(".mark-modal__title").text(title);
@@ -29,7 +29,7 @@ $(document).ready( function() {
     })
 
     // pop up modify mark modal
-    $(".mark-panel a.edit").click(function(e) {
+    $(".mark-panel a.edit").on('click', function(e) {
         e.preventDefault();
         let title = $(".mark-panel__status").text().trim();
         $(".mark-modal__title").text(title);
@@ -79,7 +79,7 @@ $(document).ready( function() {
     if ($("#statusSelection input[type='radio']:checked").val() == WISH_CODE) {
         $(".mark-modal .rating-star-edit").hide();
     }
-    $("#statusSelection input[type='radio']").click(function() {
+    $("#statusSelection input[type='radio']").on('click', function() {
         if ($(this).val() == WISH_CODE) {
             $(".mark-modal .rating-star-edit").hide();
         } else {
@@ -89,14 +89,14 @@ $(document).ready( function() {
     });
 
     // show confirm modal
-    $(".mark-panel a.delete").click(function(e) {
+    $(".mark-panel a.delete").on('click', function(e) {
         e.preventDefault();
         $(".confirm-modal").show();
         $(".bg-mask").show();
     });
 
     // confirm modal
-    $(".confirm-modal input[type='submit']").click(function(e) {
+    $(".confirm-modal input[type='submit']").on('click', function(e) {
         e.preventDefault();
         $(".mark-panel form").submit();
     });
@@ -116,20 +116,20 @@ $(document).ready( function() {
     });
 
     // expand hidden long text
-    $(".entity-desc__unfold-button a").click(function() {
+    $(".entity-desc__unfold-button a").on('click', function() {
         $(this).parent().siblings(".entity-desc__content").removeClass('entity-desc__content--folded');
         $(this).parent(".entity-desc__unfold-button").remove();
     });
     
     // disable delete mark button after click
     const confirmDeleteMarkButton = $('.confirm-modal__confirm-button > input');
-    confirmDeleteMarkButton.click(function() {
+    confirmDeleteMarkButton.on('click', function() {
         confirmDeleteMarkButton.prop("disabled", true);
     });
 
     // disable sumbit button after click
     const confirmSumbitMarkButton = $('.mark-modal__confirm-button > input');
-    confirmSumbitMarkButton.click(function() {
+    confirmSumbitMarkButton.on('click', function() {
         confirmSumbitMarkButton.prop("disabled", true);
         confirmSumbitMarkButton.closest('form')[0].submit();
     });
diff --git a/common/static/js/home.js b/common/static/js/home.js
index 456c637b..ca1cb32b 100644
--- a/common/static/js/home.js
+++ b/common/static/js/home.js
@@ -1,114 +1,98 @@
-
 $(document).ready( function() {
+    $("#userInfoCard .mast-brief").text($("<div>"+$("#userInfoCard .mast-brief").text().replace(/\<br/g,'\n<br').replace(/\<p/g,'\n<p')+"</div>").text());
+    $("#userInfoCard .mast-brief").html($("#userInfoCard .mast-brief").html().replace(/\n/g,'<br/>'));
     
     let token = $("#oauth2Token").text();
-    let mast_uri = $("#mastodonURI").text();
-    let mast_domain = new URL(mast_uri);
-    mast_domain = mast_domain.hostname;
+    let mast_domain = $("#mastodonURI").text();
+    let mast_uri = 'https://' + mast_domain
     let id = $("#userMastodonID").text();
 
-    let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
-    let followersSpinner = $("#spinner").clone().removeAttr("hidden");
-    let followingSpinner = $("#spinner").clone().removeAttr("hidden");
-    $("#userInfoCard").append(userInfoSpinner);
-    $("#followings h5").after(followingSpinner);
-    $("#followers h5").after(followersSpinner);
-    $(".mast-following-more").hide();
-    $(".mast-followers-more").hide();
+    if (id && id != 'None' && mast_domain != 'twitter.com') {
+        // let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
+        let followersSpinner = $("#spinner").clone().removeAttr("hidden");
+        let followingSpinner = $("#spinner").clone().removeAttr("hidden");
+        // $("#userInfoCard").append(userInfoSpinner);
+        $("#followings h5").after(followingSpinner);
+        $("#followers h5").after(followersSpinner);
+        $(".mast-following-more").hide();
+        $(".mast-followers-more").hide();
 
-    getUserInfo(
-        id, 
-        mast_uri, 
-        token, 
-        function(userData) {
-            let userName;
-            if (userData.display_name) {
-                userName = translateEmojis(userData.display_name, userData.emojis, true);
-            } else {
-                userName = userData.username;
-            }
-            $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
-            $("#userInfoCard .mast-displayname").html(userName);
-            $("#userInfoCard .mast-brief").text($(userData.note).text());
-            $(userInfoSpinner).remove();
-        }
-    );
+        getFollowers(
+            id,
+            mast_uri,
+            token,
+            function(userList, request) {
+                if (userList.length == 0) {
+                    $(".mast-followers").hide();
+                    $(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
 
-    getFollowers(
-        id,
-        mast_uri,
-        token,
-        function(userList, request) {
-            if (userList.length == 0) {
-                $(".mast-followers").hide();
-                $(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
-
-            } else {
-                if (userList.length > 4){
-                    userList = userList.slice(0, 4);
-                    $(".mast-followers-more").show();
+                } else {
+                    if (userList.length > 4){
+                        userList = userList.slice(0, 4);
+                        $(".mast-followers-more").show();
+                    }
+                    let template = $(".mast-followers li").clone();
+                    $(".mast-followers").html("");
+                    userList.forEach(data => {
+                        temp = $(template).clone();
+                        temp.find("img").attr("src", data.avatar);
+                        if (data.display_name) {
+                            temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
+                        } else {
+                            temp.find(".mast-displayname").text(data.username);
+                        }
+                        let url;
+                        if (data.acct.includes('@')) {
+                            url = $("#userPageURL").text().replace('0', data.acct);
+                        } else {
+                            url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
+                        }
+                        temp.find("a").attr('href', url);
+                        $(".mast-followers").append(temp);
+                    });
                 }
-                let template = $(".mast-followers li").clone();
-                $(".mast-followers").html("");
-                userList.forEach(data => {
-                    temp = $(template).clone();
-                    temp.find("img").attr("src", data.avatar);
-                    if (data.display_name) {
-                        temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
-                    } else {
-                        temp.find(".mast-displayname").text(data.username);
-                    }
-                    let url;
-                    if (data.acct.includes('@')) {
-                        url = $("#userPageURL").text().replace('0', data.acct);
-                    } else {
-                        url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
-                    }
-                    temp.find("a").attr('href', url);
-                    $(".mast-followers").append(temp);
-                });
+                $(followersSpinner).remove();
             }
-            $(followersSpinner).remove();
-        }
-    );
+        );
 
-    getFollowing(
-        id,
-        mast_uri,
-        token,
-        function(userList, request) {
-            if (userList.length == 0) {
-                $(".mast-following").hide();
-                $(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
-            } else {
-                if (userList.length > 4){
-                    userList = userList.slice(0, 4);
-                    $(".mast-following-more").show();
+        getFollowing(
+            id,
+            mast_uri,
+            token,
+            function(userList, request) {
+                if (userList.length == 0) {
+                    $(".mast-following").hide();
+                    $(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
+                } else {
+                    if (userList.length > 4){
+                        userList = userList.slice(0, 4);
+                        $(".mast-following-more").show();
+                    }
+                    let template = $(".mast-following li").clone();
+                    $(".mast-following").html("");
+                    userList.forEach(data => {
+                        temp = $(template).clone()
+                        temp.find("img").attr("src", data.avatar);
+                        if (data.display_name) {
+                            temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
+                        } else {
+                            temp.find(".mast-displayname").text(data.username);
+                        }
+                        let url;
+                        if (data.acct.includes('@')) {
+                            url = $("#userPageURL").text().replace('0', data.acct);
+                        } else {
+                            url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
+                        }
+                        temp.find("a").attr('href', url);
+                        $(".mast-following").append(temp);
+                    });
                 }
-                let template = $(".mast-following li").clone();
-                $(".mast-following").html("");
-                userList.forEach(data => {
-                    temp = $(template).clone()
-                    temp.find("img").attr("src", data.avatar);
-                    if (data.display_name) {
-                        temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
-                    } else {
-                        temp.find(".mast-displayname").text(data.username);
-                    }
-                    let url;
-                    if (data.acct.includes('@')) {
-                        url = $("#userPageURL").text().replace('0', data.acct);
-                    } else {
-                        url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
-                    }
-                    temp.find("a").attr('href', url);
-                    $(".mast-following").append(temp);
-                });
-            }
-            $(followingSpinner).remove();
+                $(followingSpinner).remove();
 
-        }
-    );
+            }
+        );
+    }
 
     // mobile dropdown
     $(".relation-dropdown__button").data("collapse", true);
@@ -118,7 +102,7 @@ $(document).ready( function() {
         button.children('.icon-arrow').toggleClass("icon-arrow--expand");
         button.siblings('.relation-dropdown__body').toggleClass("relation-dropdown__body--expand");
     }
-    $(".relation-dropdown__button").click(onClickDropdownButton)
+    $(".relation-dropdown__button").on('click', onClickDropdownButton);
 
     // close when click outside
     window.onclick = evt => {
@@ -129,7 +113,7 @@ $(document).ready( function() {
     };
 
     // import panel
-    $("#uploadBtn").click(e => {
+    $("#uploadBtn").on('click', e => {
         const btn = $("#uploadBtn")
         const form = $(".import-panel__body form")
 
@@ -201,7 +185,8 @@ $(document).ready( function() {
                     if (!data.total_items == 0) {
                         progress.attr("max", data.total_items);
                         progress.attr("value", data.finished_items);
-                        percent.text(Math.floor(100 * data.finished_items / data.total_items) + '%');
+                        progress.attr("value", data.finished_items);
+                        percent.text("" + data.finished_items + "/" + data.total_items);
                     }
                     setTimeout(() => {
                         poll();
diff --git a/common/static/js/mastodon.js b/common/static/js/mastodon.js
index 91defcbe..02a54679 100644
--- a/common/static/js/mastodon.js
+++ b/common/static/js/mastodon.js
@@ -54,38 +54,50 @@ const NUMBER_PER_REQUEST = 20
 //       "fields": []
 //     }
 //   ]
-function getFollowers(id, mastodonURI, token, callback) {
-    let url = mastodonURI + API_FOLLOWERS.replace(":id", id);
-    $.ajax({
-        url: url,
-        method: 'GET',
-        headers: {
-            'Authorization': 'Bearer ' + token,
-        },
-        data: {
-            'limit': NUMBER_PER_REQUEST
-        },
-        success: function(data, status, request){
-            callback(data, request);
-        },
-    });
+async function getFollowers(id, mastodonURI, token, callback) {
+    const url = mastodonURI + API_FOLLOWERS.replace(":id", id);
+    var response;
+    try {
+        response = await fetch(url+'?limit='+NUMBER_PER_REQUEST, {headers: {'Authorization': 'Bearer ' + token}});
+    } catch (e) {
+        console.error('loading followers failed.');
+        return;
+    }
+    const json = await response.json();
+    let nextUrl = null;
+    let links = response.headers.get('link');
+    if (links) {
+        links.split(',').forEach(link => {
+            if (link.includes('next')) {
+                let regex = /<(.*?)>/;
+                nextUrl = link.match(regex)[1];
+            }
+        });
+    }
+    callback(json, nextUrl);
 }
 
-function getFollowing(id, mastodonURI, token, callback) {
-    let url = mastodonURI + API_FOLLOWING.replace(":id", id);
-    $.ajax({
-        url: url,
-        method: 'GET',
-        headers: {
-            'Authorization': 'Bearer ' + token,
-        },        
-        data: {
-            'limit': NUMBER_PER_REQUEST
-        },
-        success: function(data, status, request){
-            callback(data, request);
-        },
-    });
+async function getFollowing(id, mastodonURI, token, callback) {
+    const url = mastodonURI + API_FOLLOWING.replace(":id", id);
+    var response;
+    try {
+        response = await fetch(url+'?limit='+NUMBER_PER_REQUEST, {headers: {'Authorization': 'Bearer ' + token}});
+    } catch (e) {
+        console.error('loading following failed.');
+        return;
+    }
+    const json = await response.json();
+    let nextUrl = null;
+    let links = response.headers.get('link');
+    if (links) {
+        links.split(',').forEach(link => {
+            if (link.includes('next')) {
+                let regex = /<(.*?)>/;
+                nextUrl = link.match(regex)[1];
+            }
+        });
+    }
+    callback(json, nextUrl);
 }
 
 // {
diff --git a/common/static/js/rating-star-readonly.js b/common/static/js/rating-star-readonly.js
index 76802ea8..a552a6b1 100644
--- a/common/static/js/rating-star-readonly.js
+++ b/common/static/js/rating-star-readonly.js
@@ -1,5 +1,5 @@
 $(document).ready( function() {
-    
+let render = function() {
     let ratingLabels = $(".rating-star");
     $(ratingLabels).each( function(index, value) {
         let ratingScore = $(this).data("rating-score") / 2;
@@ -8,5 +8,9 @@ $(document).ready( function() {
             readOnly: true
         });
     });
-    
+};
+document.body.addEventListener('htmx:load', function(evt) {
+    render();
+});
+render();
 });
\ No newline at end of file
diff --git a/common/static/js/scrape.js b/common/static/js/scrape.js
index 67ce3e3a..bd05bbd8 100644
--- a/common/static/js/scrape.js
+++ b/common/static/js/scrape.js
@@ -1,6 +1,6 @@
 $(document).ready( function() {
     
-    $(".submit").click(function(e) {
+    $(".submit").on('click', function(e) {
         e.preventDefault();
         let form = $("#scrapeForm form");
         if (form.data('submitted') === true) {
diff --git a/common/static/js/sort_layout.js b/common/static/js/sort_layout.js
index 1ac72eba..c20e370d 100644
--- a/common/static/js/sort_layout.js
+++ b/common/static/js/sort_layout.js
@@ -8,7 +8,7 @@ $(() => {
             $(e).data("visibility", true);
         }
         let btn = $("#toggleDisplayButtonTemplate").clone().removeAttr("id");
-        btn.click(e => {
+        btn.on('click', e => {
             if ($(e.currentTarget).parent().data('visibility') === true) {                
                 // flip text
                 $(e.currentTarget).children("span.showText").show();
@@ -72,7 +72,7 @@ $(() => {
     });
 
     // activate sorting
-    $("#sortEditButton").click(evt => {
+    $("#sortEditButton").on('click', evt => {
         // test if edit mode is activated
         isActivated = $("#sortSaveIcon").is(":visible");
 
@@ -134,7 +134,7 @@ $(() => {
     });
     
     // exit edit mode
-    $("#sortExitButton").click(evt => {
+    $("#sortExitButton").on('click', evt => {
         initialLayoutData.forEach(elem => {
             // set visiblity
             $('#' + elem.id).data('visibility', elem.visibility);
diff --git a/common/static/lib/css/milligram.css b/common/static/lib/css/milligram.css
deleted file mode 100644
index be254df8..00000000
--- a/common/static/lib/css/milligram.css
+++ /dev/null
@@ -1,605 +0,0 @@
-/*!
- * Milligram v1.3.0
- * https://milligram.github.io
- *
- * Copyright (c) 2017 CJ Patoilo
- * Licensed under the MIT license
- */
-
-*,
-*:after,
-*:before {
-  box-sizing: inherit;
-}
-
-html {
-  box-sizing: border-box;
-  font-size: 62.5%;
-}
-
-body {
-  color: #606c76;
-  font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
-  font-size: 1.6em;
-  font-weight: 300;
-  letter-spacing: .01em;
-  line-height: 1.6;
-}
-
-textarea {
-  font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
-}
-
-blockquote {
-  border-left: 0.3rem solid #d1d1d1;
-  margin-left: 0;
-  margin-right: 0;
-  padding: 1rem 1.5rem;
-}
-
-blockquote *:last-child {
-  margin-bottom: 0;
-}
-
-.button,
-button,
-input[type='button'],
-input[type='reset'],
-input[type='submit'] {
-  background-color: #00a1cc;
-  border: 0.1rem solid #00a1cc;
-  border-radius: .4rem;
-  color: #fff;
-  cursor: pointer;
-  display: inline-block;
-  font-size: 1.1rem;
-  font-weight: 700;
-  height: 3.8rem;
-  letter-spacing: .1rem;
-  line-height: 3.8rem;
-  padding: 0 3.0rem;
-  text-align: center;
-  text-decoration: none;
-  text-transform: uppercase;
-  white-space: nowrap;
-}
-
-.button:focus, .button:hover,
-button:focus,
-button:hover,
-input[type='button']:focus,
-input[type='button']:hover,
-input[type='reset']:focus,
-input[type='reset']:hover,
-input[type='submit']:focus,
-input[type='submit']:hover {
-  background-color: #606c76;
-  border-color: #606c76;
-  color: #fff;
-  outline: 0;
-}
-
-.button[disabled],
-button[disabled],
-input[type='button'][disabled],
-input[type='reset'][disabled],
-input[type='submit'][disabled] {
-  cursor: default;
-  opacity: .5;
-}
-
-.button[disabled]:focus, .button[disabled]:hover,
-button[disabled]:focus,
-button[disabled]:hover,
-input[type='button'][disabled]:focus,
-input[type='button'][disabled]:hover,
-input[type='reset'][disabled]:focus,
-input[type='reset'][disabled]:hover,
-input[type='submit'][disabled]:focus,
-input[type='submit'][disabled]:hover {
-  background-color: #00a1cc;
-  border-color: #00a1cc;
-}
-
-.button.button-outline,
-button.button-outline,
-input[type='button'].button-outline,
-input[type='reset'].button-outline,
-input[type='submit'].button-outline {
-  background-color: transparent;
-  color: #00a1cc;
-}
-
-.button.button-outline:focus, .button.button-outline:hover,
-button.button-outline:focus,
-button.button-outline:hover,
-input[type='button'].button-outline:focus,
-input[type='button'].button-outline:hover,
-input[type='reset'].button-outline:focus,
-input[type='reset'].button-outline:hover,
-input[type='submit'].button-outline:focus,
-input[type='submit'].button-outline:hover {
-  background-color: transparent;
-  border-color: #606c76;
-  color: #606c76;
-}
-
-.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
-button.button-outline[disabled]:focus,
-button.button-outline[disabled]:hover,
-input[type='button'].button-outline[disabled]:focus,
-input[type='button'].button-outline[disabled]:hover,
-input[type='reset'].button-outline[disabled]:focus,
-input[type='reset'].button-outline[disabled]:hover,
-input[type='submit'].button-outline[disabled]:focus,
-input[type='submit'].button-outline[disabled]:hover {
-  border-color: inherit;
-  color: #00a1cc;
-}
-
-.button.button-clear,
-button.button-clear,
-input[type='button'].button-clear,
-input[type='reset'].button-clear,
-input[type='submit'].button-clear {
-  background-color: transparent;
-  border-color: transparent;
-  color: #00a1cc;
-}
-
-.button.button-clear:focus, .button.button-clear:hover,
-button.button-clear:focus,
-button.button-clear:hover,
-input[type='button'].button-clear:focus,
-input[type='button'].button-clear:hover,
-input[type='reset'].button-clear:focus,
-input[type='reset'].button-clear:hover,
-input[type='submit'].button-clear:focus,
-input[type='submit'].button-clear:hover {
-  background-color: transparent;
-  border-color: transparent;
-  color: #606c76;
-}
-
-.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
-button.button-clear[disabled]:focus,
-button.button-clear[disabled]:hover,
-input[type='button'].button-clear[disabled]:focus,
-input[type='button'].button-clear[disabled]:hover,
-input[type='reset'].button-clear[disabled]:focus,
-input[type='reset'].button-clear[disabled]:hover,
-input[type='submit'].button-clear[disabled]:focus,
-input[type='submit'].button-clear[disabled]:hover {
-  color: #00a1cc;
-}
-
-code {
-  background: #f4f5f6;
-  border-radius: .4rem;
-  font-size: 86%;
-  margin: 0 .2rem;
-  padding: .2rem .5rem;
-  white-space: nowrap;
-}
-
-pre {
-  background: #f4f5f6;
-  border-left: 0.3rem solid #00a1cc;
-  overflow-y: hidden;
-}
-
-pre > code {
-  border-radius: 0;
-  display: block;
-  padding: 1rem 1.5rem;
-  white-space: pre;
-}
-
-hr {
-  border: 0;
-  border-top: 0.1rem solid #f4f5f6;
-  margin: 3.0rem 0;
-}
-
-input[type='email'],
-input[type='number'],
-input[type='password'],
-input[type='search'],
-input[type='tel'],
-input[type='text'],
-input[type='url'],
-textarea,
-select {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
-  background-color: transparent;
-  border: 0.1rem solid #d1d1d1;
-  border-radius: .4rem;
-  box-shadow: none;
-  box-sizing: inherit;
-  height: 3.8rem;
-  padding: .6rem 1.0rem;
-  width: 100%;
-}
-
-input[type='email']:focus,
-input[type='number']:focus,
-input[type='password']:focus,
-input[type='search']:focus,
-input[type='tel']:focus,
-input[type='text']:focus,
-input[type='url']:focus,
-textarea:focus,
-select:focus {
-  border-color: #00a1cc;
-  outline: 0;
-}
-
-select {
-  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
-  padding-right: 3.0rem;
-}
-
-select:focus {
-  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#00a1cc" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
-}
-
-textarea {
-  min-height: 6.5rem;
-}
-
-label,
-legend {
-  display: block;
-  font-size: 1.6rem;
-  font-weight: 700;
-  margin-bottom: .5rem;
-}
-
-fieldset {
-  border-width: 0;
-  padding: 0;
-}
-
-input[type='checkbox'],
-input[type='radio'] {
-  display: inline;
-}
-
-.label-inline {
-  display: inline-block;
-  font-weight: normal;
-  margin-left: .5rem;
-}
-
-.container {
-  margin: 0 auto;
-  max-width: 112.0rem;
-  padding: 0 2.0rem;
-  position: relative;
-  width: 100%;
-}
-
-.row {
-  display: flex;
-  flex-direction: column;
-  padding: 0;
-  width: 100%;
-}
-
-.row.row-no-padding {
-  padding: 0;
-}
-
-.row.row-no-padding > .column {
-  padding: 0;
-}
-
-.row.row-wrap {
-  flex-wrap: wrap;
-}
-
-.row.row-top {
-  align-items: flex-start;
-}
-
-.row.row-bottom {
-  align-items: flex-end;
-}
-
-.row.row-center {
-  align-items: center;
-}
-
-.row.row-stretch {
-  align-items: stretch;
-}
-
-.row.row-baseline {
-  align-items: baseline;
-}
-
-.row .column {
-  display: block;
-  flex: 1 1 auto;
-  margin-left: 0;
-  max-width: 100%;
-  width: 100%;
-}
-
-.row .column.column-offset-10 {
-  margin-left: 10%;
-}
-
-.row .column.column-offset-20 {
-  margin-left: 20%;
-}
-
-.row .column.column-offset-25 {
-  margin-left: 25%;
-}
-
-.row .column.column-offset-33, .row .column.column-offset-34 {
-  margin-left: 33.3333%;
-}
-
-.row .column.column-offset-50 {
-  margin-left: 50%;
-}
-
-.row .column.column-offset-66, .row .column.column-offset-67 {
-  margin-left: 66.6666%;
-}
-
-.row .column.column-offset-75 {
-  margin-left: 75%;
-}
-
-.row .column.column-offset-80 {
-  margin-left: 80%;
-}
-
-.row .column.column-offset-90 {
-  margin-left: 90%;
-}
-
-.row .column.column-10 {
-  flex: 0 0 10%;
-  max-width: 10%;
-}
-
-.row .column.column-20 {
-  flex: 0 0 20%;
-  max-width: 20%;
-}
-
-.row .column.column-25 {
-  flex: 0 0 25%;
-  max-width: 25%;
-}
-
-.row .column.column-33, .row .column.column-34 {
-  flex: 0 0 33.3333%;
-  max-width: 33.3333%;
-}
-
-.row .column.column-40 {
-  flex: 0 0 40%;
-  max-width: 40%;
-}
-
-.row .column.column-50 {
-  flex: 0 0 50%;
-  max-width: 50%;
-}
-
-.row .column.column-60 {
-  flex: 0 0 60%;
-  max-width: 60%;
-}
-
-.row .column.column-66, .row .column.column-67 {
-  flex: 0 0 66.6666%;
-  max-width: 66.6666%;
-}
-
-.row .column.column-75 {
-  flex: 0 0 75%;
-  max-width: 75%;
-}
-
-.row .column.column-80 {
-  flex: 0 0 80%;
-  max-width: 80%;
-}
-
-.row .column.column-90 {
-  flex: 0 0 90%;
-  max-width: 90%;
-}
-
-.row .column .column-top {
-  align-self: flex-start;
-}
-
-.row .column .column-bottom {
-  align-self: flex-end;
-}
-
-.row .column .column-center {
-  -ms-grid-row-align: center;
-      align-self: center;
-}
-
-@media (min-width: 40rem) {
-  .row {
-    flex-direction: row;
-    margin-left: -1.0rem;
-    width: calc(100% + 2.0rem);
-  }
-  .row .column {
-    margin-bottom: inherit;
-    padding: 0 1.0rem;
-  }
-}
-
-a {
-  color: #00a1cc;
-  text-decoration: none;
-}
-
-a:focus, a:hover {
-  color: #606c76;
-}
-
-dl,
-ol,
-ul {
-  list-style: none;
-  margin-top: 0;
-  padding-left: 0;
-}
-
-dl dl,
-dl ol,
-dl ul,
-ol dl,
-ol ol,
-ol ul,
-ul dl,
-ul ol,
-ul ul {
-  font-size: 90%;
-  margin: 1.5rem 0 1.5rem 3.0rem;
-}
-
-ol {
-  list-style: decimal inside;
-}
-
-ul {
-  list-style: circle inside;
-}
-
-.button,
-button,
-dd,
-dt,
-li {
-  margin-bottom: 1.0rem;
-}
-
-fieldset,
-input,
-select,
-textarea {
-  margin-bottom: 1.5rem;
-}
-
-blockquote,
-dl,
-figure,
-form,
-ol,
-p,
-pre,
-table,
-ul {
-  margin-bottom: 2.5rem;
-}
-
-table {
-  border-spacing: 0;
-  width: 100%;
-}
-
-td,
-th {
-  border-bottom: 0.1rem solid #e1e1e1;
-  padding: 1.2rem 1.5rem;
-  text-align: left;
-}
-
-td:first-child,
-th:first-child {
-  padding-left: 0;
-}
-
-td:last-child,
-th:last-child {
-  padding-right: 0;
-}
-
-b,
-strong {
-  font-weight: bold;
-}
-
-p {
-  margin-top: 0;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  font-weight: 300;
-  letter-spacing: -.1rem;
-  margin-bottom: 2.0rem;
-  margin-top: 0;
-}
-
-h1 {
-  font-size: 4.6rem;
-  line-height: 1.2;
-}
-
-h2 {
-  font-size: 3.6rem;
-  line-height: 1.25;
-}
-
-h3 {
-  font-size: 2.8rem;
-  line-height: 1.3;
-}
-
-h4 {
-  font-size: 2.2rem;
-  letter-spacing: -.08rem;
-  line-height: 1.35;
-}
-
-h5 {
-  font-size: 1.8rem;
-  letter-spacing: -.05rem;
-  line-height: 1.5;
-}
-
-h6 {
-  font-size: 1.6rem;
-  letter-spacing: 0;
-  line-height: 1.4;
-}
-
-img {
-  max-width: 100%;
-}
-
-.clearfix:after {
-  clear: both;
-  content: ' ';
-  display: table;
-}
-
-.float-left {
-  float: left;
-}
-
-.float-right {
-  float: right;
-}
-
diff --git a/common/static/lib/css/multiple-select.min.css b/common/static/lib/css/multiple-select.min.css
deleted file mode 100644
index 5a6f1b19..00000000
--- a/common/static/lib/css/multiple-select.min.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
-  * multiple-select - Multiple select is a jQuery plugin to select multiple elements with checkboxes :).
-  *
-  * @version v1.5.2
-  * @homepage http://multiple-select.wenzhixin.net.cn
-  * @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
-  * @license MIT
-  */
-
-@charset "UTF-8";.ms-offscreen{clip:rect(0 0 0 0)!important;width:1px!important;height:1px!important;border:0!important;margin:0!important;padding:0!important;overflow:hidden!important;position:absolute!important;outline:0!important;left:auto!important;top:auto!important}.ms-parent{display:inline-block;position:relative;vertical-align:middle}.ms-choice{display:block;width:100%;height:26px;padding:0;overflow:hidden;cursor:pointer;border:1px solid #aaa;text-align:left;white-space:nowrap;line-height:26px;color:#444;text-decoration:none;border-radius:4px;background-color:#fff}.ms-choice.disabled{background-color:#f4f4f4;background-image:none;border:1px solid #ddd;cursor:default}.ms-choice>span{position:absolute;top:0;left:0;right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;padding-left:8px}.ms-choice>span.placeholder{color:#999}.ms-choice>div.icon-close{position:absolute;top:0;right:16px;height:100%;width:16px}.ms-choice>div.icon-close:before{content:'×';color:#888;font-weight:bold;position:absolute;top:50%;margin-top:-14px}.ms-choice>div.icon-close:hover:before{color:#333}.ms-choice>div.icon-caret{position:absolute;width:0;height:0;top:50%;right:8px;margin-top:-2px;border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px}.ms-choice>div.icon-caret.open{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.ms-drop{width:auto;min-width:100%;overflow:hidden;display:none;margin-top:-1px;padding:0;position:absolute;z-index:1000;background:#fff;color:#000;border:1px solid #aaa;border-radius:4px}.ms-drop.bottom{top:100%;box-shadow:0 4px 5px rgba(0,0,0,0.15)}.ms-drop.top{bottom:100%;box-shadow:0 -4px 5px rgba(0,0,0,0.15)}.ms-search{display:inline-block;margin:0;min-height:26px;padding:2px;position:relative;white-space:nowrap;width:100%;z-index:10000;box-sizing:border-box}.ms-search input{width:100%;height:auto!important;min-height:24px;padding:0 5px;margin:0;outline:0;font-family:sans-serif;border:1px solid #aaa;border-radius:5px;box-shadow:none}.ms-drop ul{overflow:auto;margin:0;padding:0}.ms-drop ul>li{list-style:none;display:list-item;background-image:none;position:static;padding:.25rem 8px}.ms-drop ul>li .disabled{font-weight:normal!important;opacity:.35;filter:Alpha(Opacity=35);cursor:default}.ms-drop ul>li.multiple{display:block;float:left}.ms-drop ul>li.group{clear:both}.ms-drop ul>li.multiple label{width:100%;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ms-drop ul>li label{position:relative;padding-left:1.25rem;margin-bottom:0;font-weight:normal;display:block;white-space:nowrap;cursor:pointer}.ms-drop ul>li label.optgroup{font-weight:bold}.ms-drop ul>li.hide-radio{padding:0}.ms-drop ul>li.hide-radio:focus,.ms-drop ul>li.hide-radio:hover{background-color:#f8f9fa}.ms-drop ul>li.hide-radio.selected{color:#fff;background-color:#007bff}.ms-drop ul>li.hide-radio label{margin-bottom:0;padding:5px 8px}.ms-drop ul>li.hide-radio input{display:none}.ms-drop ul>li.option-level-1 label{padding-left:28px}.ms-drop input[type="radio"],.ms-drop input[type="checkbox"]{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.ms-drop .ms-no-results{display:none}
\ No newline at end of file
diff --git a/common/static/lib/css/neo.css b/common/static/lib/css/neo.css
new file mode 100644
index 00000000..2f2fcbce
--- /dev/null
+++ b/common/static/lib/css/neo.css
@@ -0,0 +1,166 @@
+.markdownx-preview h1 {
+    font-size: 2.5em;
+}
+
+.markdownx-preview h2 {
+    font-size: 2.0em;
+}
+
+.markdownx-preview h3 {
+    font-size: 1.6em;
+}
+
+.markdownx-preview blockquote {
+    border-left: lightgray solid 0.4em;
+    padding-left: 0.4em;
+}
+
+.collection-item-position-edit {
+    float: right;
+}
+
+.collection-item-position-edit a {
+    cursor: pointer;
+    color: #ccc;
+}
+
+.action-icon svg {
+    cursor: pointer;
+    fill: #ccc;
+    height: 12px;
+    vertical-align: text-bottom;
+}
+
+.entity-list__entity-img-wrapper {
+    position: relative;
+}
+
+.entity-list__entity-action-icon {
+    position: absolute;
+    top:0;
+    right:0;
+    mix-blend-mode: hard-light;
+    text-stroke: 1px black;
+    background-color: lightgray;
+    border-radius: 0 0 0 8px;
+    padding: 0 4px;
+    cursor: pointer;
+}
+
+
+/***** MODAL DIALOG ****/
+#modal {
+    /* Underlay covers entire screen. */
+    position: fixed;
+    top:0px;
+    bottom: 0px;
+    left:0px;
+    right:0px;
+    background-color:rgba(0,0,0,0.5);
+    z-index:1000;
+
+    /* Flexbox centers the .modal-content vertically and horizontally */
+    display:flex;
+    flex-direction:column;
+    align-items:center;
+
+    /* Animate when opening */
+    animation-name: fadeIn;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+#modal > .modal-underlay {
+    /* underlay takes up the entire viewport. This is only
+    required if you want to click to dismiss the popup */
+    position: absolute;
+    z-index: -1;
+    .collection_list_position_edittop:0px;
+
+
+    bottom:0px;
+    left: 0px;
+    right: 0px;
+}
+
+#modal > .modal-content {
+    /* Position visible dialog near the top of the window */
+    margin-top:10vh;
+
+    /* Sizing for visible dialog */
+    width:80%;
+    max-width:600px;
+
+    /* Display properties for visible dialog*/
+    background-color: #f7f7f7;
+    padding: 20px 20px 10px 20px;
+    color: #606c76;
+
+    /* Animate when opening */
+    animation-name:zoomIn;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+#modal.closing {
+    /* Animate when closing */
+    animation-name: fadeOut;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+#modal.closing > .modal-content {
+    /* Aniate when closing */
+    animation-name: zoomOut;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+@keyframes fadeIn {
+    0% {opacity: 0;}
+    100% {opacity: 1;}
+} 
+
+@keyframes fadeOut {
+    0% {opacity: 1;}
+    100% {opacity: 0;}
+} 
+
+@keyframes zoomIn {
+    0% {transform: scale(0.9);}
+    100% {transform: scale(1);}
+} 
+
+@keyframes zoomOut {
+    0% {transform: scale(1);}
+    100% {transform: scale(0.9);}
+}
+
+#modal .add-to-list-modal__head {
+  margin-bottom: 20px;
+}
+
+#modal .add-to-list-modal__head::after {
+  content: ' ';
+  clear: both;
+  display: table;
+}
+
+#modal .add-to-list-modal__title {
+  font-weight: bold;
+  font-size: 1.2em;
+  float: left;
+}
+
+#modal .add-to-list-modal__close-button {
+  float: right;
+  cursor: pointer;
+}
+
+#modal .add-to-list-modal__confirm-button {
+  float: right;
+}
+
+#modal li, #modal ul, #modal label {
+  display: inline;
+}
diff --git a/common/static/lib/js/hyperscript-0.9.5.min.js b/common/static/lib/js/hyperscript-0.9.5.min.js
new file mode 100644
index 00000000..a17cb5e1
--- /dev/null
+++ b/common/static/lib/js/hyperscript-0.9.5.min.js
@@ -0,0 +1,2 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e||self)._hyperscript=t()}(this,function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function t(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}function n(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,o(e,t)}function r(e){return r=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},r(e)}function o(e,t){return o=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},o(e,t)}function a(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(e){return!1}}function i(e,t,n){return i=a()?Reflect.construct:function(e,t,n){var r=[null];r.push.apply(r,t);var a=new(Function.bind.apply(e,r));return n&&o(a,n.prototype),a},i.apply(null,arguments)}function u(e){var t="function"==typeof Map?new Map:void 0;return u=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return i(e,arguments,r(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),o(n,e)},u(e)}function l(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function s(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(n)return(n=n.call(e)).next.bind(n);if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return l(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0;return function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function c(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function f(e,t){var n=e[t];if(n)return n;var r={};return e[t]=r,r}function m(e,t){return new(e.bind.apply(e,[e].concat(t)))}var p,d=globalThis,v=function(e){function n(e,t,n){this._css=e,this.relativeToElement=t,this.escape=n}var r=n.prototype;return r.contains=function(e){for(var t,n=s(this);!(t=n()).done;)if(t.value.contains(e))return!0;return!1},r[e]=function(){return this.selectMatches()[Symbol.iterator]()},r.selectMatches=function(){return T.getRootNode(this.relativeToElement).querySelectorAll(this.css)},t(n,[{key:"css",get:function(){return this.escape?T.escapeSelector(this._css):this._css}},{key:"className",get:function(){return this._css.substr(1)}},{key:"id",get:function(){return this.className()}},{key:"length",get:function(){return this.selectMatches().length}}]),n}(Symbol.iterator),h=function(){var e={"+":"PLUS","-":"MINUS","*":"MULTIPLY","/":"DIVIDE",".":"PERIOD","..":"ELLIPSIS","\\":"BACKSLASH",":":"COLON","%":"PERCENT","|":"PIPE","!":"EXCLAMATION","?":"QUESTION","#":"POUND","&":"AMPERSAND",$:"DOLLAR",";":"SEMI",",":"COMMA","(":"L_PAREN",")":"R_PAREN","<":"L_ANG",">":"R_ANG","<=":"LTE_ANG",">=":"GTE_ANG","==":"EQ","===":"EQQ","!=":"NEQ","!==":"NEQQ","{":"L_BRACE","}":"R_BRACE","[":"L_BRACKET","]":"R_BRACKET","=":"EQUALS"};function t(e){return i(e)||a(e)||"-"===e||"_"===e||":"===e}function n(e){return i(e)||a(e)||"-"===e||"_"===e||":"===e}function r(e){return" "===e||"\t"===e||o(e)}function o(e){return"\r"===e||"\n"===e}function a(e){return e>="0"&&e<="9"}function i(e){return e>="a"&&e<="z"||e>="A"&&e<="Z"}function u(e,t){return"_"===e||"$"===e}function l(e,t,n){o();var r=null;function o(){for(;"WHITESPACE"===f(0,!0).type;)t.push(e.shift())}function a(e,t){E.raiseParseError(e,t)}function i(e){if(m()&&m().op&&m().value===e)return s()}function u(e,t,n,r){if(m()&&m().type&&[e,t,n,r].indexOf(m().type)>=0)return s()}function l(e,t){if(-1===p.indexOf(e))return t=t||"IDENTIFIER",m()&&m().value===e&&m().type===t?s():void 0}function s(){var n=e.shift();return t.push(n),r=n,o(),n}function c(n,r){for(var a=[],i=f(0,!0);!(null!=r&&i.type===r||null!=n&&i.value===n||"EOF"===i.type);){var u=e.shift();t.push(u),a.push(i),i=f(0,!0)}return o(),a}function f(t,n){var r,o=0;do{if(!n)for(;e[o]&&"WHITESPACE"===e[o].type;)o++;r=e[o],t--,o++}while(t>-1);return r||{type:"EOF",value:"<<<EOF>>>"}}function m(){return f(0)}var p=[];return{pushFollow:function(e){p.push(e)},popFollow:function(){p.pop()},clearFollow:function(){var e=p;return p=[],e},restoreFollow:function(e){p=e},matchAnyToken:function(e,t,n){for(var r=0;r<arguments.length;r++){var o=arguments[r],a=l(o);if(a)return a}},matchAnyOpToken:function(e,t,n){for(var r=0;r<arguments.length;r++){var o=arguments[r],a=i(o);if(a)return a}},matchOpToken:i,requireOpToken:function(e){var t=i(e);if(t)return t;a(this,"Expected '"+e+"' but found '"+m().value+"'")},matchTokenType:u,requireTokenType:function(e,t,n,r){var o=u(e,t,n,r);if(o)return o;a(this,"Expected one of "+JSON.stringify([e,t,n]))},consumeToken:s,peekToken:function(t,n,r){return e[n]&&e[n].value===t&&e[n].type===r},matchToken:l,requireToken:function(e,t){var n=l(e,t);if(n)return n;a(this,"Expected '"+e+"' but found '"+m().value+"'")},list:e,consumed:t,source:n,hasMore:function(){return e.length>0},currentToken:m,lastMatch:function(){return r},token:f,consumeUntil:c,consumeUntilWhitespace:function(){return c(null,"WHITESPACE")},lastWhitespace:function(){return t[t.length-1]&&"WHITESPACE"===t[t.length-1].type?t[t.length-1].value:""},sourceFor:function(){return n.substring(this.startToken.start,this.endToken.end)},lineFor:function(){return n.split("\n")[this.startToken.line-1]}}}function s(e){if(e.length>0){var t=e[e.length-1];if("IDENTIFIER"===t.type||"CLASS_REF"===t.type||"ID_REF"===t.type)return!1;if(t.op&&(">"===t.value||")"===t.value))return!1}return!0}return{tokenize:function(c,f){var m,p=[],d=c,v=0,h=0,E=1,y="<START>",T=0;function k(){return f&&0===T}for(;v<d.length;)if("-"!==C()||"-"!==A()||!r(F())&&""!==F())if(r(C()))p.push(D());else if(M()||"."!==C()||!i(A())&&"{"!==A())if(M()||"#"!==C()||!i(A())&&"{"!==A())if("["===C()&&"@"===A())p.push(q());else if("@"===C())p.push(w());else if("*"===C()&&i(A()))p.push(S());else if(i(C())||!k()&&u(C()))p.push(I());else if(a(C()))p.push(R());else if(k()||'"'!==C()&&"`"!==C())if(k()||"'"!==C()){if(e[C()])"$"===y&&"{"===C()&&T++,"}"===C()&&T--,p.push(O());else if(k()||"`"===(m=C())||"^"===m)p.push(g("RESERVED",P()));else if(v<d.length)throw Error("Unknown token: "+C()+" ")}else s(p)?p.push(L()):p.push(O());else p.push(L());else p.push(N());else p.push(b());else x();return l(p,[],d);function g(e,t){return{type:e,value:t,start:v,end:v+1,column:h,line:E}}function x(){for(;C()&&!o(C());)P();P()}function b(){var e=g("CLASS_REF"),n=P();if("{"===C()){for(e.template=!0,n+=P();C()&&"}"!==C();)n+=P();if("}"!==C())throw Error("Unterminated class reference");n+=P()}else for(;t(C());)n+=P();return e.value=n,e.end=v,e}function q(){for(var e=g("ATTRIBUTE_REF"),t=P();v<d.length&&"]"!==C();)t+=P();return"]"===C()&&(t+=P()),e.value=t,e.end=v,e}function w(){for(var e=g("ATTRIBUTE_REF"),t=P();n(C());)t+=P();return e.value=t,e.end=v,e}function S(){for(var e=g("STYLE_REF"),t=P();i(C())||"-"===C();)t+=P();return e.value=t,e.end=v,e}function N(){var e=g("ID_REF"),t=P();if("{"===C()){for(e.template=!0,t+=P();C()&&"}"!==C();)t+=P();if("}"!==C())throw Error("Unterminated id reference");P()}else for(;n(C());)t+=P();return e.value=t,e.end=v,e}function I(){for(var e=g("IDENTIFIER"),t=P();i(C())||a(C())||u(C());)t+=P();return"!"===C()&&"beep"===t&&(t+=P()),e.value=t,e.end=v,e}function R(){for(var e=g("NUMBER"),t=P();a(C());)t+=P();for("."===C()&&a(A())&&(t+=P());a(C());)t+=P();return e.value=t,e.end=v,e}function O(){for(var t=(r=void 0,(r=g(void 0,void 0)).op=!0,r),n=P();C()&&e[n+C()];)n+=P();var r;return t.type=e[n],t.value=n,t.end=v,t}function L(){for(var e,t=g("STRING"),n=P(),r="";C()&&C()!==n;)if("\\"===C()){P();var o=P();r+="b"===o?"\b":"f"===o?"\f":"n"===o?"\n":"r"===o?"\r":"t"===o?"\t":"v"===o?"\v":o}else r+=P();if(C()!==n)throw Error("Unterminated string at [Line: "+(e=t).line+", Column: "+e.column+"]");return P(),t.value=r,t.end=v,t.template="`"===n,t}function C(){return d.charAt(v)}function A(){return d.charAt(v+1)}function F(){return d.charAt(v+2)}function P(){return y=C(),v++,h++,y}function M(){return i(y)||a(y)||")"===y||'"'===y||"'"===y||"`"===y||"}"===y||"]"===y}function D(){for(var e=g("WHITESPACE"),t="";C()&&r(C());)o(C())&&(h=0,E++),t+=P();return e.value=t,e.end=v,e}},makeTokensObject:l}}(),E=function(){var e={},t={},n={},r=[],o=[];function a(e,t,n){e.startToken=t,e.sourceFor=n.sourceFor,e.lineFor=n.lineFor,e.programSource=n.source}function i(t,n,r){return void 0===r&&(r=void 0),function(r){var o=e[t];if(o){var i=n.currentToken(),u=o(E,T,n,r);if(u)for(a(u,i,n),u.endToken=u.endToken||n.lastMatch(),r=u.root;null!=r;)a(r,i,n),r=r.root;return u}}(r)}function u(e,t,n,r){var o=i(e,t,r);return o||c(t,n||"Expected "+e),o}function l(e,t){for(var n=0;n<e.length;n++){var r=i(e[n],t);if(r)return r}}function s(t,n){e[t]=n}function c(e,t){t=(t||"Unexpected Token : "+e.currentToken().value)+"\n\n"+function(e){var t=e.currentToken(),n=e.source.split("\n"),r=n[t&&t.line?t.line-1:n.length-1];return r+"\n"+" ".repeat(t&&t.line?t.column:r.length-1)+"^^\n\n"}(e);var n=new Error(t);throw n.tokens=e,n}function f(e){return t[e.value]}function m(e){return n[e.value]}return s("feature",function(e,t,r){if(r.matchOpToken("(")){var o=e.requireElement("feature",r);return r.requireOpToken(")"),o}var a=n[r.currentToken().value];if(a)return a(e,t,r)}),s("command",function(e,n,r){if(r.matchOpToken("(")){var o=e.requireElement("command",r);return r.requireOpToken(")"),o}var a,i=t[r.currentToken().value];return i?a=i(e,n,r):"IDENTIFIER"===r.currentToken().type&&(a=e.parseElement("pseudoCommand",r)),a?e.parseElement("indirectStatement",r,a):a}),s("commandList",function(e,t,n){var r=e.parseElement("command",n);if(r){n.matchToken("then");var o=e.parseElement("commandList",n);return o&&(r.next=o),r}}),s("leaf",function(e,t,n){var o=l(r,n);return null==o?i("symbol",n):o}),s("indirectExpression",function(e,t,n,r){for(var a=0;a<o.length;a++){var i=o[a];r.endToken=n.lastMatch();var u=e.parseElement(i,n,r);if(u)return u}return r}),s("indirectStatement",function(e,t,n,r){if(n.matchToken("unless")){r.endToken=n.lastMatch();var o={type:"unlessStatementModifier",args:[e.requireElement("expression",n)],op:function(e,t){return t?this.next:r},execute:function(e){return t.unifiedExec(this,e)}};return r.parent=o,o}return r}),s("primaryExpression",function(e,t,n){var r=e.parseElement("leaf",n);if(r)return e.parseElement("indirectExpression",n,r);e.raiseParseError(n,"Unexpected value: "+n.currentToken().value)}),{setParent:function e(t,n){"object"==typeof t&&(t.parent=n,"object"==typeof n&&(n.children=n.children||new Set,n.children.add(t)),e(t.next,n))},requireElement:u,parseElement:i,featureStart:m,commandStart:f,commandBoundary:function(e){return!("end"!=e.value&&"then"!=e.value&&"else"!=e.value&&"otherwise"!=e.value&&")"!=e.value&&!f(e)&&!m(e)&&"EOF"!=e.type)},parseAnyOf:l,parseHyperScript:function(e){var t=i("hyperscript",e);if(e.hasMore()&&c(e),t)return t},raiseParseError:c,addGrammarElement:s,addCommand:function(n,r){var o=n+"Command",a=function(e,t,n){var a=r(e,t,n);if(a)return a.type=o,a.execute=function(e){return e.meta.command=a,t.unifiedExec(this,e)},a};e[o]=a,t[n]=a},addFeature:function(t,r){var o=t+"Feature",a=function(e,n,a){var i=r(e,n,a);if(i)return i.isFeature=!0,i.keyword=t,i.type=o,i};e[o]=a,n[t]=a},addLeafExpression:function(e,t){r.push(e),s(e,t)},addIndirectExpression:function(e,t){o.push(e),s(e,t)},parseStringTemplate:function(e){var t=[""];do{if(t.push(e.lastWhitespace()),"$"===e.currentToken().value){e.consumeToken();var n=e.matchOpToken("{");t.push(u("expression",e)),n&&e.requireOpToken("}"),t.push("")}else if("\\"===e.currentToken().value)e.consumeToken(),e.consumeToken();else{var r=e.consumeToken();t[t.length-1]+=r?r.value:""}}while(e.hasMore());return t.push(e.lastWhitespace()),t},ensureTerminated:function(e){for(var t={type:"implicitReturn",op:function(e){return e.meta.returned=!0,e.meta.resolve&&e.meta.resolve(),T.HALT},execute:function(e){}},n=e;n.next;)n=n.next;n.next=t}}}(),y={dynamicResolvers:[function(e,t){if("Fixed"===e)return Number(t).toFixed();if(0===e.indexOf("Fixed:")){var n=e.split(":")[1];return Number(t).toFixed(parseInt(n))}}],String:function(e){return e.toString?e.toString():""+e},Int:function(e){return parseInt(e)},Float:function(e){return parseFloat(e)},Number:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return Number(e)}),Date:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return new Date(e)}),Array:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return Array.from(e)}),JSON:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return JSON.stringify(e)}),Object:function(e){return e instanceof String&&(e=e.toString()),"string"==typeof e?JSON.parse(e):c({},e)}},T=function(){function e(e,t){var n=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return n&&n.call(e,t)}function t(e,t,n,r){(n=n||{}).sender=r;var o=function(e,t){var n;return d.Event&&"function"==typeof d.Event?(n=new Event(e,{bubbles:!0,cancelable:!0})).detail=t:(n=document.createEvent("CustomEvent")).initCustomEvent(e,!0,!0,t),n}(t,n);return e.dispatchEvent(o)}function r(e){return Array.isArray(e)||"undefined"!=typeof NodeList&&(e instanceof NodeList||e instanceof HTMLCollection)}function o(e){return e instanceof v||r(e)}function a(e,t){if(null==e);else if(function(e){return"object"==typeof e&&Symbol.iterator in e&&"function"==typeof e[Symbol.iterator]}(e))for(var n,o=s(e);!(n=o()).done;)t(n.value);else if(r(e))for(var a=0;a<e.length;a++)t(e[a]);else t(e)}function i(e){for(var t=0;t<e.length;t++){var n=e[t];if(n.asyncWrapper&&(e[t]=n.value),Array.isArray(n))for(var r=0;r<n.length;r++){var o=n[r];o.asyncWrapper&&(n[r]=o.value)}}}var l={};function m(e,t){var n=[t],r=!1,o=!1;if(e.args)for(var a=0;a<e.args.length;a++){var u=e.args[a];if(null==u)n.push(null);else if(Array.isArray(u)){for(var l=[],s=0;s<u.length;s++){var c=u[s];(f=c?c.evaluate(t):null)&&(f.then?r=!0:f.asyncWrapper&&(o=!0)),l.push(f)}n.push(l)}else if(u.evaluate){var f;(f=u.evaluate(t))&&(f.then?r=!0:f.asyncWrapper&&(o=!0)),n.push(f)}else n.push(u)}return r?new Promise(function(t,r){n=function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n];Array.isArray(r)?t.push(Promise.all(r)):t.push(r)}return t}(n),Promise.all(n).then(function(n){o&&i(n);try{var a=e.op.apply(e,n);t(a)}catch(e){r(e)}}).catch(function(e){r(e)})}):(o&&i(n),e.op.apply(e,n))}var k=null;function g(){return null==k&&(k=p.config.attributes.replace(/ /g,"").split(",")),k}function x(e){for(var t=0;t<g().length;t++){var n=g()[t];if(e.hasAttribute&&e.hasAttribute(n))return e.getAttribute(n)}return e instanceof HTMLScriptElement&&"text/hyperscript"===e.type?e.innerText:null}var b=new WeakMap;function q(e){var t=b.get(e);return void 0===t&&b.set(e,t={}),t}function w(e,t){e&&(c(t,q(e)),w(e.parentElement,t))}function S(e,t,n,r){var o={meta:{parser:E,lexer:h,runtime:T,owner:e,feature:t,iterators:{}},me:n,event:r,target:r?r.target:null,detail:r?r.detail:null,sender:r&&r.detail?r.detail.sender:null,body:"document"in d?document.body:null};return o.meta.ctx=o,w(e,o),o}function N(e){var t=h.tokenize(e);if(E.commandStart(t.currentToken())){var n=E.requireElement("commandList",t);return t.hasMore()&&E.raiseParseError(t),E.ensureTerminated(n),n}if(E.featureStart(t.currentToken())){var r=E.requireElement("hyperscript",t);return t.hasMore()&&E.raiseParseError(t),r}var o=E.requireElement("expression",t);return t.hasMore()&&E.raiseParseError(t),o}function I(e,n){if(!e.closest||!e.closest(p.config.disableSelector)){var r=O(e);if(!r.initialized){var o=x(e);if(o)try{r.initialized=!0,r.script=o;var a=h.tokenize(o),i=E.parseHyperScript(a);if(!i)return;i.apply(n||e,e),setTimeout(function(){t(n||e,"load",{hyperscript:!0})},1)}catch(t){T.triggerEvent(e,"exception",{error:t}),console.error("hyperscript errors were found on the following element:",e,"\n\n",t.message,t.stack)}}}}var R=new WeakMap;function O(e){var t=R.get(e);return void 0===t&&R.set(e,t={}),t}function L(e){var t=e.meta&&e.meta.owner;if(t){var n=O(t),r="elementScope";return e.meta.feature&&e.meta.feature.behavior&&(r=e.meta.feature.behavior+"Scope"),f(n,r)}return{}}function C(e,t,n){if(null!=e){var r=n(e,t);if(void 0!==r)return r;if(o(e)){for(var a,i=[],u=s(e);!(a=u()).done;){var l=n(a.value,t);l&&i.push(l)}return i}}}return{typeCheck:function(e,t,n){return!(null!=e||!n)||Object.prototype.toString.call(e).slice(8,-1)===t},forEach:a,implicitLoop:function(e,t){if(o(e))for(var n,r=s(e);!(n=r()).done;)t(n.value);else t(e)},shouldAutoIterate:o,triggerEvent:t,matchesSelector:e,getScript:x,processNode:function(t){var n=T.getScriptSelector();e(t,n)&&I(t,t),t instanceof HTMLScriptElement&&"text/hyperscript"===t.type&&I(t,document.body),t.querySelectorAll&&a(t.querySelectorAll(n+", [type='text/hyperscript']"),function(e){I(e,e instanceof HTMLScriptElement&&"text/hyperscript"===e.type?document.body:e)})},evaluate:function(e,t,r){var o=function(e){function t(t){var n;return(n=e.call(this)||this).module=t,n}return n(t,e),t.prototype.toString=function(){return this.module.id},t}(u(EventTarget)),a="document"in d?d.document.body:new o(r&&r.module);t=c(S(a,null,a,null),t||{});var i=N(e);return i.execute?(i.execute(t),t.result):i.apply?(i.apply(a,a,r),q(a)):i.evaluate(t)},evaluateNoPromise:function(e,t){var n=e.evaluate(t);if(n.next)throw new Error(e.sourceFor()+" returned a Promise in a context that they are not allowed.");return n},parse:N,getScriptSelector:function(){return g().map(function(e){return"["+e+"]"}).join(", ")},resolveSymbol:function(e,t,n){if("me"===e||"my"===e||"I"===e)return t.me;if("it"===e||"its"===e)return t.result;if("you"===e||"your"===e||"yourself"===e)return t.beingTold;if("global"===n)return d[e];if("element"===n)return L(t)[e];if("local"===n)return t[e];if(t.meta&&t.meta.context){var r=t.meta.context[e];if(void 0!==r)return r}var o=t[e];return void 0!==o||void 0!==(o=L(t)[e])?o:d[e]},setSymbol:function(e,t,n,r){if("global"===n)d[e]=r;else if("element"===n)(o=L(t))[e]=r;else if("local"===n)t[e]=r;else{var o,a=t[e];void 0!==a?t[e]=r:void 0!==(a=(o=L(t))[e])?o[e]=r:t[e]=r}},makeContext:S,findNext:function e(t,n){if(t)return t.resolveNext?t.resolveNext(n):t.next?t.next:e(t.parent,n)},unifiedEval:m,convertValue:function(e,t){for(var n=y.dynamicResolvers,r=0;r<n.length;r++){var o=(0,n[r])(t,e);if(void 0!==o)return o}if(null==e)return null;var a=y[t];if(a)return a(e);throw"Unknown conversion : "+t},unifiedExec:function e(t,n){for(;;){try{var r=m(t,n)}catch(e){if(n.meta.handlingFinally)console.error(" Exception in finally block: ",e),r=l;else{if(T.registerHyperTrace(n,e),n.meta.errorHandler&&!n.meta.handlingError){n.meta.handlingError=!0,n[n.meta.errorSymbol]=e,t=n.meta.errorHandler;continue}n.meta.currentException=e,r=l}}if(null==r)return void console.error(t," did not return a next element to execute! context: ",n);if(r.then)return void r.then(function(t){e(t,n)}).catch(function(t){e({op:function(){throw t}},n)});if(r===l){if(!n.meta.finallyHandler||n.meta.handlingFinally){if(n.meta.onHalt&&n.meta.onHalt(),n.meta.currentException){if(n.meta.reject)return void n.meta.reject(n.meta.currentException);throw n.meta.currentException}return}n.meta.handlingFinally=!0,t=n.meta.finallyHandler}else t=r}},resolveProperty:function(e,t){return C(e,t,function(e,t){return e[t]})},resolveAttribute:function(e,t){return C(e,t,function(e,t){return e.getAttribute&&e.getAttribute(t)})},resolveStyle:function(e,t){return C(e,t,function(e,t){return e.style&&e.style[t]})},resolveComputedStyle:function(e,t){return C(e,t,function(e,t){return getComputedStyle(e).getPropertyValue(t)})},assignToNamespace:function(e,t,n,r){var o;for(o="undefined"!=typeof document&&e===document.body?d:q(e);t.length>0;){var a=t.shift(),i=o[a];null==i&&(o[a]=i={}),o=i}o[n]=r},registerHyperTrace:function(e,t){for(var n=[],r=null;null!=e;)n.push(e),r=e,e=e.meta.caller;null==r.meta.traceMap&&(r.meta.traceMap=new Map),r.meta.traceMap.get(t)||r.meta.traceMap.set(t,{trace:n,print:function(e){(e=e||console.error)("hypertrace /// ");for(var t=0,r=0;r<n.length;r++)t=Math.max(t,n[r].meta.feature.displayName.length);for(r=0;r<n.length;r++){var o=n[r];e("  ->",o.meta.feature.displayName.padEnd(t+2),"-",o.meta.owner)}}})},getHyperTrace:function(e,t){for(var n=e;n.meta.caller;)n=n.meta.caller;if(n.meta.traceMap)return n.meta.traceMap.get(t,[])},getInternalData:O,getHyperscriptFeatures:q,escapeSelector:function(e){return e.replace(/:/g,function(e){return"\\"+e})},nullCheck:function(e,t){if(null==e)throw new Error("'"+t.sourceFor()+"' is null")},isEmpty:function(e){return null==e||0===e.length},doesExist:function(e){if(null==e)return!1;if(o(e))for(var t=s(e);!t().done;)return!0;return!1},getRootNode:function(e){if(e&&e instanceof Node){var t=e.getRootNode();if(t instanceof Document||t instanceof ShadowRoot)return t}return document},getEventQueueFor:function(e,t){var n=O(e),r=n.eventQueues;null==r&&(r=new Map,n.eventQueues=r);var o=r.get(t);return null==o&&r.set(t,o={queue:[],executing:!1}),o},hyperscriptUrl:"document"in d?"undefined"==typeof document&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("_hyperscript_web.min.js",document.baseURI).href:null,HALT:l}}(),k=function(e,t,n){if(t.contains)return t.contains(n);if(t.includes)return t.includes(n);throw Error("The value of "+e.sourceFor()+" does not have a contains or includes method on it")},g=function(e,t,n){if(t.match)return!!t.match(n);if(t.matches)return t.matches(n);throw Error("The value of "+e.sourceFor()+" does not have a match or matches method on it")},x=function(e,t,n,r){var o=t.requireElement("eventName",r),a=t.parseElement("namedArgumentList",r);if("send"===e&&r.matchToken("to")||"trigger"===e&&r.matchToken("on"))var i=t.requireElement("expression",r);else i=t.requireElement("implicitMeTarget",r);var u={eventName:o,details:a,to:i,args:[i,o,a],op:function(e,t,r,o){return n.nullCheck(t,i),n.forEach(t,function(t){n.triggerEvent(t,r,o,e.me)}),n.findNext(u,e)}};return u},b=function(e,t){var n,r="text";return e.matchToken("a")||e.matchToken("an"),e.matchToken("json")||e.matchToken("Object")?r="json":e.matchToken("response")?r="response":e.matchToken("html")?r="html":e.matchToken("text")||(n=t.requireElement("dotOrColonPath",e).evaluate()),{type:r,conversion:n}};E.addLeafExpression("parenthesized",function(e,t,n){if(n.matchOpToken("(")){var r=n.clearFollow();try{var o=e.requireElement("expression",n)}finally{n.restoreFollow(r)}return n.requireOpToken(")"),o}}),E.addLeafExpression("string",function(e,t,n){var r=n.matchTokenType("STRING");if(r){var o,a=r.value;if(r.template){var i=h.tokenize(a,!0);o=e.parseStringTemplate(i)}else o=[];return{type:"string",token:r,args:o,op:function(e){for(var t="",n=1;n<arguments.length;n++){var r=arguments[n];void 0!==r&&(t+=r)}return t},evaluate:function(e){return 0===o.length?a:t.unifiedEval(this,e)}}}}),E.addGrammarElement("nakedString",function(e,t,n){if(n.hasMore()){var r=n.consumeUntilWhitespace();return n.matchTokenType("WHITESPACE"),{type:"nakedString",tokens:r,evaluate:function(e){return r.map(function(e){return e.value}).join("")}}}}),E.addLeafExpression("number",function(e,t,n){var r=n.matchTokenType("NUMBER");if(r){var o=r,a=parseFloat(r.value);return{type:"number",value:a,numberToken:o,evaluate:function(){return a}}}}),E.addLeafExpression("idRef",function(e,t,n){var r=n.matchTokenType("ID_REF");if(r){if(r.template){var o=r.value.substr(2,r.value.length-2),a=h.tokenize(o);return{type:"idRefTemplate",args:[e.requireElement("expression",a)],op:function(e,n){return t.getRootNode(e.me).getElementById(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}var i=r.value.substr(1);return{type:"idRef",css:r.value,value:i,evaluate:function(e){return t.getRootNode(e.me).getElementById(i)}}}}),E.addLeafExpression("classRef",function(e,t,n){var r=n.matchTokenType("CLASS_REF");if(r){if(r.template){var o=r.value.substr(2,r.value.length-2),a=h.tokenize(o);return{type:"classRefTemplate",args:[e.requireElement("expression",a)],op:function(e,t){return new v("."+t,e.me,!0)},evaluate:function(e){return t.unifiedEval(this,e)}}}var i=r.value;return{type:"classRef",css:i,evaluate:function(e){return new v(i,e.me,!0)}}}});var q=function(e,r){function o(t,n,r){var o;return(o=e.call(this,t,n)||this).templateParts=r,o.elements=r.filter(function(e){return e instanceof Element}),o}return n(o,e),o.prototype[r]=function(){this.elements.forEach(function(e,t){return e.dataset.hsQueryId=t});var t=e.prototype[Symbol.iterator].call(this);return this.elements.forEach(function(e){return e.removeAttribute("data-hs-query-id")}),t},t(o,[{key:"css",get:function(){for(var e,t="",n=0,r=s(this.templateParts);!(e=r()).done;){var o=e.value;o instanceof Element?t+="[data-hs-query-id='"+n+++"']":t+=o}return t}}]),o}(v,Symbol.iterator);E.addLeafExpression("queryRef",function(e,t,n){if(n.matchOpToken("<")){var r=n.consumeUntil("/");n.requireOpToken("/"),n.requireOpToken(">");var o=r.map(function(e){return"STRING"===e.type?'"'+e.value+'"':e.value}).join("");if(o.indexOf("$")>=0)var a=!0,i=h.tokenize(o,!0),u=e.parseStringTemplate(i);return{type:"queryRef",css:o,args:u,op:function(e){return a?new q(o,e.me,[].slice.call(arguments,1)):new v(o,e.me)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("attributeRef",function(e,t,n){var r=n.matchTokenType("ATTRIBUTE_REF");if(r){var o=r.value;if(0===o.indexOf("["))var a=o.substring(2,o.length-1);else a=o.substring(1);var i="["+a+"]",u=a.split("="),l=u[0],s=u[1];return s&&0===s.indexOf('"')&&(s=s.substring(1,s.length-1)),{type:"attributeRef",name:l,css:i,value:s,op:function(e){var t=e.beingTold||e.me;if(t)return t.getAttribute(l)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("styleRef",function(e,t,n){var r=n.matchTokenType("STYLE_REF");if(r){var o=r.value.substr(1);return o.startsWith("computed-")?{type:"computedStyleRef",name:o=o.substr("computed-".length),op:function(e){var n=e.beingTold||e.me;if(n)return t.resolveComputedStyle(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}}:{type:"styleRef",name:o,op:function(e){var n=e.beingTold||e.me;if(n)return t.resolveStyle(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("objectKey",function(e,t,n){var r;if(r=n.matchTokenType("STRING"))return{type:"objectKey",key:r.value,evaluate:function(){return r.value}};if(n.matchOpToken("[")){var o=e.parseElement("expression",n);return n.requireOpToken("]"),{type:"objectKey",expr:o,args:[o],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}var a="";do{(r=n.matchTokenType("IDENTIFIER")||n.matchOpToken("-"))&&(a+=r.value)}while(r);return{type:"objectKey",key:a,evaluate:function(){return a}}}),E.addLeafExpression("objectLiteral",function(e,t,n){if(n.matchOpToken("{")){var r=[],o=[];if(!n.matchOpToken("}")){do{var a=e.requireElement("objectKey",n);n.requireOpToken(":");var i=e.requireElement("expression",n);o.push(i),r.push(a)}while(n.matchOpToken(","));n.requireOpToken("}")}return{type:"objectLiteral",args:[r,o],op:function(e,t,n){for(var r={},o=0;o<t.length;o++)r[t[o]]=n[o];return r},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("nakedNamedArgumentList",function(e,t,n){var r=[],o=[];if("IDENTIFIER"===n.currentToken().type)do{var a=n.requireTokenType("IDENTIFIER");n.requireOpToken(":");var i=e.requireElement("expression",n);o.push(i),r.push({name:a,value:i})}while(n.matchOpToken(","));return{type:"namedArgumentList",fields:r,args:[o],op:function(e,t){for(var n={_namedArgList_:!0},o=0;o<t.length;o++)n[r[o].name.value]=t[o];return n},evaluate:function(e){return t.unifiedEval(this,e)}}}),E.addGrammarElement("namedArgumentList",function(e,t,n){if(n.matchOpToken("(")){var r=e.requireElement("nakedNamedArgumentList",n);return n.requireOpToken(")"),r}}),E.addGrammarElement("symbol",function(e,t,n){var r="default";n.matchToken("global")?r="global":n.matchToken("element")||n.matchToken("module")?(r="element",n.matchOpToken("'")&&n.requireToken("s")):n.matchToken("local")&&(r="local");var o=n.matchOpToken(":"),a=n.matchTokenType("IDENTIFIER");if(a){var i=a.value;return o&&(i=":"+i),"default"===r&&(0===i.indexOf("$")&&(r="global"),0===i.indexOf(":")&&(r="element")),{type:"symbol",token:a,scope:r,name:i,evaluate:function(e){return t.resolveSymbol(i,e,r)}}}}),E.addGrammarElement("implicitMeTarget",function(e,t,n){return{type:"implicitMeTarget",evaluate:function(e){return e.beingTold||e.me}}}),E.addLeafExpression("boolean",function(e,t,n){var r=n.matchToken("true")||n.matchToken("false");if(r){var o="true"===r.value;return{type:"boolean",evaluate:function(e){return o}}}}),E.addLeafExpression("null",function(e,t,n){if(n.matchToken("null"))return{type:"null",evaluate:function(e){return null}}}),E.addLeafExpression("arrayLiteral",function(e,t,n){if(n.matchOpToken("[")){var r=[];if(!n.matchOpToken("]")){do{var o=e.requireElement("expression",n);r.push(o)}while(n.matchOpToken(","));n.requireOpToken("]")}return{type:"arrayLiteral",values:r,args:[r],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("blockLiteral",function(e,t,n){if(n.matchOpToken("\\")){var r=[],o=n.matchTokenType("IDENTIFIER");if(o)for(r.push(o);n.matchOpToken(",");)r.push(n.requireTokenType("IDENTIFIER"));n.requireOpToken("-"),n.requireOpToken(">");var a=e.requireElement("expression",n);return{type:"blockLiteral",args:r,expr:a,evaluate:function(e){return function(){for(var t=0;t<r.length;t++)e[r[t].value]=arguments[t];return a.evaluate(e)}}}}}),E.addIndirectExpression("propertyAccess",function(e,t,n,r){if(n.matchOpToken(".")){var o=n.requireTokenType("IDENTIFIER");return e.parseElement("indirectExpression",n,{type:"propertyAccess",root:r,prop:o,args:[r],op:function(e,n){return t.resolveProperty(n,o.value)},evaluate:function(e){return t.unifiedEval(this,e)}})}}),E.addIndirectExpression("of",function(e,t,n,r){if(n.matchToken("of")){for(var o=e.requireElement("unaryExpression",n),a=null,i=r;i.root;)a=i,i=i.root;"symbol"!==i.type&&"attributeRef"!==i.type&&"styleRef"!==i.type&&"computedStyleRef"!==i.type&&e.raiseParseError(n,"Cannot take a property of a non-symbol: "+i.type);var u="attributeRef"===i.type,l="styleRef"===i.type||"computedStyleRef"===i.type;if(u||l)var s=i;var c=i.name,f={type:"ofExpression",prop:i.token,root:o,attribute:s,expression:r,args:[o],op:function(e,n){return u?t.resolveAttribute(n,c):l?"computedStyleRef"===i.type?t.resolveComputedStyle(n,c):t.resolveStyle(n,c):t.resolveProperty(n,c)},evaluate:function(e){return t.unifiedEval(this,e)}};return"attributeRef"===i.type&&(f.attribute=i),a?(a.root=f,a.args=[f]):r=f,e.parseElement("indirectExpression",n,r)}}),E.addIndirectExpression("possessive",function(e,t,n,r){if(!e.possessivesDisabled){var o=n.matchOpToken("'");if(o||"symbol"===r.type&&("my"===r.name||"its"===r.name||"your"===r.name)&&("IDENTIFIER"===n.currentToken().type||"ATTRIBUTE_REF"===n.currentToken().type||"STYLE_REF"===n.currentToken().type)){o&&n.requireToken("s");var a=e.parseElement("attributeRef",n);if(null==a){var i=e.parseElement("styleRef",n);if(null==i)var u=n.requireTokenType("IDENTIFIER")}return e.parseElement("indirectExpression",n,{type:"possessive",root:r,attribute:a||i,prop:u,args:[r],op:function(e,n){if(a)var r=t.resolveAttribute(n,a.name);else r=i?"computedStyleRef"===i.type?t.resolveComputedStyle(n,i.name):t.resolveStyle(n,i.name):t.resolveProperty(n,u.value);return r},evaluate:function(e){return t.unifiedEval(this,e)}})}}}),E.addIndirectExpression("inExpression",function(e,t,n,r){if(n.matchToken("in")){var o={type:"inExpression",root:r,args:[r,e.requireElement("unaryExpression",n)],op:function(e,n,r){var o=[];if(n.css)t.implicitLoop(r,function(e){for(var t=e.querySelectorAll(n.css),r=0;r<t.length;r++)o.push(t[r])});else if(n instanceof Element){var a=!1;if(t.implicitLoop(r,function(e){e.contains(n)&&(a=!0)}),a)return n}else t.implicitLoop(n,function(e){t.implicitLoop(r,function(t){e===t&&o.push(e)})});return o},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",n,o)}}),E.addIndirectExpression("asExpression",function(e,t,n,r){if(n.matchToken("as")){n.matchToken("a")||n.matchToken("an");var o=e.requireElement("dotOrColonPath",n).evaluate();return e.parseElement("indirectExpression",n,{type:"asExpression",root:r,args:[r],op:function(e,n){return t.convertValue(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}})}}),E.addIndirectExpression("functionCall",function(e,t,n,r){if(n.matchOpToken("(")){var o=[];if(!n.matchOpToken(")")){do{o.push(e.requireElement("expression",n))}while(n.matchOpToken(","));n.requireOpToken(")")}if(r.root)var a={type:"functionCall",root:r,argExressions:o,args:[r.root,o],op:function(e,n,o){t.nullCheck(n,r.root);var a=n[r.prop.value];return t.nullCheck(a,r),a.hyperfunc&&o.push(e),a.apply(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}};else a={type:"functionCall",root:r,argExressions:o,args:[r,o],op:function(e,n,o){return t.nullCheck(n,r),n.hyperfunc&&o.push(e),n.apply(null,o)},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",n,a)}}),E.addIndirectExpression("attributeRefAccess",function(e,t,n,r){var o=e.parseElement("attributeRef",n);if(o)return{type:"attributeRefAccess",root:r,attribute:o,args:[r],op:function(e,n){return t.resolveAttribute(n,o.name)},evaluate:function(e){return T.unifiedEval(this,e)}}}),E.addIndirectExpression("arrayIndex",function(e,t,n,r){if(n.matchOpToken("[")){var o=!1,a=!1,i=null,u=null;n.matchOpToken("..")?(o=!0,i=e.requireElement("expression",n)):(i=e.requireElement("expression",n),n.matchOpToken("..")&&(a=!0,"R_BRACKET"!==n.currentToken().type&&(u=e.parseElement("expression",n)))),n.requireOpToken("]");var l={type:"arrayIndex",root:r,prop:i,firstIndex:i,secondIndex:u,args:[r,i,u],op:function(e,t,n,r){return null==t?null:o?(n<0&&(n=t.length+n),t.slice(0,n+1)):a?null!=r?(r<0&&(r=t.length+r),t.slice(n,r+1)):t.slice(n):t[n]},evaluate:function(e){return T.unifiedEval(this,e)}};return E.parseElement("indirectExpression",n,l)}});var w=["em","ex","cap","ch","ic","rem","lh","rlh","vw","vh","vi","vb","vmin","vmax","cm","mm","Q","pc","pt","px"];E.addGrammarElement("postfixExpression",function(e,t,n){var r=e.parseElement("primaryExpression",n),o=n.matchAnyToken.apply(n,w)||n.matchOpToken("%");if(o)return{type:"stringPostfix",postfix:o.value,args:[r],op:function(e,t){return""+t+o.value},evaluate:function(e){return t.unifiedEval(this,e)}};var a=null;if(n.matchToken("s")||n.matchToken("seconds")?a=1e3:(n.matchToken("ms")||n.matchToken("milliseconds"))&&(a=1),a)return{type:"timeExpression",time:r,factor:a,args:[r],op:function(e,t){return t*a},evaluate:function(e){return t.unifiedEval(this,e)}};if(n.matchOpToken(":")){var i=n.requireTokenType("IDENTIFIER"),u=!n.matchOpToken("!");return{type:"typeCheck",typeName:i,nullOk:u,args:[r],op:function(e,n){if(t.typeCheck(n,i.value,u))return n;throw new Error("Typecheck failed!  Expected: "+i.value)},evaluate:function(e){return t.unifiedEval(this,e)}}}return r}),E.addGrammarElement("logicalNot",function(e,t,n){if(n.matchToken("not")){var r=e.requireElement("unaryExpression",n);return{type:"logicalNot",root:r,args:[r],op:function(e,t){return!t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("noExpression",function(e,t,n){if(n.matchToken("no")){var r=e.requireElement("unaryExpression",n);return{type:"noExpression",root:r,args:[r],op:function(e,n){return t.isEmpty(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("some",function(e,t,n){if(n.matchToken("some")){var r=e.requireElement("expression",n);return{type:"noExpression",root:r,args:[r],op:function(e,n){return!t.isEmpty(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("negativeNumber",function(e,t,n){if(n.matchOpToken("-")){var r=e.requireElement("unaryExpression",n);return{type:"negativeNumber",root:r,args:[r],op:function(e,t){return-1*t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("unaryExpression",function(e,t,n){return n.matchToken("the"),e.parseAnyOf(["beepExpression","logicalNot","relativePositionalExpression","positionalExpression","noExpression","negativeNumber","postfixExpression"],n)}),E.addGrammarElement("beepExpression",function(e,t,n){if(n.matchToken("beep!")){var r=e.parseElement("unaryExpression",n);if(r){r.booped=!0;var o=r.evaluate;return r.evaluate=function(e){var n=o.apply(r,arguments),a=e.me;if(t.triggerEvent(a,"hyperscript:beep",{element:a,expression:r,value:n})){var i,u=n;"String"===(i=n?n instanceof v?"ElementCollection":n.constructor?n.constructor.name:"unknown":"object (null)")?u='"'+u+'"':n instanceof v&&(u=Array.from(n)),console.log("///_ BEEP! The expression ("+r.sourceFor().substr(6)+") evaluates to:",u,"of type "+i)}return n},r}}});var S=function(e,t,n,r){var o=[];T.forEach(t,function(t){(t.matches(n)||t===e)&&o.push(t)});for(var a=0;a<o.length-1;a++)if(o[a]===e)return o[a+1];if(r){var i=o[0];if(i&&i.matches(n))return i}};E.addGrammarElement("relativePositionalExpression",function(e,t,n){var r=n.matchAnyToken("next","previous");if(r){if("next"===r.value)var o=!0;var a=e.parseElement("expression",n);if(n.matchToken("from")){n.pushFollow("in");try{var i=e.requireElement("unaryExpression",n)}finally{n.popFollow()}}else i=e.requireElement("implicitMeTarget",n);var u,l=!1;if(n.matchToken("in")){l=!0;var s=e.requireElement("unaryExpression",n)}else u=n.matchToken("within")?e.requireElement("unaryExpression",n):document.body;var c=!1;return n.matchToken("with")&&(n.requireToken("wrapping"),c=!0),{type:"relativePositionalExpression",from:i,forwardSearch:o,inSearch:l,wrapping:c,inElt:s,withinElt:u,operator:r.value,args:[a,i,s,u],op:function(e,t,n,r,a){var i,u,s=t.css;if(null==s)throw"Expected a CSS value";if(l){if(r)return o?S(n,r,s,c):(i=s,u=c,S(n,Array.from(r).reverse(),i,u))}else if(a)return o?function(e,t,n,r){for(var o=t.querySelectorAll(n),a=0;a<o.length;a++){var i=o[a];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return i}if(r)return o[0]}(n,a,s,c):function(e,t,n,r){for(var o=t.querySelectorAll(n),a=o.length-1;a>=0;a--){var i=o[a];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}if(r)return o[o.length-1]}(n,a,s,c)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("positionalExpression",function(e,t,n){var r=n.matchAnyToken("first","last","random");if(r){n.matchAnyToken("in","from","of");var o=e.requireElement("unaryExpression",n),a=r.value;return{type:"positionalExpression",rhs:o,operator:r.value,args:[o],op:function(e,t){if(t&&!Array.isArray(t)&&(t=t.children?t.children:Array.from(t)),t){if("first"===a)return t[0];if("last"===a)return t[t.length-1];if("random"===a)return t[Math.floor(Math.random()*t.length)]}},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("mathOperator",function(e,t,n){var r,o=e.parseElement("unaryExpression",n),a=null;for(r=n.matchAnyOpToken("+","-","*","/","%");r;){var i=r.value;(a=a||r).value!==i&&e.raiseParseError(n,"You must parenthesize math operations with different operators");var u=e.parseElement("unaryExpression",n);o={type:"mathOperator",lhs:o,rhs:u,operator:i,args:[o,u],op:function(e,t,n){return"+"===i?t+n:"-"===i?t-n:"*"===i?t*n:"/"===i?t/n:"%"===i?t%n:void 0},evaluate:function(e){return t.unifiedEval(this,e)}},r=n.matchAnyOpToken("+","-","*","/","%")}return o}),E.addGrammarElement("mathExpression",function(e,t,n){return e.parseAnyOf(["mathOperator","unaryExpression"],n)}),E.addGrammarElement("comparisonOperator",function(e,t,n){var r=e.parseElement("mathExpression",n),o=n.matchAnyOpToken("<",">","<=",">=","==","===","!=","!=="),a=o?o.value:null,i=!0,u=!1;if(null==a&&(n.matchToken("is")||n.matchToken("am")?n.matchToken("not")?n.matchToken("in")?a="not in":n.matchToken("a")?(a="not a",u=!0):n.matchToken("empty")?(a="not empty",i=!1):a="!=":n.matchToken("in")?a="in":n.matchToken("a")?(a="a",u=!0):n.matchToken("empty")?(a="empty",i=!1):n.matchToken("less")?(n.requireToken("than"),n.matchToken("or")?(n.requireToken("equal"),n.requireToken("to"),a="<="):a="<"):n.matchToken("greater")?(n.requireToken("than"),n.matchToken("or")?(n.requireToken("equal"),n.requireToken("to"),a=">="):a=">"):a="==":n.matchToken("exist")||n.matchToken("exists")?(a="exist",i=!1):n.matchToken("matches")||n.matchToken("match")?a="match":n.matchToken("contains")||n.matchToken("contain")?a="contain":n.matchToken("includes")||n.matchToken("include")?a="include":(n.matchToken("do")||n.matchToken("does"))&&(n.requireToken("not"),n.matchToken("matches")||n.matchToken("match")?a="not match":n.matchToken("contains")||n.matchToken("contain")?a="not contain":n.matchToken("exist")||n.matchToken("exist")?(a="not exist",i=!1):n.matchToken("include")?a="not include":e.raiseParseError(n,"Expected matches or contains"))),a){if(u)var l=n.requireTokenType("IDENTIFIER"),s=!n.matchOpToken("!");else if(i){var c=e.requireElement("mathExpression",n);"match"!==a&&"not match"!==a||(c=c.css?c.css:c)}var f=r;r={type:"comparisonOperator",operator:a,typeName:l,nullOk:s,lhs:r,rhs:c,args:[r,c],op:function(e,n,r){if("=="===a)return n==r;if("!="===a)return n!=r;if("match"===a)return null!=n&&g(f,n,r);if("not match"===a)return null==n||!g(f,n,r);if("in"===a)return null!=r&&k(c,r,n);if("not in"===a)return null==r||!k(c,r,n);if("contain"===a)return null!=n&&k(f,n,r);if("not contain"===a)return null==n||!k(f,n,r);if("include"===a)return null!=n&&k(f,n,r);if("not include"===a)return null==n||!k(f,n,r);if("==="===a)return n===r;if("!=="===a)return n!==r;if("<"===a)return n<r;if(">"===a)return n>r;if("<="===a)return n<=r;if(">="===a)return n>=r;if("empty"===a)return t.isEmpty(n);if("not empty"===a)return!t.isEmpty(n);if("exist"===a)return t.doesExist(n);if("not exist"===a)return!t.doesExist(n);if("a"===a)return t.typeCheck(n,l.value,s);if("not a"===a)return!t.typeCheck(n,l.value,s);throw"Unknown comparison : "+a},evaluate:function(e){return t.unifiedEval(this,e)}}}return r}),E.addGrammarElement("comparisonExpression",function(e,t,n){return e.parseAnyOf(["comparisonOperator","mathExpression"],n)}),E.addGrammarElement("logicalOperator",function(e,t,n){var r,o=e.parseElement("comparisonExpression",n),a=null;r=n.matchToken("and")||n.matchToken("or");for(var i=function(){(a=a||r).value!==r.value&&e.raiseParseError(n,"You must parenthesize logical operations with different operators"),u=e.requireElement("comparisonExpression",n);var i=r.value;o={type:"logicalOperator",operator:i,lhs:o,rhs:u,args:[o,u],op:function(e,t,n){return"and"===i?t&&n:t||n},evaluate:function(e){return t.unifiedEval(this,e)}},r=n.matchToken("and")||n.matchToken("or")};r;){var u;i()}return o}),E.addGrammarElement("logicalExpression",function(e,t,n){return e.parseAnyOf(["logicalOperator","mathExpression"],n)}),E.addGrammarElement("asyncExpression",function(e,t,n){return n.matchToken("async")?{type:"asyncExpression",value:e.requireElement("logicalExpression",n),evaluate:function(e){return{asyncWrapper:!0,value:this.value.evaluate(e)}}}:e.parseElement("logicalExpression",n)}),E.addGrammarElement("expression",function(e,t,n){return n.matchToken("the"),e.parseElement("asyncExpression",n)}),E.addGrammarElement("assignableExpression",function(e,t,n){n.matchToken("the");var r=e.parseElement("primaryExpression",n);return!r||"symbol"!==r.type&&"ofExpression"!==r.type&&"propertyAccess"!==r.type&&"attributeRefAccess"!==r.type&&"attributeRef"!==r.type&&"styleRef"!==r.type&&"arrayIndex"!==r.type&&"possessive"!==r.type?(E.raiseParseError(n,"A target expression must be writable.  The expression type '"+(r&&r.type)+"' is not."),r):r}),E.addGrammarElement("hyperscript",function(e,t,n){var r=[];if(n.hasMore())for(;e.featureStart(n.currentToken())||"("===n.currentToken().value;){var o=e.requireElement("feature",n);r.push(o),n.matchToken("end")}return{type:"hyperscript",features:r,apply:function(e,t,n){for(var o,a=s(r);!(o=a()).done;)o.value.install(e,t,n)}}});var N=function(e){var t=[];if("("===e.token(0).value&&(")"===e.token(1).value||","===e.token(2).value||")"===e.token(2).value)){e.matchOpToken("(");do{t.push(e.requireTokenType("IDENTIFIER"))}while(e.matchOpToken(","));e.requireOpToken(")")}return t};E.addFeature("on",function(e,t,n){if(n.matchToken("on")){var r=!1;n.matchToken("every")&&(r=!0);var o=[],a=null;do{var i=e.requireElement("eventName",n,"Expected event name").evaluate();a=a?a+" or "+i:"on "+i;var u=N(n),l=null;if(n.matchOpToken("[")&&(l=e.requireElement("expression",n),n.requireOpToken("]")),"NUMBER"===n.currentToken().type){var f=n.consumeToken(),m=parseInt(f.value);if(n.matchToken("to"))var p=n.consumeToken(),d=parseInt(p.value);else if(n.matchToken("and")){var v=!0;n.requireToken("on")}}if("intersection"===i){var h={};if(n.matchToken("with")&&(h.with=e.requireElement("expression",n).evaluate()),n.matchToken("having"))do{n.matchToken("margin")?h.rootMargin=e.requireElement("stringLike",n).evaluate():n.matchToken("threshold")?h.threshold=e.requireElement("expression",n).evaluate():e.raiseParseError(n,"Unknown intersection config specification")}while(n.matchToken("and"))}else if("mutation"===i){var E={};if(n.matchToken("of"))do{if(n.matchToken("anything"))E.attributes=!0,E.subtree=!0,E.characterData=!0,E.childList=!0;else if(n.matchToken("childList"))E.childList=!0;else if(n.matchToken("attributes"))E.attributes=!0,E.attributeOldValue=!0;else if(n.matchToken("subtree"))E.subtree=!0;else if(n.matchToken("characterData"))E.characterData=!0,E.characterDataOldValue=!0;else if("ATTRIBUTE_REF"===n.currentToken().type){var y=n.consumeToken();null==E.attributeFilter&&(E.attributeFilter=[]),0==y.value.indexOf("@")?E.attributeFilter.push(y.value.substring(1)):e.raiseParseError(n,"Only shorthand attribute references are allowed here")}else e.raiseParseError(n,"Unknown mutation config specification")}while(n.matchToken("or"));else E.attributes=!0,E.characterData=!0,E.childList=!0}var k=null,g=!1;if(n.matchToken("from")&&(n.matchToken("elsewhere")?g=!0:(k=e.parseElement("expression",n))||e.raiseParseError(n,'Expected either target value or "elsewhere".')),null===k&&!1===g&&n.matchToken("elsewhere")&&(g=!0),n.matchToken("in"))var x=e.parseElement("unaryExpression",n);if(n.matchToken("debounced")){n.requireToken("at");var b=e.requireElement("expression",n).evaluate({})}else if(n.matchToken("throttled")){n.requireToken("at");var q=e.requireElement("expression",n).evaluate({})}o.push({execCount:0,every:r,on:i,args:u,filter:l,from:k,inExpr:x,elsewhere:g,startCount:m,endCount:d,unbounded:v,debounceTime:b,throttleTime:q,mutationSpec:E,intersectionSpec:h,debounced:void 0,lastExec:void 0})}while(n.matchToken("or"));var w=!0;if(!r&&n.matchToken("queue"))if(n.matchToken("all"))w=!1;else if(n.matchToken("first"))var S=!0;else if(n.matchToken("none"))var I=!0;else n.requireToken("last");var R=e.requireElement("commandList",n);if(e.ensureTerminated(R),n.matchToken("catch")){var O=n.requireTokenType("IDENTIFIER").value,L=e.requireElement("commandList",n);e.ensureTerminated(L)}if(n.matchToken("finally")){var C=e.requireElement("commandList",n);e.ensureTerminated(C)}var A={displayName:a,events:o,start:R,every:r,execCount:0,errorHandler:L,errorSymbol:O,execute:function(e){var n=t.getEventQueueFor(e.me,A);if(n.executing&&!1===r){if(I||S&&n.queue.length>0)return;return w&&(n.queue.length=0),void n.queue.push(e)}A.execCount++,n.executing=!0,e.meta.onHalt=function(){n.executing=!1;var e=n.queue.shift();e&&setTimeout(function(){A.execute(e)},1)},e.meta.reject=function(n){console.error(n.message?n.message:n);var r=t.getHyperTrace(e,n);r&&r.print(),t.triggerEvent(e.me,"exception",{error:n})},R.execute(e)},install:function(e,n){for(var r,o=function(){var n=r.value;i=n.elsewhere?[document]:n.from?n.from.evaluate(t.makeContext(e,A,e,null)):[e],t.implicitLoop(i,function(r){var o=n.on;if(n.mutationSpec&&(o="hyperscript:mutation",new MutationObserver(function(e,t){A.executing||T.triggerEvent(r,o,{mutationList:e,observer:t})}).observe(r,n.mutationSpec)),n.intersectionSpec){o="hyperscript:insersection";var a=new IntersectionObserver(function(e){for(var t,n=s(e);!(t=n()).done;){var i=t.value,u={observer:a};(u=c(u,i)).intersecting=i.isIntersecting,T.triggerEvent(r,o,u)}},n.intersectionSpec);a.observe(r)}(r.addEventListener||r.on).call(r,o,function a(i){if("undefined"!=typeof Node&&e instanceof Node&&r!==e&&!e.isConnected)r.removeEventListener(o,a);else{var u=t.makeContext(e,A,e,i);if(!n.elsewhere||!e.contains(i.target)){n.from&&(u.result=r);for(var l,c=s(n.args);!(l=c()).done;){var f=l.value,m=u.event[f.value];void 0!==m?u[f.value]=m:"detail"in u.event&&(u[f.value]=u.event.detail[f.value])}if(u.meta.errorHandler=L,u.meta.errorSymbol=O,u.meta.finallyHandler=C,n.filter){var p=u.meta.context;u.meta.context=u.event;try{if(!n.filter.evaluate(u))return}finally{u.meta.context=p}}if(n.inExpr)for(var d=i.target;;){if(d.matches&&d.matches(n.inExpr.css)){u.result=d;break}if(null==(d=d.parentElement))return}if(n.execCount++,n.startCount)if(n.endCount){if(n.execCount<n.startCount||n.execCount>n.endCount)return}else if(n.unbounded){if(n.execCount<n.startCount)return}else if(n.execCount!==n.startCount)return;if(n.debounceTime)return n.debounced&&clearTimeout(n.debounced),void(n.debounced=setTimeout(function(){A.execute(u)},n.debounceTime));if(n.throttleTime){if(n.lastExec&&Date.now()<n.lastExec+n.throttleTime)return;n.lastExec=Date.now()}A.execute(u)}}})})},a=s(A.events);!(r=a()).done;){var i;o()}}};return e.setParent(R,A),A}}),E.addFeature("def",function(e,t,n){if(n.matchToken("def")){var r=e.requireElement("dotOrColonPath",n).evaluate(),o=r.split("."),a=o.pop(),i=[];if(n.matchOpToken("("))if(n.matchOpToken(")"));else{do{i.push(n.requireTokenType("IDENTIFIER"))}while(n.matchOpToken(","));n.requireOpToken(")")}var u=e.requireElement("commandList",n);if(n.matchToken("catch"))var l=n.requireTokenType("IDENTIFIER").value,s=e.parseElement("commandList",n);if(n.matchToken("finally")){var c=e.requireElement("commandList",n);e.ensureTerminated(c)}var f={displayName:a+"("+i.map(function(e){return e.value}).join(", ")+")",name:a,args:i,start:u,errorHandler:s,errorSymbol:l,finallyHandler:c,install:function(e,n){var m=function(){var r=t.makeContext(n,f,e,null);r.meta.errorHandler=s,r.meta.errorSymbol=l,r.meta.finallyHandler=c;for(var o=0;o<i.length;o++){var a=i[o],m=arguments[o];a&&(r[a.value]=m)}r.meta.caller=arguments[i.length],r.meta.caller&&(r.meta.callingCommand=r.meta.caller.meta.command);var p,d=null,v=new Promise(function(e,t){p=e,d=t});return u.execute(r),r.meta.returned?r.meta.returnValue:(r.meta.resolve=p,r.meta.reject=d,v)};m.hyperfunc=!0,m.hypername=r,t.assignToNamespace(e,o,a,m)}};return e.ensureTerminated(u),s&&e.ensureTerminated(s),e.setParent(u,f),f}}),E.addFeature("set",function(e,t,n){var r=e.parseElement("setCommand",n);if(r){"element"!==r.target.scope&&e.raiseParseError(n,"variables declared at the feature level must be element scoped.");var o={start:r,install:function(e,n){r&&r.execute(t.makeContext(e,o,e,null))}};return e.ensureTerminated(r),o}}),E.addFeature("init",function(e,t,n){if(n.matchToken("init")){var r=e.requireElement("commandList",n),o={start:r,install:function(e,n){setTimeout(function(){r&&r.execute(t.makeContext(e,o,e,null))},0)}};return e.ensureTerminated(r),e.setParent(r,o),o}}),E.addFeature("worker",function(e,t,n){n.matchToken("worker")&&e.raiseParseError(n,"In order to use the 'worker' feature, include the _hyperscript worker plugin. See https://hyperscript.org/features/worker/ for more info.")}),E.addFeature("behavior",function(e,t,n){if(n.matchToken("behavior")){var r=e.requireElement("dotOrColonPath",n).evaluate(),o=r.split("."),a=o.pop(),i=[];if(n.matchOpToken("(")&&!n.matchOpToken(")")){do{i.push(n.requireTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));n.requireOpToken(")")}for(var u=e.requireElement("hyperscript",n),l=0;l<u.features.length;l++)u.features[l].behavior=r;return{install:function(e,n){t.assignToNamespace(d.document&&d.document.body,o,a,function(e,n,o){for(var a=f(t.getInternalData(e),r+"Scope"),l=0;l<i.length;l++)a[i[l]]=o[i[l]];u.apply(e,n)})}}}}),E.addFeature("install",function(e,t,n){if(n.matchToken("install")){var r,o=e.requireElement("dotOrColonPath",n).evaluate(),a=o.split("."),i=e.parseElement("namedArgumentList",n);return r={install:function(e,n){t.unifiedEval({args:[i],op:function(t,r){for(var i=d,u=0;u<a.length;u++)if("object"!=typeof(i=i[a[u]])&&"function"!=typeof i)throw new Error("No such behavior defined as "+o);if(!(i instanceof Function))throw new Error(o+" is not a behavior");i(e,n,r)}},t.makeContext(e,r,e))}}}}),E.addGrammarElement("jsBody",function(e,t,n){for(var r=n.currentToken().start,o=n.currentToken(),a=[],i="",u=!1;n.hasMore();){o=n.consumeToken();var l=n.token(0,!0);if("IDENTIFIER"===l.type&&"end"===l.value)break;u?"IDENTIFIER"===o.type||"NUMBER"===o.type?i+=o.value:(""!==i&&a.push(i),i="",u=!1):"IDENTIFIER"===o.type&&"function"===o.value&&(u=!0)}return{type:"jsBody",exposedFunctionNames:a,jsSource:n.source.substring(r,o.end+1)}}),E.addFeature("js",function(e,t,n){if(n.matchToken("js")){var r=e.requireElement("jsBody",n),o=r.jsSource+"\nreturn { "+r.exposedFunctionNames.map(function(e){return e+":"+e}).join(",")+" } ",a=new Function(o);return{jsSource:o,function:a,exposedFunctionNames:r.exposedFunctionNames,install:function(){c(d,a())}}}}),E.addCommand("js",function(e,t,n){if(n.matchToken("js")){var r=[];if(n.matchOpToken("("))if(n.matchOpToken(")"));else{do{var o=n.requireTokenType("IDENTIFIER");r.push(o.value)}while(n.matchOpToken(","));n.requireOpToken(")")}var a=e.requireElement("jsBody",n);n.matchToken("end");var i=m(Function,r.concat([a.jsSource]));return{jsSource:a.jsSource,function:i,inputs:r,op:function(e){var n=[];r.forEach(function(r){n.push(t.resolveSymbol(r,e,"default"))});var o=i.apply(d,n);return o&&"function"==typeof o.then?new Promise(function(n){o.then(function(r){e.result=r,n(t.findNext(this,e))})}):(e.result=o,t.findNext(this,e))}}}}),E.addCommand("async",function(e,t,n){if(n.matchToken("async")){if(n.matchToken("do")){for(var r=o=e.requireElement("commandList",n);r.next;)r=r.next;r.next=t.HALT,n.requireToken("end")}else var o=e.requireElement("command",n);var a={body:o,op:function(e){return setTimeout(function(){o.execute(e)}),t.findNext(this,e)}};return e.setParent(o,a),a}}),E.addCommand("tell",function(e,t,n){var r=n.currentToken();if(n.matchToken("tell")){var o=e.requireElement("expression",n),a=e.requireElement("commandList",n);n.hasMore()&&!e.featureStart(n.currentToken())&&n.requireToken("end");var i="tell_"+r.start,u={value:o,body:a,args:[o],resolveNext:function(e){var n=e.meta.iterators[i];return n.index<n.value.length?(e.beingTold=n.value[n.index++],a):(e.beingTold=n.originalBeingTold,this.next?this.next:t.findNext(this.parent,e))},op:function(e,t){return null==t?t=[]:Array.isArray(t)||t instanceof NodeList||(t=[t]),e.meta.iterators[i]={originalBeingTold:e.beingTold,index:0,value:t},this.resolveNext(e)}};return e.setParent(a,u),u}}),E.addCommand("wait",function(e,t,n){if(n.matchToken("wait")){var r,o;if(n.matchToken("for")){n.matchToken("a");var a=[];do{var i=n.token(0);a.push("NUMBER"===i.type||"L_PAREN"===i.type?{time:e.requireElement("expression",n).evaluate()}:{name:E.requireElement("dotOrColonPath",n,"Expected event name").evaluate(),args:N(n)})}while(n.matchToken("or"));if(n.matchToken("from"))var u=e.requireElement("expression",n);return r={event:a,on:u,args:[u],op:function(e,n){var r=this,o=n||e.me;if(!(o instanceof EventTarget))throw new Error("Not a valid event target: "+this.on.sourceFor());return new Promise(function(n){for(var i,u=!1,l=function(){var a=i.value;f=function(o){if(e.result=o,a.args)for(var i,l=s(a.args);!(i=l()).done;){var c=i.value;e[c.value]=o[c.value]||(o.detail?o.detail[c.value]:null)}u||(u=!0,n(t.findNext(r,e)))},a.name?o.addEventListener(a.name,f,{once:!0}):null!=a.time&&setTimeout(f,a.time,a.time)},c=s(a);!(i=c()).done;){var f;l()}})}},r}return n.matchToken("a")?(n.requireToken("tick"),o=0):o=E.requireElement("expression",n),{type:"waitCmd",time:o,args:[o],op:function(e,n){var r=this;return new Promise(function(o){setTimeout(function(){o(t.findNext(r,e))},n)})},execute:function(e){return t.unifiedExec(this,e)}}}}),E.addGrammarElement("dotOrColonPath",function(e,t,n){var r=n.matchTokenType("IDENTIFIER");if(r){var o=[r.value],a=n.matchOpToken(".")||n.matchOpToken(":");if(a)do{o.push(n.requireTokenType("IDENTIFIER","NUMBER").value)}while(n.matchOpToken(a.value));return{type:"dotOrColonPath",path:o,evaluate:function(){return o.join(a?a.value:"")}}}}),E.addGrammarElement("eventName",function(e,t,n){var r;return(r=n.matchTokenType("STRING"))?{evaluate:function(){return r.value}}:e.parseElement("dotOrColonPath",n)}),E.addCommand("trigger",function(e,t,n){if(n.matchToken("trigger"))return x("trigger",e,t,n)}),E.addCommand("send",function(e,t,n){if(n.matchToken("send"))return x("send",e,t,n)});var I=function(e,t,n,r){if(r)if(e.commandBoundary(n.currentToken()))e.raiseParseError(n,"'return' commands must return a value.  If you do not wish to return a value, use 'exit' instead.");else var o=e.requireElement("expression",n);var a={value:o,args:[o],op:function(e,n){var r=e.meta.resolve;return e.meta.returned=!0,e.meta.returnValue=n,r&&(n?r(n):r()),t.HALT}};return a};E.addCommand("return",function(e,t,n){if(n.matchToken("return"))return I(e,t,n,!0)}),E.addCommand("exit",function(e,t,n){if(n.matchToken("exit"))return I(e,t,n,!1)}),E.addCommand("halt",function(e,t,n){if(n.matchToken("halt")){if(n.matchToken("the")){n.requireToken("event"),n.matchOpToken("'")&&n.requireToken("s");var r=!0}if(n.matchToken("bubbling"))var o=!0;else if(n.matchToken("default"))var a=!0;var i=I(e,t,n,!1);return{keepExecuting:!0,bubbling:o,haltDefault:a,exit:i,op:function(e){if(e.event)return o?e.event.stopPropagation():(a||e.event.stopPropagation(),e.event.preventDefault()),r?t.findNext(this,e):i}}}}),E.addCommand("log",function(e,t,n){if(n.matchToken("log")){for(var r=[e.parseElement("expression",n)];n.matchOpToken(",");)r.push(e.requireElement("expression",n));if(n.matchToken("with"))var o=e.requireElement("expression",n);var a={exprs:r,withExpr:o,args:[o,r],op:function(e,n,r){return n?n.apply(null,r):console.log.apply(null,r),t.findNext(this,e)}};return a}}),E.addCommand("throw",function(e,t,n){if(n.matchToken("throw")){var r=e.requireElement("expression",n),o={expr:r,args:[r],op:function(e,n){throw t.registerHyperTrace(e,n),n}};return o}});var R=function(e,t,n){var r=e.requireElement("expression",n),o={expr:r,args:[r],op:function(e,n){return e.result=n,t.findNext(o,e)}};return o};E.addCommand("call",function(e,t,n){if(n.matchToken("call")){var r=R(e,t,n);return r.expr&&"functionCall"!==r.expr.type&&e.raiseParseError(n,"Must be a function invocation"),r}}),E.addCommand("get",function(e,t,n){if(n.matchToken("get"))return R(e,t,n)}),E.addCommand("make",function(e,t,n){if(n.matchToken("make")){n.matchToken("a")||n.matchToken("an");var r,o=e.requireElement("expression",n),a=[];if("queryRef"!==o.type&&n.matchToken("from"))do{a.push(e.requireElement("expression",n))}while(n.matchOpToken(","));if(n.matchToken("called"))var i=e.requireElement("symbol",n);return"queryRef"===o.type?r={op:function(e){for(var n,r,a="div",u=[],l=/(?:(^|#|\.)([^#\. ]+))/g;n=l.exec(o.css);)""===n[1]?a=n[2].trim():"#"===n[1]?r=n[2].trim():u.push(n[2].trim());var s=document.createElement(a);void 0!==r&&(s.id=r);for(var c=0;c<u.length;c++)s.classList.add(u[c]);return e.result=s,i&&t.setSymbol(i.name,e,i.scope,s),t.findNext(this,e)}}:(r={args:[o,a],op:function(e,n,r){return e.result=m(n,r),i&&t.setSymbol(i.name,e,i.scope,e.result),t.findNext(this,e)}},r)}}),E.addGrammarElement("pseudoCommand",function(e,t,n){var r=n.token(1);if(!r||!r.op||"."!==r.value&&"("!==r.value)return null;for(var o=e.requireElement("primaryExpression",n),a=o.root,i=o;null!=a.root;)i=i.root,a=a.root;if("functionCall"!==o.type&&e.raiseParseError(n,"Pseudo-commands must be function calls"),"functionCall"===i.type&&null==i.root.root)if(n.matchAnyToken("the","to","on","with","into","from","at"))var u=e.requireElement("expression",n);else n.matchToken("me")&&(u=e.requireElement("implicitMeTarget",n));if(u)var l={type:"pseudoCommand",root:u,argExressions:i.argExressions,args:[u,i.argExressions],op:function(e,n,r){t.nullCheck(n,u);var o=n[i.root.name];return t.nullCheck(o,i),o.hyperfunc&&r.push(e),e.result=o.apply(n,r),t.findNext(l,e)},execute:function(e){return t.unifiedExec(this,e)}};else l={type:"pseudoCommand",expr:o,args:[o],op:function(e,n){return e.result=n,t.findNext(l,e)},execute:function(e){return t.unifiedExec(this,e)}};return l});var O=function(e,t,n,r,o){var a="symbol"===r.type,i="attributeRef"===r.type,u="styleRef"===r.type,l="arrayIndex"===r.type;i||u||a||null!=r.root||e.raiseParseError(n,"Can only put directly into symbols, not references");var s=null,c=null;if(a);else if(i||u){s=e.requireElement("implicitMeTarget",n);var f=r}else l?(c=r.firstIndex,s=r.root):(c=r.prop?r.prop.value:null,f=r.attribute,s=r.root);var m={target:r,symbolWrite:a,value:o,args:[s,c,o],op:function(e,n,o,i){return a?t.setSymbol(r.name,e,r.scope,i):(t.nullCheck(n,s),l?n[o]=i:t.implicitLoop(n,function(e){f?"attributeRef"===f.type?null==i?e.removeAttribute(f.name):e.setAttribute(f.name,i):e.style[f.name]=i:e[o]=i})),t.findNext(this,e)}};return m};E.addCommand("default",function(e,t,n){if(n.matchToken("default")){var r=e.requireElement("assignableExpression",n);n.requireToken("to");var o=e.requireElement("expression",n),a=O(e,t,n,r,o),i={target:r,value:o,setter:a,args:[r],op:function(e,n){return n?t.findNext(this,e):a}};return a.parent=i,i}}),E.addCommand("set",function(e,t,n){if(n.matchToken("set")){if("L_BRACE"===n.currentToken().type){var r=e.requireElement("objectLiteral",n);n.requireToken("on");var o={objectLiteral:r,target:a=e.requireElement("expression",n),args:[r,a],op:function(e,n,r){return c(r,n),t.findNext(this,e)}};return o}try{n.pushFollow("to");var a=e.requireElement("assignableExpression",n)}finally{n.popFollow()}n.requireToken("to");var i=e.requireElement("expression",n);return O(e,t,n,a,i)}}),E.addCommand("if",function(e,t,n){if(n.matchToken("if")){var r=e.requireElement("expression",n);n.matchToken("then");var o=e.parseElement("commandList",n);if(n.matchToken("else")||n.matchToken("otherwise"))var a=e.parseElement("commandList",n);n.hasMore()&&n.requireToken("end");var i={expr:r,trueBranch:o,falseBranch:a,args:[r],op:function(e,n){return n?o:a||t.findNext(this,e)}};return e.setParent(o,i),e.setParent(a,i),i}});var L=function(e,t,n,r){var o,a=t.currentToken();if(t.matchToken("for")||r){var i=t.requireTokenType("IDENTIFIER");o=i.value,t.requireToken("in");var u=e.requireElement("expression",t)}else if(t.matchToken("in"))o="it",u=e.requireElement("expression",t);else if(t.matchToken("while"))var l=e.requireElement("expression",t);else if(t.matchToken("until")){var s=!0;if(t.matchToken("event")){var c=E.requireElement("dotOrColonPath",t,"Expected event name");if(t.matchToken("from"))var f=e.requireElement("expression",t)}else l=e.requireElement("expression",t)}else if(e.commandBoundary(t.currentToken())||"forever"===t.currentToken().value){t.matchToken("forever");var m=!0}else{var p=e.requireElement("expression",t);t.requireToken("times")}if(t.matchToken("index"))var d=(i=t.requireTokenType("IDENTIFIER")).value;var v=e.parseElement("commandList",t);if(v&&c){for(var h=v;h.next;)h=h.next;var y={type:"waitATick",op:function(){return new Promise(function(e){setTimeout(function(){e(n.findNext(y))},0)})}};h.next=y}if(t.hasMore()&&t.requireToken("end"),null==o)var T=o="_implicit_repeat_"+a.start;else T=o+"_"+a.start;var k={identifier:o,indexIdentifier:d,slot:T,expression:u,forever:m,times:p,until:s,event:c,on:f,whileExpr:l,resolveNext:function(){return this},loop:v,args:[l,p],op:function(e,t,r){var a=e.meta.iterators[T],i=!1,u=null;if(this.forever)i=!0;else if(this.until)i=c?!1===e.meta.iterators[T].eventFired:!0!==t;else if(l)i=t;else if(r)i=a.index<r;else{var s=a.iterator.next();i=!s.done,u=s.value}return i?(e.result=a.value?e[o]=u:a.index,d&&(e[d]=a.index),a.index++,v):(e.meta.iterators[T]=null,n.findNext(this.parent,e))}};e.setParent(v,k);var g={name:"repeatInit",args:[u,c,f],op:function(e,t,n,r){var o={index:0,value:t,eventFired:!1};return e.meta.iterators[T]=o,t&&t[Symbol.iterator]&&(o.iterator=t[Symbol.iterator]()),c&&(r||e.me).addEventListener(n,function(t){e.meta.iterators[T].eventFired=!0},{once:!0}),k},execute:function(e){return n.unifiedExec(this,e)}};return e.setParent(k,g),g};if(E.addCommand("repeat",function(e,t,n){if(n.matchToken("repeat"))return L(e,n,t,!1)}),E.addCommand("for",function(e,t,n){if(n.matchToken("for"))return L(e,n,t,!0)}),E.addCommand("continue",function(e,t,n){if(n.matchToken("continue"))return{op:function(t){for(var r=this.parent;;r=r.parent)if(null==r&&e.raiseParseError(n,"Command `continue` cannot be used outside of a `repeat` loop."),null!=r.loop)return r.resolveNext(t)}}}),E.addCommand("break",function(e,t,n){if(n.matchToken("break"))return{op:function(r){for(var o=this.parent;;o=o.parent)if(null==o&&e.raiseParseError(n,"Command `continue` cannot be used outside of a `repeat` loop."),null!=o.loop)return t.findNext(o.parent,r)}}}),E.addGrammarElement("stringLike",function(e,t,n){return E.parseAnyOf(["string","nakedString"],n)}),E.addCommand("append",function(e,t,n){if(n.matchToken("append")){var r,o=e.requireElement("expression",n),a={type:"symbol",evaluate:function(e){return t.resolveSymbol("result",e)}};r=n.matchToken("to")?e.requireElement("expression",n):a;var i=null;"symbol"!==r.type&&"attributeRef"!==r.type&&null==r.root||(i=O(e,t,n,r,a));var u={value:o,target:r,args:[r,o],op:function(e,n,r){if(Array.isArray(n))return n.push(r),t.findNext(this,e);if(n instanceof Element)return n.innerHTML+=r,t.findNext(this,e);if(i)return e.result=(n||"")+r,i;throw Error("Unable to append a value!")},execute:function(e){return t.unifiedExec(this,e)}};return null!=i&&(i.parent=u),u}}),E.addCommand("increment",function(e,t,n){if(n.matchToken("increment")){var r,o=e.parseElement("assignableExpression",n);n.matchToken("by")&&(r=e.requireElement("expression",n));var a={type:"implicitIncrementOp",target:o,args:[o,r],op:function(e,t,n){var r=(t=t?parseFloat(t):0)+(n=n?parseFloat(n):1);return e.result=r,r},evaluate:function(e){return t.unifiedEval(this,e)}};return O(e,t,n,o,a)}}),E.addCommand("decrement",function(e,t,n){if(n.matchToken("decrement")){var r,o=e.parseElement("assignableExpression",n);n.matchToken("by")&&(r=e.requireElement("expression",n));var a={type:"implicitDecrementOp",target:o,args:[o,r],op:function(e,t,n){var r=(t=t?parseFloat(t):0)-(n=n?parseFloat(n):1);return e.result=r,r},evaluate:function(e){return t.unifiedEval(this,e)}};return O(e,t,n,o,a)}}),E.addCommand("fetch",function(e,t,n){if(n.matchToken("fetch")){var r=e.requireElement("stringLike",n);if(n.matchToken("as"))var o=b(n,e);if(n.matchToken("with")&&"{"!==n.currentToken().value)var a=e.parseElement("nakedNamedArgumentList",n);else a=e.parseElement("objectLiteral",n);null==o&&n.matchToken("as")&&(o=b(n,e));var i=o?o.type:"text",u=o?o.conversion:null,l={url:r,argExpressions:a,args:[r,a],op:function(e,n,r){var o=r||{};o.sender=e.me,o.headers=o.headers||{};var a=new AbortController,s=e.me.addEventListener("fetch:abort",function(){a.abort()},{once:!0});o.signal=a.signal,t.triggerEvent(e.me,"hyperscript:beforeFetch",o),t.triggerEvent(e.me,"fetch:beforeRequest",o);var c=!1;return(r=o).timeout&&setTimeout(function(){c||a.abort()},r.timeout),fetch(n,r).then(function(n){var r={response:n};return t.triggerEvent(e.me,"fetch:afterResponse",r),n=r.response,"response"===i?(e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)):"json"===i?n.json().then(function(n){return e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)}):n.text().then(function(n){return u&&(n=t.convertValue(n,u)),"html"===i&&(n=t.convertValue(n,"Fragment")),e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)})}).catch(function(n){throw t.triggerEvent(e.me,"fetch:error",{reason:n}),n}).finally(function(){e.me.removeEventListener("fetch:abort",s)})}};return l}}),"document"in d){var C=Array.from(document.querySelectorAll("script[type='text/hyperscript'][src]"));Promise.all(C.map(function(e){return fetch(e.src).then(function(e){return e.text()})})).then(function(e){return e.forEach(T.evaluate)}).then(function(){var e;e=function(){var e,t;(t=(e=document.querySelector('meta[name="htmx-config"]'))?function(e){try{return JSON.parse(e)}catch(e){return t=e,console.error?console.error(t):console.log&&console.log("ERROR: ",t),null}var t}(e.content):null)&&(p.config=c(p.config,t)),T.processNode(document.documentElement),document.addEventListener("htmx:load",function(e){T.processNode(e.detail.elt)})},"loading"!==document.readyState?setTimeout(e):document.addEventListener("DOMContentLoaded",e)})}var A=p=c(function(e,t){return T.evaluate(e,t)},{internals:{lexer:h,parser:E,runtime:T},ElementCollection:v,addFeature:function(e,t){E.addFeature(e,t)},addCommand:function(e,t){E.addCommand(e,t)},addLeafExpression:function(e,t){E.addLeafExpression(e,t)},addIndirectExpression:function(e,t){E.addIndirectExpression(e,t)},evaluate:T.evaluate.bind(T),parse:T.parse.bind(T),processNode:T.processNode.bind(T),config:{attributes:"_, script, data-script",defaultTransition:"all 500ms ease-in",disableSelector:"[disable-scripting], [data-disable-scripting]",conversions:y}});return function(e){e.addCommand("settle",function(e,t,n){if(n.matchToken("settle")){if(e.commandBoundary(n.currentToken()))r=e.requireElement("implicitMeTarget",n);else var r=e.requireElement("expression",n);var o={type:"settleCmd",args:[r],op:function(e,n){t.nullCheck(n,r);var a=null,i=!1,u=new Promise(function(e){a=e});return n.addEventListener("transitionstart",function(){i=!0},{once:!0}),setTimeout(function(){i||a(t.findNext(o,e))},500),n.addEventListener("transitionend",function(){a(t.findNext(o,e))},{once:!0}),u},execute:function(e){return t.unifiedExec(this,e)}};return o}}),e.addCommand("add",function(e,t,n){if(n.matchToken("add")){var r=e.parseElement("classRef",n),o=null,a=null;if(null==r)null==(o=e.parseElement("attributeRef",n))&&null==(a=e.parseElement("styleLiteral",n))&&e.raiseParseError(n,"Expected either a class reference or attribute expression");else for(var i=[r];r=e.parseElement("classRef",n);)i.push(r);if(n.matchToken("to"))var u=e.requireElement("expression",n);else u=e.requireElement("implicitMeTarget",n);if(n.matchToken("when")){a&&e.raiseParseError(n,"Only class and properties are supported with a when clause");var l=e.requireElement("expression",n)}return i?{classRefs:i,to:u,args:[u,i],op:function(e,n,r){return t.nullCheck(n,u),t.forEach(r,function(r){t.implicitLoop(n,function(n){l?(e.result=n,t.evaluateNoPromise(l,e)?n instanceof Element&&n.classList.add(r.className):n instanceof Element&&n.classList.remove(r.className),e.result=null):n instanceof Element&&n.classList.add(r.className)})}),t.findNext(this,e)}}:o?{type:"addCmd",attributeRef:o,to:u,args:[u],op:function(e,n,r){return t.nullCheck(n,u),t.implicitLoop(n,function(n){l?(e.result=n,t.evaluateNoPromise(l,e)?n.setAttribute(o.name,o.value):n.removeAttribute(o.name),e.result=null):n.setAttribute(o.name,o.value)}),t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}:{type:"addCmd",cssDeclaration:a,to:u,args:[u,a],op:function(e,n,r){return t.nullCheck(n,u),t.implicitLoop(n,function(e){e.style.cssText+=r}),t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}}}),e.internals.parser.addGrammarElement("styleLiteral",function(e,t,n){if(n.matchOpToken("{")){for(var r=[""],o=[];n.hasMore();){if(n.matchOpToken("\\"))n.consumeToken();else{if(n.matchOpToken("}"))break;if(n.matchToken("$")){var a=n.matchOpToken("{"),i=e.parseElement("expression",n);a&&n.requireOpToken("}"),o.push(i),r.push("")}else{var u=n.consumeToken();r[r.length-1]+=n.source.substring(u.start,u.end)}}r[r.length-1]+=n.lastWhitespace()}return{type:"styleLiteral",args:[o],op:function(e,t){var n="";return r.forEach(function(e,r){n+=e,r in t&&(n+=t[r])}),n},evaluate:function(e){return t.unifiedEval(this,e)}}}}),e.addCommand("remove",function(e,t,n){if(n.matchToken("remove")){var r=e.parseElement("classRef",n),o=null,a=null;if(null==r)null==(o=e.parseElement("attributeRef",n))&&null==(a=e.parseElement("expression",n))&&e.raiseParseError(n,"Expected either a class reference, attribute expression or value expression");else for(var i=[r];r=e.parseElement("classRef",n);)i.push(r);if(n.matchToken("from"))var u=e.requireElement("expression",n);else u=e.requireElement("implicitMeTarget",n);return a?{elementExpr:a,from:u,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(e){e.parentElement&&e.parentElement.removeChild(e)}),t.findNext(this,e)}}:{classRefs:i,attributeRef:o,elementExpr:a,from:u,args:[i,u],op:function(e,n,r){return t.nullCheck(r,u),n?t.forEach(n,function(e){t.implicitLoop(r,function(t){t.classList.remove(e.className)})}):t.implicitLoop(r,function(e){e.removeAttribute(o.name)}),t.findNext(this,e)}}}}),e.addCommand("toggle",function(e,t,n){if(n.matchToken("toggle")){if(n.matchAnyToken("the","my"),"STYLE_REF"===n.currentToken().type){var o=n.consumeToken().value.substr(1),a=!0,i=r(e,n,o);if(n.matchToken("of")){n.pushFollow("with");try{var u=e.requireElement("expression",n)}finally{n.popFollow()}}else u=e.requireElement("implicitMeTarget",n)}else if(n.matchToken("between")){var l=!0,s=e.parseElement("classRef",n);n.requireToken("and");var c=e.requireElement("classRef",n)}else{s=e.parseElement("classRef",n);var f=null;if(null==s)null==(f=e.parseElement("attributeRef",n))&&e.raiseParseError(n,"Expected either a class reference or attribute expression");else for(var m=[s];s=e.parseElement("classRef",n);)m.push(s)}if(!0!==a&&(u=n.matchToken("on")?e.requireElement("expression",n):e.requireElement("implicitMeTarget",n)),n.matchToken("for"))var p=e.requireElement("expression",n);else if(n.matchToken("until")){var d=e.requireElement("dotOrColonPath",n,"Expected event name");if(n.matchToken("from"))var v=e.requireElement("expression",n)}var h={classRef:s,classRef2:c,classRefs:m,attributeRef:f,on:u,time:p,evt:d,from:v,toggle:function(e,n,r,o){t.nullCheck(e,u),a?t.implicitLoop(e,function(e){i("toggle",e)}):l?t.implicitLoop(e,function(e){e.classList.contains(n.className)?(e.classList.remove(n.className),e.classList.add(r.className)):(e.classList.add(n.className),e.classList.remove(r.className))}):o?t.forEach(o,function(n){t.implicitLoop(e,function(e){e.classList.toggle(n.className)})}):t.forEach(e,function(e){e.hasAttribute(f.name)?e.removeAttribute(f.name):e.setAttribute(f.name,f.value)})},args:[u,p,d,v,s,c,m],op:function(e,n,r,o,a,i,u,l){return r?new Promise(function(o){h.toggle(n,i,u,l),setTimeout(function(){h.toggle(n,i,u,l),o(t.findNext(h,e))},r)}):o?new Promise(function(r){(a||e.me).addEventListener(o,function(){h.toggle(n,i,u,l),r(t.findNext(h,e))},{once:!0}),h.toggle(n,i,u,l)}):(this.toggle(n,i,u,l),t.findNext(h,e))}};return h}});var t={display:function(n,r,o){if(o)r.style.display=o;else if("toggle"===n)"none"===getComputedStyle(r).display?t.display("show",r,o):t.display("hide",r,o);else if("hide"===n){var a=e.internals.runtime.getInternalData(r);null==a.originalDisplay&&(a.originalDisplay=r.style.display),r.style.display="none"}else{var i=e.internals.runtime.getInternalData(r);i.originalDisplay&&"none"!==i.originalDisplay?r.style.display=i.originalDisplay:r.style.removeProperty("display")}},visibility:function(e,n,r){r?n.style.visibility=r:"toggle"===e?"hidden"===getComputedStyle(n).visibility?t.visibility("show",n,r):t.visibility("hide",n,r):n.style.visibility="hide"===e?"hidden":"visible"},opacity:function(e,n,r){r?n.style.opacity=r:"toggle"===e?"0"===getComputedStyle(n).opacity?t.opacity("show",n,r):t.opacity("hide",n,r):n.style.opacity="hide"===e?"0":"1"}},n=function(e,t,n){var r=n.currentToken();return"when"===r.value||"with"===r.value||e.commandBoundary(r)?e.parseElement("implicitMeTarget",n):e.parseElement("expression",n)},r=function(n,r,o){var a=e.config.defaultHideShowStrategy,i=t;e.config.hideShowStrategies&&(i=c(i,e.config.hideShowStrategies));var u=i[o=o||a||"display"];return null==u&&n.raiseParseError(r,"Unknown show/hide strategy : "+o),u};function o(t,n,r,o){if(null!=r)var a=t.resolveSymbol(r,n);else a=n;if(a instanceof Element||a instanceof HTMLDocument){for(;a.firstChild;)a.removeChild(a.firstChild);a.append(e.internals.runtime.convertValue(o,"Fragment"))}else{if(null==r)throw"Don't know how to put a value into "+typeof n;t.setSymbol(r,n,null,o)}}function a(e,t,n){var r;if(n.matchToken("the")||n.matchToken("element")||n.matchToken("elements")||"CLASS_REF"===n.currentToken().type||"ID_REF"===n.currentToken().type||n.currentToken().op&&"<"===n.currentToken().value){e.possessivesDisabled=!0;try{r=e.parseElement("expression",n)}finally{delete e.possessivesDisabled}n.matchOpToken("'")&&n.requireToken("s")}else if("IDENTIFIER"===n.currentToken().type&&"its"===n.currentToken().value){var o=n.matchToken("its");r={type:"pseudopossessiveIts",token:o,name:o.value,evaluate:function(e){return t.resolveSymbol("it",e)}}}else n.matchToken("my")||n.matchToken("me"),r=e.parseElement("implicitMeTarget",n);return r}e.addCommand("hide",function(e,t,o){if(o.matchToken("hide")){var a=n(e,0,o),i=null;o.matchToken("with")&&0===(i=o.requireTokenType("IDENTIFIER","STYLE_REF").value).indexOf("*")&&(i=i.substr(1));var u=r(e,o,i);return{target:a,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(e){u("hide",e)}),t.findNext(this,e)}}}}),e.addCommand("show",function(e,t,o){if(o.matchToken("show")){var a=n(e,0,o),i=null;o.matchToken("with")&&0===(i=o.requireTokenType("IDENTIFIER","STYLE_REF").value).indexOf("*")&&(i=i.substr(1));var u=null;if(o.matchOpToken(":")){var l=o.consumeUntilWhitespace();o.matchTokenType("WHITESPACE"),u=l.map(function(e){return e.value}).join("")}if(o.matchToken("when"))var s=e.requireElement("expression",o);var c=r(e,o,i);return{target:a,when:s,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(n){s?(e.result=n,t.evaluateNoPromise(s,e)?c("show",n,u):c("hide",n),e.result=null):c("show",n,u)}),t.findNext(this,e)}}}}),e.addCommand("take",function(e,t,n){if(n.matchToken("take")){var r=e.requireElement("classRef",n);if(n.matchToken("from"))var o=e.requireElement("expression",n);else o=r;if(n.matchToken("for"))var a=e.requireElement("expression",n);else a=e.requireElement("implicitMeTarget",n);return{classRef:r,from:o,forElt:a,args:[r,o,a],op:function(e,n,r,i){t.nullCheck(r,o),t.nullCheck(i,a);var u=n.className;return t.implicitLoop(r,function(e){e.classList.remove(u)}),t.implicitLoop(i,function(e){e.classList.add(u)}),t.findNext(this,e)}}}}),e.addCommand("put",function(e,t,n){if(n.matchToken("put")){var r=e.requireElement("expression",n),a=n.matchAnyToken("into","before","after");null==a&&n.matchToken("at")&&(n.matchToken("the"),a=n.matchAnyToken("start","end"),n.requireToken("of")),null==a&&e.raiseParseError(n,"Expected one of 'into', 'before', 'at start of', 'at end of', 'after'");var i=e.requireElement("expression",n),u=a.value,l=!1,s=!1,c=null,f=null;if("arrayIndex"===i.type&&"into"===u)l=!0,f=i.prop,c=i.root;else if(i.prop&&i.root&&"into"===u)f=i.prop.value,c=i.root;else if("symbol"===i.type&&"into"===u)s=!0,f=i.name;else if("attributeRef"===i.type&&"into"===u){var m=!0;f=i.name,c=e.requireElement("implicitMeTarget",n)}else if("styleRef"===i.type&&"into"===u){var p=!0;f=i.name,c=e.requireElement("implicitMeTarget",n)}else i.attribute&&"into"===u?(m="attributeRef"===i.attribute.type,p="styleRef"===i.attribute.type,f=i.attribute.name,c=i.root):c=i;var d={target:i,operation:u,symbolWrite:s,value:r,args:[c,f,r],op:function(e,n,r,a){if(s)o(t,e,r,a);else if(t.nullCheck(n,c),"into"===u)m?t.implicitLoop(n,function(e){e.setAttribute(r,a)}):p?t.implicitLoop(n,function(e){e.style[r]=a}):l?n[r]=a:t.implicitLoop(n,function(e){o(t,e,r,a)});else{var i="before"===u?Element.prototype.before:"after"===u?Element.prototype.after:"start"===u?Element.prototype.prepend:Element.prototype.append;t.implicitLoop(n,function(e){i.call(e,a instanceof Node?a:t.convertValue(a,"Fragment"))})}return t.findNext(this,e)}};return d}}),e.addCommand("transition",function(t,n,r){if(r.matchToken("transition")){for(var o=a(t,n,r),i=[],u=[],l=[],s=r.currentToken();!t.commandBoundary(s)&&"over"!==s.value&&"using"!==s.value;)"STYLE_REF"===r.currentToken().type?function(){var e=r.consumeToken().value.substr(1);i.push({type:"styleRefValue",evaluate:function(){return e}})}():i.push(t.requireElement("stringLike",r)),r.matchToken("from")?u.push(t.requireElement("expression",r)):u.push(null),r.requireToken("to"),r.matchToken("initial")?l.push({type:"initial_literal",evaluate:function(){return"initial"}}):l.push(t.requireElement("expression",r)),s=r.currentToken();if(r.matchToken("over"))var c=t.requireElement("expression",r);else if(r.matchToken("using"))var f=t.requireElement("expression",r);var m={to:l,args:[o,i,u,l,f,c],op:function(t,r,a,i,u,l,s){n.nullCheck(r,o);var c=[];return n.implicitLoop(r,function(t){var r=new Promise(function(r,o){var c=t.style.transition;t.style.transition=s?"all "+s+"ms ease-in":l||e.config.defaultTransition;for(var f=n.getInternalData(t),m=getComputedStyle(t),p={},d=0;d<m.length;d++){var v=m[d];p[v]=m[v]}for(f.initalStyles||(f.initalStyles=p),d=0;d<a.length;d++){var h=a[d],E=i[d];t.style[h]="computed"===E||null==E?p[h]:E}var y=!1,T=!1;t.addEventListener("transitionend",function(){T||(t.style.transition=c,T=!0,r())},{once:!0}),t.addEventListener("transitionstart",function(){y=!0},{once:!0}),setTimeout(function(){T||y||(t.style.transition=c,T=!0,r())},100),setTimeout(function(){for(var e=0;e<a.length;e++){var n=a[e],r=u[e];t.style[n]="initial"===r?f.initalStyles[n]:r}},0)});c.push(r)}),Promise.all(c).then(function(){return n.findNext(m,t)})}};return m}}),e.addCommand("measure",function(e,t,n){if(n.matchToken("measure")){var r=a(e,t,n),o=[];if(!e.commandBoundary(n.currentToken()))do{o.push(n.matchTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));return{properties:o,args:[r],op:function(e,n){t.nullCheck(n,r),0 in n&&(n=n[0]);var a=n.getBoundingClientRect(),i={top:n.scrollTop,left:n.scrollLeft,topMax:n.scrollTopMax,leftMax:n.scrollLeftMax,height:n.scrollHeight,width:n.scrollWidth};return e.result={x:a.x,y:a.y,left:a.left,top:a.top,right:a.right,bottom:a.bottom,width:a.width,height:a.height,bounds:a,scrollLeft:i.left,scrollTop:i.top,scrollLeftMax:i.leftMax,scrollTopMax:i.topMax,scrollWidth:i.width,scrollHeight:i.height,scroll:i},t.forEach(o,function(t){if(!(t in e.result))throw"No such measurement as "+t;e[t]=e.result[t]}),t.findNext(this,e)}}}}),e.addLeafExpression("closestExpr",function(e,t,n){if(n.matchToken("closest")){if(n.matchToken("parent"))var r=!0;var o=null;if("ATTRIBUTE_REF"===n.currentToken().type){var a=e.requireElement("attributeRefAccess",n,null);o="["+a.attribute.name+"]"}if(null==o){var i=e.requireElement("expression",n);null==i.css?e.raiseParseError(n,"Expected a CSS expression"):o=i.css}if(n.matchToken("to"))var u=e.parseElement("expression",n);else u=e.parseElement("implicitMeTarget",n);var l={type:"closestExpr",parentSearch:r,expr:i,css:o,to:u,args:[u],op:function(e,n){if(null==n)return null;var a=[];return t.implicitLoop(n,function(e){a.push(r?e.parentElement?e.parentElement.closest(o):null:e.closest(o))}),t.shouldAutoIterate(n)?a:a[0]},evaluate:function(e){return t.unifiedEval(this,e)}};return a?(a.root=l,a.args=[l],a):l}}),e.addCommand("go",function(e,t,n){if(n.matchToken("go")){if(n.matchToken("back"))var r=!0;else if(n.matchToken("to"),n.matchToken("url")){var o=e.requireElement("stringLike",n),a=!0;if(n.matchToken("in")){n.requireToken("new"),n.requireToken("window");var i=!0}}else{n.matchToken("the");var u=n.matchAnyToken("top","middle","bottom"),l=n.matchAnyToken("left","center","right");(u||l)&&n.requireToken("of"),o=e.requireElement("unaryExpression",n);var s=n.matchAnyOpToken("+","-");if(s){n.pushFollow("px");try{var c=e.requireElement("expression",n)}finally{n.popFollow()}}n.matchToken("px");var f=n.matchAnyToken("smoothly","instantly"),m={};u&&("top"===u.value?m.block="start":"bottom"===u.value?m.block="end":"middle"===u.value&&(m.block="center")),l&&("left"===l.value?m.inline="start":"center"===l.value?m.inline="center":"right"===l.value&&(m.inline="end")),f&&("smoothly"===f.value?m.behavior="smooth":"instantly"===f.value&&(m.behavior="instant"))}var p={target:o,args:[o,c],op:function(e,n,o){return r?window.history.back():a?n&&(i?window.open(n):window.location.href=n):t.implicitLoop(n,function(e){if(e===window&&(e=document.body),s){var t=e.getBoundingClientRect(),n=document.createElement("div");if("-"===s.value)var r=-o;else r=o;n.style.position="absolute",n.style.top=t.x+r+"px",n.style.left=t.y+r+"px",n.style.height=t.height+2*r+"px",n.style.width=t.width+2*r+"px",n.style.zIndex=""+Number.MIN_SAFE_INTEGER,n.style.opacity="0",document.body.appendChild(n),setTimeout(function(){document.body.removeChild(n)},100),e=n}e.scrollIntoView(m)}),t.findNext(p,e)}};return p}}),e.config.conversions.dynamicResolvers.push(function(t,n){if("Values"===t||0===t.indexOf("Values:")){var r=t.split(":")[1],o={};if((0,e.internals.runtime.implicitLoop)(n,function(e){var t=i(e);void 0===t?null!=e.querySelectorAll&&e.querySelectorAll("input,select,textarea").forEach(a):o[t.name]=t.value}),r){if("JSON"===r)return JSON.stringify(o);if("Form"===r)return new URLSearchParams(o).toString();throw"Unknown conversion: "+r}return o}function a(e){var t=i(e);null!=t&&(null!=o[t.name]?Array.isArray(o[t.name])&&Array.isArray(t.value)&&(o[t.name]=[].concat(o[t.name],t.value)):o[t.name]=t.value)}function i(e){try{var t={name:e.name,value:e.value};if(null==t.name||null==t.value)return;if("radio"==e.type&&0==e.checked)return;if("checkbox"==e.type&&(0==e.checked?t.value=void 0:"string"==typeof t.value&&(t.value=[t.value])),"select-multiple"==e.type){var n=e.querySelectorAll("option[selected]");t.value=[];for(var r=0;r<n.length;r++)t.value.push(n[r].value)}return t}catch(e){return}}}),e.config.conversions.HTML=function(e){return function e(t){if(t instanceof Array)return t.map(function(t){return e(t)}).join("");if(t instanceof HTMLElement)return t.outerHTML;if(t instanceof NodeList){for(var n="",r=0;r<t.length;r++){var o=t[r];o instanceof HTMLElement&&(n+=o.outerHTML)}return n}return t.toString?t.toString():""}(e)},e.config.conversions.Fragment=function(t){var n=document.createDocumentFragment();return e.internals.runtime.implicitLoop(t,function(e){if(e instanceof Node)n.append(e);else{var t=document.createElement("template");t.innerHTML=e,n.append(t.content)}}),n}}(A),A});
+//# sourceMappingURL=_hyperscript_web.min.js.map
diff --git a/common/static/lib/js/multiple-select.min.js b/common/static/lib/js/multiple-select.min.js
deleted file mode 100644
index 7bffe99d..00000000
--- a/common/static/lib/js/multiple-select.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
-  * multiple-select - Multiple select is a jQuery plugin to select multiple elements with checkboxes :).
-  *
-  * @version v1.5.2
-  * @homepage http://multiple-select.wenzhixin.net.cn
-  * @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
-  * @license MIT
-  */
-
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):e((t=t||self).jQuery)}(this,(function(t){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function r(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t}function u(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){if(!(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t)))return;var n=[],i=!0,r=!1,u=void 0;try{for(var o,s=t[Symbol.iterator]();!(i=(o=s.next()).done)&&(n.push(o.value),!e||n.length!==e);i=!0);}catch(t){r=!0,u=t}finally{try{i||null==s.return||s.return()}finally{if(r)throw u}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}function o(t){return function(t){if(Array.isArray(t)){for(var e=0,n=new Array(t.length);e<t.length;e++)n[e]=t[e];return n}}(t)||function(t){if(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t))return Array.from(t)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}t=t&&t.hasOwnProperty("default")?t.default:t;var s="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function l(t,e){return t(e={exports:{}},e.exports),e.exports}var a,c,h,f="object",p=function(t){return t&&t.Math==Math&&t},d=p(typeof globalThis==f&&globalThis)||p(typeof window==f&&window)||p(typeof self==f&&self)||p(typeof s==f&&s)||Function("return this")(),v=function(t){try{return!!t()}catch(t){return!0}},g=!v((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})),y={}.propertyIsEnumerable,E=Object.getOwnPropertyDescriptor,b={f:E&&!y.call({1:2},1)?function(t){var e=E(this,t);return!!e&&e.enumerable}:y},m=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},A={}.toString,F=function(t){return A.call(t).slice(8,-1)},S="".split,C=v((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==F(t)?S.call(t,""):Object(t)}:Object,k=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},D=function(t){return C(k(t))},O=function(t){return"object"==typeof t?null!==t:"function"==typeof t},w=function(t,e){if(!O(t))return t;var n,i;if(e&&"function"==typeof(n=t.toString)&&!O(i=n.call(t)))return i;if("function"==typeof(n=t.valueOf)&&!O(i=n.call(t)))return i;if(!e&&"function"==typeof(n=t.toString)&&!O(i=n.call(t)))return i;throw TypeError("Can't convert object to primitive value")},x={}.hasOwnProperty,$=function(t,e){return x.call(t,e)},B=d.document,j=O(B)&&O(B.createElement),T=function(t){return j?B.createElement(t):{}},L=!g&&!v((function(){return 7!=Object.defineProperty(T("div"),"a",{get:function(){return 7}}).a})),_=Object.getOwnPropertyDescriptor,I={f:g?_:function(t,e){if(t=D(t),e=w(e,!0),L)try{return _(t,e)}catch(t){}if($(t,e))return m(!b.f.call(t,e),t[e])}},R=function(t){if(!O(t))throw TypeError(String(t)+" is not an object");return t},M=Object.defineProperty,P={f:g?M:function(t,e,n){if(R(t),e=w(e,!0),R(n),L)try{return M(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(t[e]=n.value),t}},N=g?function(t,e,n){return P.f(t,e,m(1,n))}:function(t,e,n){return t[e]=n,t},H=function(t,e){try{N(d,t,e)}catch(n){d[t]=e}return e},G=l((function(t){var e=d["__core-js_shared__"]||H("__core-js_shared__",{});(t.exports=function(t,n){return e[t]||(e[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.2.1",mode:"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})})),U=G("native-function-to-string",Function.toString),W=d.WeakMap,V="function"==typeof W&&/native code/.test(U.call(W)),K=0,z=Math.random(),q=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++K+z).toString(36)},Y=G("keys"),J=function(t){return Y[t]||(Y[t]=q(t))},Q={},Z=d.WeakMap;if(V){var X=new Z,tt=X.get,et=X.has,nt=X.set;a=function(t,e){return nt.call(X,t,e),e},c=function(t){return tt.call(X,t)||{}},h=function(t){return et.call(X,t)}}else{var it=J("state");Q[it]=!0,a=function(t,e){return N(t,it,e),e},c=function(t){return $(t,it)?t[it]:{}},h=function(t){return $(t,it)}}var rt={set:a,get:c,has:h,enforce:function(t){return h(t)?c(t):a(t,{})},getterFor:function(t){return function(e){var n;if(!O(e)||(n=c(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}}},ut=l((function(t){var e=rt.get,n=rt.enforce,i=String(U).split("toString");G("inspectSource",(function(t){return U.call(t)})),(t.exports=function(t,e,r,u){var o=!!u&&!!u.unsafe,s=!!u&&!!u.enumerable,l=!!u&&!!u.noTargetGet;"function"==typeof r&&("string"!=typeof e||$(r,"name")||N(r,"name",e),n(r).source=i.join("string"==typeof e?e:"")),t!==d?(o?!l&&t[e]&&(s=!0):delete t[e],s?t[e]=r:N(t,e,r)):s?t[e]=r:H(e,r)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||U.call(this)}))})),ot=d,st=function(t){return"function"==typeof t?t:void 0},lt=function(t,e){return arguments.length<2?st(ot[t])||st(d[t]):ot[t]&&ot[t][e]||d[t]&&d[t][e]},at=Math.ceil,ct=Math.floor,ht=function(t){return isNaN(t=+t)?0:(t>0?ct:at)(t)},ft=Math.min,pt=function(t){return t>0?ft(ht(t),9007199254740991):0},dt=Math.max,vt=Math.min,gt=function(t,e){var n=ht(t);return n<0?dt(n+e,0):vt(n,e)},yt=function(t){return function(e,n,i){var r,u=D(e),o=pt(u.length),s=gt(i,o);if(t&&n!=n){for(;o>s;)if((r=u[s++])!=r)return!0}else for(;o>s;s++)if((t||s in u)&&u[s]===n)return t||s||0;return!t&&-1}},Et={includes:yt(!0),indexOf:yt(!1)},bt=Et.indexOf,mt=function(t,e){var n,i=D(t),r=0,u=[];for(n in i)!$(Q,n)&&$(i,n)&&u.push(n);for(;e.length>r;)$(i,n=e[r++])&&(~bt(u,n)||u.push(n));return u},At=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Ft=At.concat("length","prototype"),St={f:Object.getOwnPropertyNames||function(t){return mt(t,Ft)}},Ct={f:Object.getOwnPropertySymbols},kt=lt("Reflect","ownKeys")||function(t){var e=St.f(R(t)),n=Ct.f;return n?e.concat(n(t)):e},Dt=function(t,e){for(var n=kt(e),i=P.f,r=I.f,u=0;u<n.length;u++){var o=n[u];$(t,o)||i(t,o,r(e,o))}},Ot=/#|\.prototype\./,wt=function(t,e){var n=$t[xt(t)];return n==jt||n!=Bt&&("function"==typeof e?v(e):!!e)},xt=wt.normalize=function(t){return String(t).replace(Ot,".").toLowerCase()},$t=wt.data={},Bt=wt.NATIVE="N",jt=wt.POLYFILL="P",Tt=wt,Lt=I.f,_t=function(t,e){var n,i,r,u,o,s=t.target,l=t.global,a=t.stat;if(n=l?d:a?d[s]||H(s,{}):(d[s]||{}).prototype)for(i in e){if(u=e[i],r=t.noTargetGet?(o=Lt(n,i))&&o.value:n[i],!Tt(l?i:s+(a?".":"#")+i,t.forced)&&void 0!==r){if(typeof u==typeof r)continue;Dt(u,r)}(t.sham||r&&r.sham)&&N(u,"sham",!0),ut(n,i,u,t)}},It=!!Object.getOwnPropertySymbols&&!v((function(){return!String(Symbol())})),Rt=d.Symbol,Mt=G("wks"),Pt=function(t){return Mt[t]||(Mt[t]=It&&Rt[t]||(It?Rt:q)("Symbol."+t))},Nt=Object.keys||function(t){return mt(t,At)},Ht=g?Object.defineProperties:function(t,e){R(t);for(var n,i=Nt(e),r=i.length,u=0;r>u;)P.f(t,n=i[u++],e[n]);return t},Gt=lt("document","documentElement"),Ut=J("IE_PROTO"),Wt=function(){},Vt=function(){var t,e=T("iframe"),n=At.length;for(e.style.display="none",Gt.appendChild(e),e.src=String("javascript:"),(t=e.contentWindow.document).open(),t.write("<script>document.F=Object<\/script>"),t.close(),Vt=t.F;n--;)delete Vt.prototype[At[n]];return Vt()},Kt=Object.create||function(t,e){var n;return null!==t?(Wt.prototype=R(t),n=new Wt,Wt.prototype=null,n[Ut]=t):n=Vt(),void 0===e?n:Ht(n,e)};Q[Ut]=!0;var zt=Pt("unscopables"),qt=Array.prototype;null==qt[zt]&&N(qt,zt,Kt(null));var Yt=function(t){qt[zt][t]=!0},Jt=Et.includes;_t({target:"Array",proto:!0},{includes:function(t){return Jt(this,t,arguments.length>1?arguments[1]:void 0)}}),Yt("includes");var Qt=function(t){return Object(k(t))},Zt=Object.assign,Xt=!Zt||v((function(){var t={},e={},n=Symbol();return t[n]=7,"abcdefghijklmnopqrst".split("").forEach((function(t){e[t]=t})),7!=Zt({},t)[n]||"abcdefghijklmnopqrst"!=Nt(Zt({},e)).join("")}))?function(t,e){for(var n=Qt(t),i=arguments.length,r=1,u=Ct.f,o=b.f;i>r;)for(var s,l=C(arguments[r++]),a=u?Nt(l).concat(u(l)):Nt(l),c=a.length,h=0;c>h;)s=a[h++],g&&!o.call(l,s)||(n[s]=l[s]);return n}:Zt;_t({target:"Object",stat:!0,forced:Object.assign!==Xt},{assign:Xt});var te=Pt("match"),ee=function(t){var e;return O(t)&&(void 0!==(e=t[te])?!!e:"RegExp"==F(t))},ne=function(t){if(ee(t))throw TypeError("The method doesn't accept regular expressions");return t},ie=Pt("match");_t({target:"String",proto:!0,forced:!function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[ie]=!1,"/./"[t](e)}catch(t){}}return!1}("includes")},{includes:function(t){return!!~String(k(this)).indexOf(ne(t),arguments.length>1?arguments[1]:void 0)}});var re="\t\n\v\f\r                 \u2028\u2029\ufeff",ue="["+re+"]",oe=RegExp("^"+ue+ue+"*"),se=RegExp(ue+ue+"*$"),le=function(t){return function(e){var n=String(k(e));return 1&t&&(n=n.replace(oe,"")),2&t&&(n=n.replace(se,"")),n}},ae={start:le(1),end:le(2),trim:le(3)},ce=ae.trim;_t({target:"String",proto:!0,forced:function(t){return v((function(){return!!re[t]()||"​…᠎"!="​…᠎"[t]()||re[t].name!==t}))}("trim")},{trim:function(){return ce(this)}});var he={name:"",placeholder:"",data:void 0,locale:void 0,selectAll:!0,single:void 0,singleRadio:!1,multiple:!1,hideOptgroupCheckboxes:!1,multipleWidth:80,width:void 0,dropWidth:void 0,maxHeight:250,maxHeightUnit:"px",position:"bottom",displayValues:!1,displayTitle:!1,displayDelimiter:", ",minimumCountSelected:3,ellipsis:!1,isOpen:!1,keepOpen:!1,openOnHover:!1,container:null,filter:!1,filterGroup:!1,filterPlaceholder:"",filterAcceptOnEnter:!1,filterByDataLength:void 0,customFilter:function(t,e){return t.includes(e)},showClear:!1,animate:void 0,styler:function(){return!1},textTemplate:function(t){return t[0].innerHTML.trim()},labelTemplate:function(t){return t[0].getAttribute("label")},onOpen:function(){return!1},onClose:function(){return!1},onCheckAll:function(){return!1},onUncheckAll:function(){return!1},onFocus:function(){return!1},onBlur:function(){return!1},onOptgroupClick:function(){return!1},onClick:function(){return!1},onFilter:function(){return!1},onClear:function(){return!1},onAfterCreate:function(){return!1}},fe={formatSelectAll:function(){return"[Select all]"},formatAllSelected:function(){return"All selected"},formatCountSelected:function(t,e){return t+" of "+e+" selected"},formatNoMatchesFound:function(){return"No matches found"}};Object.assign(he,fe);var pe={VERSION:"1.5.2",BLOCK_ROWS:50,CLUSTER_BLOCKS:4,DEFAULTS:he,METHODS:["getOptions","refreshOptions","getSelects","setSelects","enable","disable","open","close","check","uncheck","checkAll","uncheckAll","checkInvert","focus","blur","refresh","destroy"],LOCALES:{en:fe,"en-US":fe}},de=Array.isArray||function(t){return"Array"==F(t)},ve=St.f,ge={}.toString,ye="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],Ee={f:function(t){return ye&&"[object Window]"==ge.call(t)?function(t){try{return ve(t)}catch(t){return ye.slice()}}(t):ve(D(t))}},be={f:Pt},me=P.f,Ae=function(t){var e=ot.Symbol||(ot.Symbol={});$(e,t)||me(e,t,{value:be.f(t)})},Fe=P.f,Se=Pt("toStringTag"),Ce=function(t,e,n){t&&!$(t=n?t:t.prototype,Se)&&Fe(t,Se,{configurable:!0,value:e})},ke=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t},De=Pt("species"),Oe=function(t,e){var n;return de(t)&&("function"!=typeof(n=t.constructor)||n!==Array&&!de(n.prototype)?O(n)&&null===(n=n[De])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===e?0:e)},we=[].push,xe=function(t){var e=1==t,n=2==t,i=3==t,r=4==t,u=6==t,o=5==t||u;return function(s,l,a,c){for(var h,f,p=Qt(s),d=C(p),v=function(t,e,n){if(ke(t),void 0===e)return t;switch(n){case 0:return function(){return t.call(e)};case 1:return function(n){return t.call(e,n)};case 2:return function(n,i){return t.call(e,n,i)};case 3:return function(n,i,r){return t.call(e,n,i,r)}}return function(){return t.apply(e,arguments)}}(l,a,3),g=pt(d.length),y=0,E=c||Oe,b=e?E(s,g):n?E(s,0):void 0;g>y;y++)if((o||y in d)&&(f=v(h=d[y],y,p),t))if(e)b[y]=f;else if(f)switch(t){case 3:return!0;case 5:return h;case 6:return y;case 2:we.call(b,h)}else if(r)return!1;return u?-1:i||r?r:b}},$e={forEach:xe(0),map:xe(1),filter:xe(2),some:xe(3),every:xe(4),find:xe(5),findIndex:xe(6)},Be=$e.forEach,je=J("hidden"),Te=Pt("toPrimitive"),Le=rt.set,_e=rt.getterFor("Symbol"),Ie=Object.prototype,Re=d.Symbol,Me=d.JSON,Pe=Me&&Me.stringify,Ne=I.f,He=P.f,Ge=Ee.f,Ue=b.f,We=G("symbols"),Ve=G("op-symbols"),Ke=G("string-to-symbol-registry"),ze=G("symbol-to-string-registry"),qe=G("wks"),Ye=d.QObject,Je=!Ye||!Ye.prototype||!Ye.prototype.findChild,Qe=g&&v((function(){return 7!=Kt(He({},"a",{get:function(){return He(this,"a",{value:7}).a}})).a}))?function(t,e,n){var i=Ne(Ie,e);i&&delete Ie[e],He(t,e,n),i&&t!==Ie&&He(Ie,e,i)}:He,Ze=function(t,e){var n=We[t]=Kt(Re.prototype);return Le(n,{type:"Symbol",tag:t,description:e}),g||(n.description=e),n},Xe=It&&"symbol"==typeof Re.iterator?function(t){return"symbol"==typeof t}:function(t){return Object(t)instanceof Re},tn=function(t,e,n){t===Ie&&tn(Ve,e,n),R(t);var i=w(e,!0);return R(n),$(We,i)?(n.enumerable?($(t,je)&&t[je][i]&&(t[je][i]=!1),n=Kt(n,{enumerable:m(0,!1)})):($(t,je)||He(t,je,m(1,{})),t[je][i]=!0),Qe(t,i,n)):He(t,i,n)},en=function(t,e){R(t);var n=D(e),i=Nt(n).concat(on(n));return Be(i,(function(e){g&&!nn.call(n,e)||tn(t,e,n[e])})),t},nn=function(t){var e=w(t,!0),n=Ue.call(this,e);return!(this===Ie&&$(We,e)&&!$(Ve,e))&&(!(n||!$(this,e)||!$(We,e)||$(this,je)&&this[je][e])||n)},rn=function(t,e){var n=D(t),i=w(e,!0);if(n!==Ie||!$(We,i)||$(Ve,i)){var r=Ne(n,i);return!r||!$(We,i)||$(n,je)&&n[je][i]||(r.enumerable=!0),r}},un=function(t){var e=Ge(D(t)),n=[];return Be(e,(function(t){$(We,t)||$(Q,t)||n.push(t)})),n},on=function(t){var e=t===Ie,n=Ge(e?Ve:D(t)),i=[];return Be(n,(function(t){!$(We,t)||e&&!$(Ie,t)||i.push(We[t])})),i};It||(ut((Re=function(){if(this instanceof Re)throw TypeError("Symbol is not a constructor");var t=arguments.length&&void 0!==arguments[0]?String(arguments[0]):void 0,e=q(t),n=function(t){this===Ie&&n.call(Ve,t),$(this,je)&&$(this[je],e)&&(this[je][e]=!1),Qe(this,e,m(1,t))};return g&&Je&&Qe(Ie,e,{configurable:!0,set:n}),Ze(e,t)}).prototype,"toString",(function(){return _e(this).tag})),b.f=nn,P.f=tn,I.f=rn,St.f=Ee.f=un,Ct.f=on,g&&(He(Re.prototype,"description",{configurable:!0,get:function(){return _e(this).description}}),ut(Ie,"propertyIsEnumerable",nn,{unsafe:!0})),be.f=function(t){return Ze(Pt(t),t)}),_t({global:!0,wrap:!0,forced:!It,sham:!It},{Symbol:Re}),Be(Nt(qe),(function(t){Ae(t)})),_t({target:"Symbol",stat:!0,forced:!It},{for:function(t){var e=String(t);if($(Ke,e))return Ke[e];var n=Re(e);return Ke[e]=n,ze[n]=e,n},keyFor:function(t){if(!Xe(t))throw TypeError(t+" is not a symbol");if($(ze,t))return ze[t]},useSetter:function(){Je=!0},useSimple:function(){Je=!1}}),_t({target:"Object",stat:!0,forced:!It,sham:!g},{create:function(t,e){return void 0===e?Kt(t):en(Kt(t),e)},defineProperty:tn,defineProperties:en,getOwnPropertyDescriptor:rn}),_t({target:"Object",stat:!0,forced:!It},{getOwnPropertyNames:un,getOwnPropertySymbols:on}),_t({target:"Object",stat:!0,forced:v((function(){Ct.f(1)}))},{getOwnPropertySymbols:function(t){return Ct.f(Qt(t))}}),Me&&_t({target:"JSON",stat:!0,forced:!It||v((function(){var t=Re();return"[null]"!=Pe([t])||"{}"!=Pe({a:t})||"{}"!=Pe(Object(t))}))},{stringify:function(t){for(var e,n,i=[t],r=1;arguments.length>r;)i.push(arguments[r++]);if(n=e=i[1],(O(e)||void 0!==t)&&!Xe(t))return de(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!Xe(e))return e}),i[1]=e,Pe.apply(Me,i)}}),Re.prototype[Te]||N(Re.prototype,Te,Re.prototype.valueOf),Ce(Re,"Symbol"),Q[je]=!0;var sn=P.f,ln=d.Symbol;if(g&&"function"==typeof ln&&(!("description"in ln.prototype)||void 0!==ln().description)){var an={},cn=function(){var t=arguments.length<1||void 0===arguments[0]?void 0:String(arguments[0]),e=this instanceof cn?new ln(t):void 0===t?ln():ln(t);return""===t&&(an[e]=!0),e};Dt(cn,ln);var hn=cn.prototype=ln.prototype;hn.constructor=cn;var fn=hn.toString,pn="Symbol(test)"==String(ln("test")),dn=/^Symbol\((.*)\)[^)]+$/;sn(hn,"description",{configurable:!0,get:function(){var t=O(this)?this.valueOf():this,e=fn.call(t);if($(an,t))return"";var n=pn?e.slice(7,-1):e.replace(dn,"$1");return""===n?void 0:n}}),_t({global:!0,forced:!0},{Symbol:cn})}Ae("iterator");var vn=function(t,e,n){var i=w(e);i in t?P.f(t,i,m(0,n)):t[i]=n},gn=Pt("species"),yn=function(t){return!v((function(){var e=[];return(e.constructor={})[gn]=function(){return{foo:1}},1!==e[t](Boolean).foo}))},En=Pt("isConcatSpreadable"),bn=!v((function(){var t=[];return t[En]=!1,t.concat()[0]!==t})),mn=yn("concat"),An=function(t){if(!O(t))return!1;var e=t[En];return void 0!==e?!!e:de(t)};_t({target:"Array",proto:!0,forced:!bn||!mn},{concat:function(t){var e,n,i,r,u,o=Qt(this),s=Oe(o,0),l=0;for(e=-1,i=arguments.length;e<i;e++)if(u=-1===e?o:arguments[e],An(u)){if(l+(r=pt(u.length))>9007199254740991)throw TypeError("Maximum allowed index exceeded");for(n=0;n<r;n++,l++)n in u&&vn(s,l,u[n])}else{if(l>=9007199254740991)throw TypeError("Maximum allowed index exceeded");vn(s,l++,u)}return s.length=l,s}});var Fn=$e.filter;_t({target:"Array",proto:!0,forced:!yn("filter")},{filter:function(t){return Fn(this,t,arguments.length>1?arguments[1]:void 0)}});var Sn=$e.find,Cn=!0;"find"in[]&&Array(1).find((function(){Cn=!1})),_t({target:"Array",proto:!0,forced:Cn},{find:function(t){return Sn(this,t,arguments.length>1?arguments[1]:void 0)}}),Yt("find");var kn,Dn,On,wn=!v((function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype})),xn=J("IE_PROTO"),$n=Object.prototype,Bn=wn?Object.getPrototypeOf:function(t){return t=Qt(t),$(t,xn)?t[xn]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?$n:null},jn=Pt("iterator"),Tn=!1;[].keys&&("next"in(On=[].keys())?(Dn=Bn(Bn(On)))!==Object.prototype&&(kn=Dn):Tn=!0),null==kn&&(kn={}),$(kn,jn)||N(kn,jn,(function(){return this}));var Ln={IteratorPrototype:kn,BUGGY_SAFARI_ITERATORS:Tn},_n=Ln.IteratorPrototype,In=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,n={};try{(t=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(n,[]),e=n instanceof Array}catch(t){}return function(n,i){return R(n),function(t){if(!O(t)&&null!==t)throw TypeError("Can't set "+String(t)+" as a prototype")}(i),e?t.call(n,i):n.__proto__=i,n}}():void 0),Rn=Ln.IteratorPrototype,Mn=Ln.BUGGY_SAFARI_ITERATORS,Pn=Pt("iterator"),Nn=function(){return this},Hn=function(t,e,n,i,r,u,o){!function(t,e,n){var i=e+" Iterator";t.prototype=Kt(_n,{next:m(1,n)}),Ce(t,i,!1)}(n,e,i);var s,l,a,c=function(t){if(t===r&&v)return v;if(!Mn&&t in p)return p[t];switch(t){case"keys":case"values":case"entries":return function(){return new n(this,t)}}return function(){return new n(this)}},h=e+" Iterator",f=!1,p=t.prototype,d=p[Pn]||p["@@iterator"]||r&&p[r],v=!Mn&&d||c(r),g="Array"==e&&p.entries||d;if(g&&(s=Bn(g.call(new t)),Rn!==Object.prototype&&s.next&&(Bn(s)!==Rn&&(In?In(s,Rn):"function"!=typeof s[Pn]&&N(s,Pn,Nn)),Ce(s,h,!0))),"values"==r&&d&&"values"!==d.name&&(f=!0,v=function(){return d.call(this)}),p[Pn]!==v&&N(p,Pn,v),r)if(l={values:c("values"),keys:u?v:c("keys"),entries:c("entries")},o)for(a in l)!Mn&&!f&&a in p||ut(p,a,l[a]);else _t({target:e,proto:!0,forced:Mn||f},l);return l},Gn=rt.set,Un=rt.getterFor("Array Iterator"),Wn=Hn(Array,"Array",(function(t,e){Gn(this,{type:"Array Iterator",target:D(t),index:0,kind:e})}),(function(){var t=Un(this),e=t.target,n=t.kind,i=t.index++;return!e||i>=e.length?(t.target=void 0,{value:void 0,done:!0}):"keys"==n?{value:i,done:!1}:"values"==n?{value:e[i],done:!1}:{value:[i,e[i]],done:!1}}),"values");Yt("keys"),Yt("values"),Yt("entries");var Vn=function(t,e){var n=[][t];return!n||!v((function(){n.call(null,e||function(){throw 1},1)}))},Kn=[].join,zn=C!=Object,qn=Vn("join",",");_t({target:"Array",proto:!0,forced:zn||qn},{join:function(t){return Kn.call(D(this),void 0===t?",":t)}});var Yn=$e.map;_t({target:"Array",proto:!0,forced:!yn("map")},{map:function(t){return Yn(this,t,arguments.length>1?arguments[1]:void 0)}});var Jn=Pt("species"),Qn=[].slice,Zn=Math.max;_t({target:"Array",proto:!0,forced:!yn("slice")},{slice:function(t,e){var n,i,r,u=D(this),o=pt(u.length),s=gt(t,o),l=gt(void 0===e?o:e,o);if(de(u)&&("function"!=typeof(n=u.constructor)||n!==Array&&!de(n.prototype)?O(n)&&null===(n=n[Jn])&&(n=void 0):n=void 0,n===Array||void 0===n))return Qn.call(u,s,l);for(i=new(void 0===n?Array:n)(Zn(l-s,0)),r=0;s<l;s++,r++)s in u&&vn(i,r,u[s]);return i.length=r,i}});var Xn=P.f,ti=Function.prototype,ei=ti.toString,ni=/^\s*function ([^ (]*)/;!g||"name"in ti||Xn(ti,"name",{configurable:!0,get:function(){try{return ei.call(this).match(ni)[1]}catch(t){return""}}});var ii=b.f,ri=function(t){return function(e){for(var n,i=D(e),r=Nt(i),u=r.length,o=0,s=[];u>o;)n=r[o++],g&&!ii.call(i,n)||s.push(t?[n,i[n]]:i[n]);return s}},ui={entries:ri(!0),values:ri(!1)}.entries;_t({target:"Object",stat:!0},{entries:function(t){return ui(t)}});var oi=v((function(){Nt(1)}));_t({target:"Object",stat:!0,forced:oi},{keys:function(t){return Nt(Qt(t))}});var si=Pt("toStringTag"),li="Arguments"==F(function(){return arguments}()),ai={};ai[Pt("toStringTag")]="z";var ci="[object z]"!==String(ai)?function(){return"[object "+function(t){var e,n,i;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=function(t,e){try{return t[e]}catch(t){}}(e=Object(t),si))?n:li?F(e):"Object"==(i=F(e))&&"function"==typeof e.callee?"Arguments":i}(this)+"]"}:ai.toString,hi=Object.prototype;ci!==hi.toString&&ut(hi,"toString",ci,{unsafe:!0});var fi=function(t){return function(e,n){var i,r,u=String(k(e)),o=ht(n),s=u.length;return o<0||o>=s?t?"":void 0:(i=u.charCodeAt(o))<55296||i>56319||o+1===s||(r=u.charCodeAt(o+1))<56320||r>57343?t?u.charAt(o):i:t?u.slice(o,o+2):r-56320+(i-55296<<10)+65536}},pi={codeAt:fi(!1),charAt:fi(!0)},di=pi.charAt,vi=rt.set,gi=rt.getterFor("String Iterator");Hn(String,"String",(function(t){vi(this,{type:"String Iterator",string:String(t),index:0})}),(function(){var t,e=gi(this),n=e.string,i=e.index;return i>=n.length?{value:void 0,done:!0}:(t=di(n,i),e.index+=t.length,{value:t,done:!1})}));var yi,Ei,bi=function(){var t=R(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e},mi=RegExp.prototype.exec,Ai=String.prototype.replace,Fi=mi,Si=(yi=/a/,Ei=/b*/g,mi.call(yi,"a"),mi.call(Ei,"a"),0!==yi.lastIndex||0!==Ei.lastIndex),Ci=void 0!==/()??/.exec("")[1];(Si||Ci)&&(Fi=function(t){var e,n,i,r,u=this;return Ci&&(n=new RegExp("^"+u.source+"$(?!\\s)",bi.call(u))),Si&&(e=u.lastIndex),i=mi.call(u,t),Si&&i&&(u.lastIndex=u.global?i.index+i[0].length:e),Ci&&i&&i.length>1&&Ai.call(i[0],n,(function(){for(r=1;r<arguments.length-2;r++)void 0===arguments[r]&&(i[r]=void 0)})),i});var ki=Fi,Di=Pt("species"),Oi=!v((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$<a>")})),wi=!v((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]})),xi=function(t,e,n,i){var r=Pt(t),u=!v((function(){var e={};return e[r]=function(){return 7},7!=""[t](e)})),o=u&&!v((function(){var e=!1,n=/a/;return n.exec=function(){return e=!0,null},"split"===t&&(n.constructor={},n.constructor[Di]=function(){return n}),n[r](""),!e}));if(!u||!o||"replace"===t&&!Oi||"split"===t&&!wi){var s=/./[r],l=n(r,""[t],(function(t,e,n,i,r){return e.exec===ki?u&&!r?{done:!0,value:s.call(e,n,i)}:{done:!0,value:t.call(n,e,i)}:{done:!1}})),a=l[0],c=l[1];ut(String.prototype,t,a),ut(RegExp.prototype,r,2==e?function(t,e){return c.call(t,this,e)}:function(t){return c.call(t,this)}),i&&N(RegExp.prototype[r],"sham",!0)}},$i=Pt("species"),Bi=pi.charAt,ji=function(t,e,n){return e+(n?Bi(t,e).length:1)},Ti=function(t,e){var n=t.exec;if("function"==typeof n){var i=n.call(t,e);if("object"!=typeof i)throw TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==F(t))throw TypeError("RegExp#exec called on incompatible receiver");return ki.call(t,e)},Li=[].push,_i=Math.min,Ii=!v((function(){return!RegExp(4294967295,"y")}));xi("split",2,(function(t,e,n){var i;return i="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,n){var i=String(k(this)),r=void 0===n?4294967295:n>>>0;if(0===r)return[];if(void 0===t)return[i];if(!ee(t))return e.call(i,t,r);for(var u,o,s,l=[],a=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),c=0,h=new RegExp(t.source,a+"g");(u=ki.call(h,i))&&!((o=h.lastIndex)>c&&(l.push(i.slice(c,u.index)),u.length>1&&u.index<i.length&&Li.apply(l,u.slice(1)),s=u[0].length,c=o,l.length>=r));)h.lastIndex===u.index&&h.lastIndex++;return c===i.length?!s&&h.test("")||l.push(""):l.push(i.slice(c)),l.length>r?l.slice(0,r):l}:"0".split(void 0,0).length?function(t,n){return void 0===t&&0===n?[]:e.call(this,t,n)}:e,[function(e,n){var r=k(this),u=null==e?void 0:e[t];return void 0!==u?u.call(e,r,n):i.call(String(r),e,n)},function(t,r){var u=n(i,t,this,r,i!==e);if(u.done)return u.value;var o=R(t),s=String(this),l=function(t,e){var n,i=R(t).constructor;return void 0===i||null==(n=R(i)[$i])?e:ke(n)}(o,RegExp),a=o.unicode,c=(o.ignoreCase?"i":"")+(o.multiline?"m":"")+(o.unicode?"u":"")+(Ii?"y":"g"),h=new l(Ii?o:"^(?:"+o.source+")",c),f=void 0===r?4294967295:r>>>0;if(0===f)return[];if(0===s.length)return null===Ti(h,s)?[s]:[];for(var p=0,d=0,v=[];d<s.length;){h.lastIndex=Ii?d:0;var g,y=Ti(h,Ii?s:s.slice(d));if(null===y||(g=_i(pt(h.lastIndex+(Ii?0:d)),s.length))===p)d=ji(s,d,a);else{if(v.push(s.slice(p,d)),v.length===f)return v;for(var E=1;E<=y.length-1;E++)if(v.push(y[E]),v.length===f)return v;d=p=g}}return v.push(s.slice(p)),v}]}),!Ii);var Ri={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0},Mi=$e.forEach,Pi=Vn("forEach")?function(t){return Mi(this,t,arguments.length>1?arguments[1]:void 0)}:[].forEach;for(var Ni in Ri){var Hi=d[Ni],Gi=Hi&&Hi.prototype;if(Gi&&Gi.forEach!==Pi)try{N(Gi,"forEach",Pi)}catch(t){Gi.forEach=Pi}}var Ui=Pt("iterator"),Wi=Pt("toStringTag"),Vi=Wn.values;for(var Ki in Ri){var zi=d[Ki],qi=zi&&zi.prototype;if(qi){if(qi[Ui]!==Vi)try{N(qi,Ui,Vi)}catch(t){qi[Ui]=Vi}if(qi[Wi]||N(qi,Wi,Ki),Ri[Ki])for(var Yi in Wn)if(qi[Yi]!==Wn[Yi])try{N(qi,Yi,Wn[Yi])}catch(t){qi[Yi]=Wn[Yi]}}}var Ji=function(){function t(e){var i=this;n(this,t),this.rows=e.rows,this.scrollEl=e.scrollEl,this.contentEl=e.contentEl,this.callback=e.callback,this.cache={},this.scrollTop=this.scrollEl.scrollTop,this.initDOM(this.rows),this.scrollEl.scrollTop=this.scrollTop,this.lastCluster=0;var r=function(){i.lastCluster!==(i.lastCluster=i.getNum())&&(i.initDOM(i.rows),i.callback())};this.scrollEl.addEventListener("scroll",r,!1),this.destroy=function(){i.contentEl.innerHtml="",i.scrollEl.removeEventListener("scroll",r,!1)}}return r(t,[{key:"initDOM",value:function(t){void 0===this.clusterHeight&&(this.cache.scrollTop=this.scrollEl.scrollTop,this.cache.data=this.contentEl.innerHTML=t[0]+t[0]+t[0],this.getRowsHeight(t));var e=this.initData(t,this.getNum()),n=e.rows.join(""),i=this.checkChanges("data",n),r=this.checkChanges("top",e.topOffset),u=this.checkChanges("bottom",e.bottomOffset),o=[];i&&r?(e.topOffset&&o.push(this.getExtra("top",e.topOffset)),o.push(n),e.bottomOffset&&o.push(this.getExtra("bottom",e.bottomOffset)),this.contentEl.innerHTML=o.join("")):u&&(this.contentEl.lastChild.style.height="".concat(e.bottomOffset,"px"))}},{key:"getRowsHeight",value:function(){if(void 0===this.itemHeight){var t=this.contentEl.children,e=t[Math.floor(t.length/2)];this.itemHeight=e.offsetHeight}this.blockHeight=this.itemHeight*pe.BLOCK_ROWS,this.clusterRows=pe.BLOCK_ROWS*pe.CLUSTER_BLOCKS,this.clusterHeight=this.blockHeight*pe.CLUSTER_BLOCKS}},{key:"getNum",value:function(){return this.scrollTop=this.scrollEl.scrollTop,Math.floor(this.scrollTop/(this.clusterHeight-this.blockHeight))||0}},{key:"initData",value:function(t,e){if(t.length<pe.BLOCK_ROWS)return{topOffset:0,bottomOffset:0,rowsAbove:0,rows:t};var n=Math.max((this.clusterRows-pe.BLOCK_ROWS)*e,0),i=n+this.clusterRows,r=Math.max(n*this.itemHeight,0),u=Math.max((t.length-i)*this.itemHeight,0),o=[],s=n;r<1&&s++;for(var l=n;l<i;l++)t[l]&&o.push(t[l]);return this.dataStart=n,this.dataEnd=i,{topOffset:r,bottomOffset:u,rowsAbove:s,rows:o}}},{key:"checkChanges",value:function(t,e){var n=e!==this.cache[t];return this.cache[t]=e,n}},{key:"getExtra",value:function(t,e){var n=document.createElement("li");return n.className="virtual-scroll-".concat(t),e&&(n.style.height="".concat(e,"px")),n.outerHTML}}]),t}(),Qi=Math.max,Zi=Math.min,Xi=Math.floor,tr=/\$([$&'`]|\d\d?|<[^>]*>)/g,er=/\$([$&'`]|\d\d?)/g;xi("replace",2,(function(t,e,n){return[function(n,i){var r=k(this),u=null==n?void 0:n[t];return void 0!==u?u.call(n,r,i):e.call(String(r),n,i)},function(t,r){var u=n(e,t,this,r);if(u.done)return u.value;var o=R(t),s=String(this),l="function"==typeof r;l||(r=String(r));var a=o.global;if(a){var c=o.unicode;o.lastIndex=0}for(var h=[];;){var f=Ti(o,s);if(null===f)break;if(h.push(f),!a)break;""===String(f[0])&&(o.lastIndex=ji(s,pt(o.lastIndex),c))}for(var p,d="",v=0,g=0;g<h.length;g++){f=h[g];for(var y=String(f[0]),E=Qi(Zi(ht(f.index),s.length),0),b=[],m=1;m<f.length;m++)b.push(void 0===(p=f[m])?p:String(p));var A=f.groups;if(l){var F=[y].concat(b,E,s);void 0!==A&&F.push(A);var S=String(r.apply(void 0,F))}else S=i(y,s,E,b,A,r);E>=v&&(d+=s.slice(v,E)+S,v=E+y.length)}return d+s.slice(v)}];function i(t,n,i,r,u,o){var s=i+t.length,l=r.length,a=er;return void 0!==u&&(u=Qt(u),a=tr),e.call(o,a,(function(e,o){var a;switch(o.charAt(0)){case"$":return"$";case"&":return t;case"`":return n.slice(0,i);case"'":return n.slice(s);case"<":a=u[o.slice(1,-1)];break;default:var c=+o;if(0===c)return e;if(c>l){var h=Xi(c/10);return 0===h?e:h<=l?void 0===r[h-1]?o.charAt(1):r[h-1]+o.charAt(1):e}a=r[c-1]}return void 0===a?"":a}))}}));var nr=function(t){if(t.normalize)return t.normalize("NFD").replace(/[\u0300-\u036F]/g,"");return[{base:"A",letters:/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},{base:"AA",letters:/[\uA732]/g},{base:"AE",letters:/[\u00C6\u01FC\u01E2]/g},{base:"AO",letters:/[\uA734]/g},{base:"AU",letters:/[\uA736]/g},{base:"AV",letters:/[\uA738\uA73A]/g},{base:"AY",letters:/[\uA73C]/g},{base:"B",letters:/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},{base:"C",letters:/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},{base:"D",letters:/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},{base:"DZ",letters:/[\u01F1\u01C4]/g},{base:"Dz",letters:/[\u01F2\u01C5]/g},{base:"E",letters:/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},{base:"F",letters:/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},{base:"G",letters:/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},{base:"H",letters:/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},{base:"I",letters:/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},{base:"J",letters:/[\u004A\u24BF\uFF2A\u0134\u0248]/g},{base:"K",letters:/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},{base:"L",letters:/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},{base:"LJ",letters:/[\u01C7]/g},{base:"Lj",letters:/[\u01C8]/g},{base:"M",letters:/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},{base:"N",letters:/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},{base:"NJ",letters:/[\u01CA]/g},{base:"Nj",letters:/[\u01CB]/g},{base:"O",letters:/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},{base:"OI",letters:/[\u01A2]/g},{base:"OO",letters:/[\uA74E]/g},{base:"OU",letters:/[\u0222]/g},{base:"P",letters:/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},{base:"Q",letters:/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},{base:"R",letters:/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},{base:"S",letters:/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},{base:"T",letters:/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},{base:"TZ",letters:/[\uA728]/g},{base:"U",letters:/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},{base:"V",letters:/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},{base:"VY",letters:/[\uA760]/g},{base:"W",letters:/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},{base:"X",letters:/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},{base:"Y",letters:/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},{base:"Z",letters:/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},{base:"a",letters:/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},{base:"aa",letters:/[\uA733]/g},{base:"ae",letters:/[\u00E6\u01FD\u01E3]/g},{base:"ao",letters:/[\uA735]/g},{base:"au",letters:/[\uA737]/g},{base:"av",letters:/[\uA739\uA73B]/g},{base:"ay",letters:/[\uA73D]/g},{base:"b",letters:/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},{base:"c",letters:/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},{base:"d",letters:/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},{base:"dz",letters:/[\u01F3\u01C6]/g},{base:"e",letters:/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},{base:"f",letters:/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},{base:"g",letters:/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},{base:"h",letters:/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},{base:"hv",letters:/[\u0195]/g},{base:"i",letters:/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},{base:"j",letters:/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},{base:"k",letters:/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},{base:"l",letters:/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},{base:"lj",letters:/[\u01C9]/g},{base:"m",letters:/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},{base:"n",letters:/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},{base:"nj",letters:/[\u01CC]/g},{base:"o",letters:/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},{base:"oi",letters:/[\u01A3]/g},{base:"ou",letters:/[\u0223]/g},{base:"oo",letters:/[\uA74F]/g},{base:"p",letters:/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},{base:"q",letters:/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},{base:"r",letters:/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},{base:"s",letters:/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},{base:"t",letters:/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},{base:"tz",letters:/[\uA729]/g},{base:"u",letters:/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},{base:"v",letters:/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},{base:"vy",letters:/[\uA761]/g},{base:"w",letters:/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},{base:"x",letters:/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},{base:"y",letters:/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},{base:"z",letters:/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}].reduce((function(t,e){var n=e.letters,i=e.base;return t.replace(n,i)}),t)},ir=function(t,e,n){var i=!0,r=!1,u=void 0;try{for(var o,s=t[Symbol.iterator]();!(i=(o=s.next()).done);i=!0){var l=o.value;if(l[e]===n||l[e]===+l[e]+""&&+l[e]===n)return l;if("optgroup"===l.type){var a=!0,c=!1,h=void 0;try{for(var f,p=l.children[Symbol.iterator]();!(a=(f=p.next()).done);a=!0){var d=f.value;if(d[e]===n||d[e]===+d[e]+""&&+d[e]===n)return d}}catch(t){c=!0,h=t}finally{try{a||null==p.return||p.return()}finally{if(c)throw h}}}}}catch(t){r=!0,u=t}finally{try{i||null==s.return||s.return()}finally{if(r)throw u}}},rr=function(t){return Object.keys(t).forEach((function(e){return void 0===t[e]?delete t[e]:""})),t},ur=function(){function i(e,r){n(this,i),this.$el=e,this.options=t.extend({},pe.DEFAULTS,r)}return r(i,[{key:"init",value:function(){this.initLocale(),this.initContainer(),this.initData(),this.initSelected(!0),this.initFilter(),this.initDrop(),this.initView(),this.options.onAfterCreate()}},{key:"initLocale",value:function(){if(this.options.locale){var e=t.fn.multipleSelect.locales,n=this.options.locale.split(/-|_/);n[0]=n[0].toLowerCase(),n[1]&&(n[1]=n[1].toUpperCase()),e[this.options.locale]?t.extend(this.options,e[this.options.locale]):e[n.join("-")]?t.extend(this.options,e[n.join("-")]):e[n[0]]&&t.extend(this.options,e[n[0]])}}},{key:"initContainer",value:function(){var e=this,n=this.$el[0],i=n.getAttribute("name")||this.options.name||"";this.$el.hide(),this.$label=this.$el.closest("label"),!this.$label.length&&this.$el.attr("id")&&(this.$label=t('label[for="'.concat(this.$el.attr("id"),'"]'))),this.$label.find(">input").length&&(this.$label=null),void 0===this.options.single&&(this.options.single=null===n.getAttribute("multiple")),this.$parent=t('\n      <div class="ms-parent '.concat(n.getAttribute("class")||"",'"\n      title="').concat(n.getAttribute("title")||"",'" />\n    ')),this.options.placeholder=this.options.placeholder||n.getAttribute("placeholder")||"",this.tabIndex=n.getAttribute("tabindex");var r="";if(null!==this.tabIndex&&(this.$el.attr("tabindex",-1),r=this.tabIndex&&'tabindex="'.concat(this.tabIndex,'"')),this.$choice=t('\n      <button type="button" class="ms-choice"'.concat(r,'>\n      <span class="placeholder">').concat(this.options.placeholder,"</span>\n      ").concat(this.options.showClear?'<div class="icon-close"></div>':"",'\n      <div class="icon-caret"></div>\n      </button>\n    ')),this.$drop=t('<div class="ms-drop '.concat(this.options.position,'" />')),this.$close=this.$choice.find(".icon-close"),this.options.dropWidth&&this.$drop.css("width",this.options.dropWidth),this.$el.after(this.$parent),this.$parent.append(this.$choice),this.$parent.append(this.$drop),n.disabled&&this.$choice.addClass("disabled"),this.selectAllName='data-name="selectAll'.concat(i,'"'),this.selectGroupName='data-name="selectGroup'.concat(i,'"'),this.selectItemName='data-name="selectItem'.concat(i,'"'),!this.options.keepOpen){var u=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return t=t||"".concat(+new Date).concat(~~(1e6*Math.random())),"click.multiple-select-".concat(t)}(this.$el.attr("id"));t(document).off(u).on(u,(function(i){t(i.target)[0]!==e.$choice[0]&&t(i.target).parents(".ms-choice")[0]!==e.$choice[0]&&(t(i.target)[0]===e.$drop[0]||t(i.target).parents(".ms-drop")[0]!==e.$drop[0]&&i.target!==n)&&e.options.isOpen&&e.close()}))}}},{key:"initData",value:function(){var n=this,i=[];if(this.options.data){if(Array.isArray(this.options.data))this.data=this.options.data.map((function(t){return"string"==typeof t||"number"==typeof t?{text:t,value:t}:t}));else if("object"===e(this.options.data)){for(var r=0,o=Object.entries(this.options.data);r<o.length;r++){var s=u(o[r],2),l=s[0],a=s[1];i.push({value:l,text:a})}this.data=i}}else t.each(this.$el.children(),(function(t,e){n.initRow(t,e)&&i.push(n.initRow(t,e))})),this.options.data=i,this.data=i,this.fromHtml=!0;this.dataTotal=function(t){var e=0;return t.forEach((function(t,n){"optgroup"===t.type?(t._key="group_".concat(n),t.visible=void 0===t.visible||t.visible,t.children.forEach((function(t,e){t._key="option_".concat(n,"_").concat(e),t.visible=void 0===t.visible||t.visible})),e+=t.children.length):(t._key="option_".concat(n),t.visible=void 0===t.visible||t.visible,e+=1)})),e}(this.data)}},{key:"initRow",value:function(e,n,i){var r=this,u={},o=t(n);return o.is("option")?(u.type="option",u.text=this.options.textTemplate(o),u.value=n.value,u.visible=!0,u.selected=!!n.selected,u.disabled=i||n.disabled,u.classes=n.getAttribute("class")||"",u.title=n.getAttribute("title")||"",o.data("value")&&(u._value=o.data("value")),Object.keys(o.data()).length&&(u._data=o.data()),u):o.is("optgroup")?(u.type="optgroup",u.label=this.options.labelTemplate(o),u.visible=!0,u.selected=!!n.selected,u.disabled=n.disabled,u.children=[],Object.keys(o.data()).length&&(u._data=o.data()),t.each(o.children(),(function(t,e){u.children.push(r.initRow(t,e,u.disabled))})),u):null}},{key:"initSelected",value:function(t){var e=0,n=!0,i=!1,r=void 0;try{for(var u,o=this.data[Symbol.iterator]();!(n=(u=o.next()).done);n=!0){var s=u.value;if("optgroup"===s.type){var l=s.children.filter((function(t){return t.selected&&!t.disabled&&t.visible})).length;s.selected=l&&l===s.children.filter((function(t){return!t.disabled&&t.visible})).length,e+=l}else e+=s.selected&&!s.disabled&&s.visible?1:0}}catch(t){i=!0,r=t}finally{try{n||null==o.return||o.return()}finally{if(i)throw r}}this.allSelected=this.data.filter((function(t){return t.selected&&!t.disabled&&t.visible})).length===this.data.filter((function(t){return!t.disabled&&t.visible})).length,t||(this.allSelected?this.options.onCheckAll():0===e&&this.options.onUncheckAll())}},{key:"initFilter",value:function(){if(this.filterText="",!this.options.filter&&this.options.filterByDataLength){var t=0,e=!0,n=!1,i=void 0;try{for(var r,u=this.data[Symbol.iterator]();!(e=(r=u.next()).done);e=!0){var o=r.value;"optgroup"===o.type?t+=o.children.length:t+=1}}catch(t){n=!0,i=t}finally{try{e||null==u.return||u.return()}finally{if(n)throw i}}this.options.filter=t>this.options.filterByDataLength}}},{key:"initDrop",value:function(){var t=this;this.initList(),this.update(!0),this.options.isOpen&&setTimeout((function(){t.open()}),50),this.options.openOnHover&&this.$parent.hover((function(){t.open()}),(function(){t.close()}))}},{key:"initList",value:function(){var t=[];this.options.filter&&t.push('\n        <div class="ms-search">\n          <input type="text" autocomplete="off" autocorrect="off"\n            autocapitalize="off" spellcheck="false"\n            placeholder="'.concat(this.options.filterPlaceholder,'">\n        </div>\n      ')),t.push("<ul></ul>"),this.$drop.html(t.join("")),this.$ul=this.$drop.find(">ul"),this.initListItems()}},{key:"initListItems",value:function(){var t=this,e=this.getListRows(),n=0;if(this.options.selectAll&&!this.options.single&&(n=-1),e.length>pe.BLOCK_ROWS*pe.CLUSTER_BLOCKS){this.virtualScroll&&this.virtualScroll.destroy();var i=this.$drop.is(":visible");i||this.$drop.css("left",-1e4).show();var r=function(){t.updateDataStart=t.virtualScroll.dataStart+n,t.updateDataEnd=t.virtualScroll.dataEnd+n,t.updateDataStart<0&&(t.updateDataStart=0),t.updateDataEnd>t.data.length&&(t.updateDataEnd=t.data.length)};this.virtualScroll=new Ji({rows:e,scrollEl:this.$ul[0],contentEl:this.$ul[0],callback:function(){r(),t.events()}}),r(),i||this.$drop.css("left",0).hide()}else this.$ul.html(e.join("")),this.updateDataStart=0,this.updateDataEnd=this.updateData.length,this.virtualScroll=null;this.events()}},{key:"getListRows",value:function(){var t=this,e=[];return this.options.selectAll&&!this.options.single&&e.push('\n        <li class="ms-select-all">\n        <label>\n        <input type="checkbox" '.concat(this.selectAllName).concat(this.allSelected?' checked="checked"':""," />\n        <span>").concat(this.options.formatSelectAll(),"</span>\n        </label>\n        </li>\n      ")),this.updateData=[],this.data.forEach((function(n){e.push.apply(e,o(t.initListItem(n)))})),e.push('<li class="ms-no-results">'.concat(this.options.formatNoMatchesFound(),"</li>")),e}},{key:"initListItem",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,i=t.title?'title="'.concat(t.title,'"'):"",r=this.options.multiple?"multiple":"",u=this.options.single?"radio":"checkbox",s="";if(!t.visible)return[];if(this.updateData.push(t),this.options.single&&!this.options.singleRadio&&(s="hide-radio "),t.selected&&(s+="selected "),"optgroup"===t.type){var l=this.options.styler(t),a=l?'style="'.concat(l,'"'):"",c=[],h=this.options.hideOptgroupCheckboxes||this.options.single?"<span ".concat(this.selectGroupName,' data-key="').concat(t._key,'"></span>'):'<input type="checkbox"\n          '.concat(this.selectGroupName,'\n          data-key="').concat(t._key,'"\n          ').concat(t.selected?' checked="checked"':"","\n          ").concat(t.disabled?' disabled="disabled"':"","\n        >");return s.includes("hide-radio")||!this.options.hideOptgroupCheckboxes&&!this.options.single||(s+="hide-radio "),c.push('\n        <li class="group '.concat(s,'" ').concat(a,'>\n        <label class="optgroup').concat(this.options.single||t.disabled?" disabled":"",'">\n        ').concat(h).concat(t.label,"\n        </label>\n        </li>\n      ")),t.children.forEach((function(t){c.push.apply(c,o(e.initListItem(t,1)))})),c}var f=this.options.styler(t),p=f?'style="'.concat(f,'"'):"";return s+=t.classes||"",n&&this.options.single&&(s+="option-level-".concat(n," ")),['\n      <li class="'.concat(r," ").concat(s,'" ').concat(i," ").concat(p,'>\n      <label class="').concat(t.disabled?"disabled":"",'">\n      <input type="').concat(u,'"\n        value="').concat(t.value,'"\n        data-key="').concat(t._key,'"\n        ').concat(this.selectItemName,"\n        ").concat(t.selected?' checked="checked"':"","\n        ").concat(t.disabled?' disabled="disabled"':"","\n      >\n      <span>").concat(t.text,"</span>\n      </label>\n      </li>\n    ")]}},{key:"events",value:function(){var e=this;this.$searchInput=this.$drop.find(".ms-search input"),this.$selectAll=this.$drop.find("input[".concat(this.selectAllName,"]")),this.$selectGroups=this.$drop.find("input[".concat(this.selectGroupName,"],span[").concat(this.selectGroupName,"]")),this.$selectItems=this.$drop.find("input[".concat(this.selectItemName,"]:enabled")),this.$disableItems=this.$drop.find("input[".concat(this.selectItemName,"]:disabled")),this.$noResults=this.$drop.find(".ms-no-results");var n=function(n){n.preventDefault(),t(n.target).hasClass("icon-close")||e[e.options.isOpen?"close":"open"]()};this.$label&&this.$label.length&&this.$label.off("click").on("click",(function(t){"label"===t.target.nodeName.toLowerCase()&&(n(t),e.options.filter&&e.options.isOpen||e.focus(),t.stopPropagation())})),this.$choice.off("click").on("click",n).off("focus").on("focus",this.options.onFocus).off("blur").on("blur",this.options.onBlur),this.$parent.off("keydown").on("keydown",(function(t){27!==t.which||e.options.keepOpen||(e.close(),e.$choice.focus())})),this.$close.off("click").on("click",(function(t){t.preventDefault(),e._checkAll(!1,!0),e.initSelected(!1),e.updateSelected(),e.update(),e.options.onClear()})),this.$searchInput.off("keydown").on("keydown",(function(t){9===t.keyCode&&t.shiftKey&&e.close()})).off("keyup").on("keyup",(function(t){if(e.options.filterAcceptOnEnter&&[13,32].includes(t.which)&&e.$searchInput.val()){if(e.options.single){var n=e.$selectItems.closest("li").filter(":visible");n.length&&e.setSelects([n.first().find("input[".concat(e.selectItemName,"]")).val()])}else e.$selectAll.click();return e.close(),void e.focus()}e.filter()})),this.$selectAll.off("click").on("click",(function(n){e._checkAll(t(n.currentTarget).prop("checked"))})),this.$selectGroups.off("click").on("click",(function(n){var i=t(n.currentTarget),r=i.prop("checked"),u=ir(e.data,"_key",i.data("key"));e._checkGroup(u,r),e.options.onOptgroupClick(rr({label:u.label,selected:u.selected,data:u._data,children:u.children.map((function(t){return rr({text:t.text,value:t.value,selected:t.selected,disabled:t.disabled,data:t._data})}))}))})),this.$selectItems.off("click").on("click",(function(n){var i=t(n.currentTarget),r=i.prop("checked"),u=ir(e.data,"_key",i.data("key"));e._check(u,r),e.options.onClick(rr({text:u.text,value:u.value,selected:u.selected,data:u._data})),e.options.single&&e.options.isOpen&&!e.options.keepOpen&&e.close()}))}},{key:"initView",value:function(){var t;window.getComputedStyle?"auto"===(t=window.getComputedStyle(this.$el[0]).width)&&(t=this.$drop.outerWidth()+20):t=this.$el.outerWidth()+20,this.$parent.css("width",this.options.width||t),this.$el.show().addClass("ms-offscreen")}},{key:"open",value:function(){if(!this.$choice.hasClass("disabled")){if(this.options.isOpen=!0,this.$choice.find(">div").addClass("open"),this.$drop[this.animateMethod("show")](),this.$selectAll.parent().show(),this.$noResults.hide(),this.data.length||(this.$selectAll.parent().hide(),this.$noResults.show()),this.options.container){var e=this.$drop.offset();this.$drop.appendTo(t(this.options.container)),this.$drop.offset({top:e.top,left:e.left}).css("min-width","auto").outerWidth(this.$parent.outerWidth())}var n=this.options.maxHeight;"row"===this.options.maxHeightUnit&&(n=this.$drop.find(">ul>li").first().outerHeight()*this.options.maxHeight),this.$drop.find(">ul").css("max-height","".concat(n,"px")),this.$drop.find(".multiple").css("width","".concat(this.options.multipleWidth,"px")),this.data.length&&this.options.filter&&(this.$searchInput.val(""),this.$searchInput.focus(),this.filter(!0)),this.options.onOpen()}}},{key:"close",value:function(){this.options.isOpen=!1,this.$choice.find(">div").removeClass("open"),this.$drop[this.animateMethod("hide")](),this.options.container&&(this.$parent.append(this.$drop),this.$drop.css({top:"auto",left:"auto"})),this.options.onClose()}},{key:"animateMethod",value:function(t){return{show:{fade:"fadeIn",slide:"slideDown"},hide:{fade:"fadeOut",slide:"slideUp"}}[t][this.options.animate]||t}},{key:"update",value:function(t){var e=this.getSelects(),n=this.getSelects("text");this.options.displayValues&&(n=e);var i=this.$choice.find(">span"),r=e.length,u="";0===r?i.addClass("placeholder").html(this.options.placeholder):u=r<this.options.minimumCountSelected?n.join(this.options.displayDelimiter):this.options.formatAllSelected()&&r===this.dataTotal?this.options.formatAllSelected():this.options.ellipsis&&r>this.options.minimumCountSelected?"".concat(n.slice(0,this.options.minimumCountSelected).join(this.options.displayDelimiter),"..."):this.options.formatCountSelected()&&r>this.options.minimumCountSelected?this.options.formatCountSelected(r,this.dataTotal):n.join(this.options.displayDelimiter),u&&i.removeClass("placeholder").html(u),this.options.displayTitle&&i.prop("title",this.getSelects("text")),this.$el.val(this.getSelects()),t||this.$el.trigger("change")}},{key:"updateSelected",value:function(){for(var t=this.updateDataStart;t<this.updateDataEnd;t++){var e=this.updateData[t];this.$drop.find("input[data-key=".concat(e._key,"]")).prop("checked",e.selected).closest("li").toggleClass("selected",e.selected)}var n=0===this.data.filter((function(t){return t.visible})).length;this.$selectAll.length&&this.$selectAll.prop("checked",this.allSelected).closest("li").toggle(!n),this.$noResults.toggle(n),this.virtualScroll&&(this.virtualScroll.rows=this.getListRows())}},{key:"getOptions",value:function(){var e=t.extend({},this.options);return delete e.data,t.extend(!0,{},e)}},{key:"refreshOptions",value:function(e){(function(t,e,n){var i=Object.keys(t),r=Object.keys(e);if(n&&i.length!==r.length)return!1;for(var u=0,o=i;u<o.length;u++){var s=o[u];if(r.includes(s)&&t[s]!==e[s])return!1}return!0})(this.options,e,!0)||(this.options=t.extend(this.options,e),this.destroy(),this.init())}},{key:"getSelects",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"value",e=[],n=!0,i=!1,r=void 0;try{for(var u,s=this.data[Symbol.iterator]();!(n=(u=s.next()).done);n=!0){var l=u.value;if("optgroup"===l.type){var a=l.children.filter((function(t){return t.selected}));if(!a.length)continue;if("value"===t||this.options.single)e.push.apply(e,o(a.map((function(e){return"value"===t&&e._value||e[t]}))));else{var c=[];c.push("["),c.push(l.label),c.push(": ".concat(a.map((function(e){return e[t]})).join(", "))),c.push("]"),e.push(c.join(""))}}else l.selected&&e.push("value"===t&&l._value||l[t])}}catch(t){i=!0,r=t}finally{try{n||null==s.return||s.return()}finally{if(i)throw r}}return e}},{key:"setSelects",value:function(t,e){var n=!1,i=function(e){var i=!0,r=!1,u=void 0;try{for(var o,s=e[Symbol.iterator]();!(i=(o=s.next()).done);i=!0){var l=o.value,a=t.includes(l._value||l.value);a||l.value!==+l.value+""||(a=t.includes(+l.value)),l.selected!==a&&(n=!0),l.selected=a}}catch(t){r=!0,u=t}finally{try{i||null==s.return||s.return()}finally{if(r)throw u}}},r=!0,u=!1,o=void 0;try{for(var s,l=this.data[Symbol.iterator]();!(r=(s=l.next()).done);r=!0){var a=s.value;"optgroup"===a.type?i(a.children):i([a])}}catch(t){u=!0,o=t}finally{try{r||null==l.return||l.return()}finally{if(u)throw o}}n&&(this.initSelected(e),this.updateSelected(),this.update(e))}},{key:"enable",value:function(){this.$choice.removeClass("disabled")}},{key:"disable",value:function(){this.$choice.addClass("disabled")}},{key:"check",value:function(t){var e=ir(this.data,"value",t);e&&this._check(e,!0)}},{key:"uncheck",value:function(t){var e=ir(this.data,"value",t);e&&this._check(e,!1)}},{key:"_check",value:function(t,e){this.options.single&&this._checkAll(!1,!0),t.selected=e,this.initSelected(),this.updateSelected(),this.update()}},{key:"checkAll",value:function(){this._checkAll(!0)}},{key:"uncheckAll",value:function(){this._checkAll(!1)}},{key:"_checkAll",value:function(t,e){var n=!0,i=!1,r=void 0;try{for(var u,o=this.data[Symbol.iterator]();!(n=(u=o.next()).done);n=!0){var s=u.value;"optgroup"===s.type?this._checkGroup(s,t,!0):s.disabled||!e&&!s.visible||(s.selected=t)}}catch(t){i=!0,r=t}finally{try{n||null==o.return||o.return()}finally{if(i)throw r}}e||(this.initSelected(),this.updateSelected(),this.update())}},{key:"_checkGroup",value:function(t,e,n){t.selected=e,t.children.forEach((function(t){t.disabled||!n&&!t.visible||(t.selected=e)})),n||(this.initSelected(),this.updateSelected(),this.update())}},{key:"checkInvert",value:function(){if(!this.options.single){var t=!0,e=!1,n=void 0;try{for(var i,r=this.data[Symbol.iterator]();!(t=(i=r.next()).done);t=!0){var u=i.value;if("optgroup"===u.type){var o=!0,s=!1,l=void 0;try{for(var a,c=u.children[Symbol.iterator]();!(o=(a=c.next()).done);o=!0){var h=a.value;h.selected=!h.selected}}catch(t){s=!0,l=t}finally{try{o||null==c.return||c.return()}finally{if(s)throw l}}}else u.selected=!u.selected}}catch(t){e=!0,n=t}finally{try{t||null==r.return||r.return()}finally{if(e)throw n}}this.initSelected(),this.updateSelected(),this.update()}}},{key:"focus",value:function(){this.$choice.focus(),this.options.onFocus()}},{key:"blur",value:function(){this.$choice.blur(),this.options.onBlur()}},{key:"refresh",value:function(){this.destroy(),this.init()}},{key:"filter",value:function(e){var n=t.trim(this.$searchInput.val()),i=n.toLowerCase();if(this.filterText!==i){this.filterText=i;var r=!0,u=!1,o=void 0;try{for(var s,l=this.data[Symbol.iterator]();!(r=(s=l.next()).done);r=!0){var a=s.value;if("optgroup"===a.type)if(this.options.filterGroup){var c=this.options.customFilter(nr(a.label.toLowerCase()),nr(i),a.label,n);a.visible=c;var h=!0,f=!1,p=void 0;try{for(var d,v=a.children[Symbol.iterator]();!(h=(d=v.next()).done);h=!0){d.value.visible=c}}catch(t){f=!0,p=t}finally{try{h||null==v.return||v.return()}finally{if(f)throw p}}}else{var g=!0,y=!1,E=void 0;try{for(var b,m=a.children[Symbol.iterator]();!(g=(b=m.next()).done);g=!0){var A=b.value;A.visible=this.options.customFilter(nr(A.text.toLowerCase()),nr(i),A.text,n)}}catch(t){y=!0,E=t}finally{try{g||null==m.return||m.return()}finally{if(y)throw E}}a.visible=a.children.filter((function(t){return t.visible})).length>0}else a.visible=this.options.customFilter(nr(a.text.toLowerCase()),nr(i),a.text,n)}}catch(t){u=!0,o=t}finally{try{r||null==l.return||l.return()}finally{if(u)throw o}}this.initListItems(),this.initSelected(e),this.updateSelected(),e||this.options.onFilter(i)}}},{key:"destroy",value:function(){this.$parent&&(this.$el.before(this.$parent).removeClass("ms-offscreen"),null!==this.tabIndex&&this.$el.attr("tabindex",this.tabIndex),this.$parent.remove(),this.fromHtml&&(delete this.options.data,this.fromHtml=!1))}}]),i}();t.fn.multipleSelect=function(n){for(var i=arguments.length,r=new Array(i>1?i-1:0),u=1;u<i;u++)r[u-1]=arguments[u];var o;return this.each((function(i,u){var s=t(u),l=s.data("multipleSelect"),a=t.extend({},s.data(),"object"===e(n)&&n);if(l||(l=new ur(s,a),s.data("multipleSelect",l)),"string"==typeof n){var c;if(t.inArray(n,pe.METHODS)<0)throw new Error("Unknown method: ".concat(n));o=(c=l)[n].apply(c,r),"destroy"===n&&s.removeData("multipleSelect")}else l.init()})),void 0!==o?o:this},t.fn.multipleSelect.defaults=pe.DEFAULTS,t.fn.multipleSelect.locales=pe.LOCALES,t.fn.multipleSelect.methods=pe.METHODS}));
diff --git a/common/static/opensearch.xml b/common/static/opensearch.xml
new file mode 100644
index 00000000..03ea8902
--- /dev/null
+++ b/common/static/opensearch.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+<ShortName>NeoDB</ShortName>
+<Description>输入关键字或站外条目链接,搜索NeoDB书影音游戏</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image type="image/jpeg" width="64" height="64">https://neodb.social/static/img/logo-square.jpg</Image>
+<Url type="text/html" template="https://neodb.social/search/?q={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/common/static/sass/_AsideSection.sass b/common/static/sass/_AsideSection.sass
index 1ae9ee88..35ff5b0f 100644
--- a/common/static/sass/_AsideSection.sass
+++ b/common/static/sass/_AsideSection.sass
@@ -236,7 +236,7 @@ $panel-padding : 0
             background-color: $color-quaternary
             border-radius: 0
             height: 10px
-            width: 65%
+            width: 54%
 
         progress::-webkit-progress-bar
             background-color: $color-quaternary
diff --git a/common/static/sass/_Label.sass b/common/static/sass/_Label.sass
index cdea7e25..ea0a28b1 100644
--- a/common/static/sass/_Label.sass
+++ b/common/static/sass/_Label.sass
@@ -7,10 +7,18 @@ $spotify-color-primary: #1ed760
 $spotify-color-secondary: black
 $imdb-color-primary: #F5C518
 $imdb-color-secondary: #121212
+$igdb-color-primary: #323A44
+$igdb-color-secondary: #DFE1E2
 $steam-color-primary: #1387b8
 $steam-color-secondary: #111d2e
 $bangumi-color-primary: #F09199
 $bangumi-color-secondary: #FCFCFC
+$goodreads-color-primary: #372213
+$goodreads-color-secondary: #F4F1EA
+$tmdb-color-primary: #91CCA3
+$tmdb-color-secondary: #1FB4E2
+$bandcamp-color-primary: #28A0C1
+$bandcamp-color-secondary: white
 
 .source-label
     display: inline
@@ -50,6 +58,11 @@ $bangumi-color-secondary: #FCFCFC
         color: $imdb-color-secondary
         border: none
         font-weight: bold
+    &.source-label__igdb
+        background-color: $igdb-color-primary
+        color: $igdb-color-secondary
+        border: none
+        font-weight: bold
     &.source-label__steam
         background: linear-gradient(30deg, $steam-color-primary, $steam-color-secondary)
         color: white
@@ -60,4 +73,27 @@ $bangumi-color-secondary: #FCFCFC
         background: $bangumi-color-secondary
         color: $bangumi-color-primary
         font-style: italic
-        font-weight: 600
\ No newline at end of file
+        font-weight: 600
+    &.source-label__goodreads
+        background: $goodreads-color-secondary
+        color: $goodreads-color-primary
+        font-weight: lighter
+    &.source-label__tmdb
+        background: linear-gradient(90deg, $tmdb-color-primary, $tmdb-color-secondary)
+        color: white
+        border: none
+        font-weight: lighter
+        padding-top: 2px
+    &.source-label__googlebooks
+        color: white
+        background-color: #4285F4
+        border-color: #4285F4
+    &.source-label__bandcamp
+        color: $bandcamp-color-secondary
+        background-color: $bandcamp-color-primary
+        // transform: skewX(-30deg)
+        display: inline-block
+    &.source-label__bandcamp span
+        // transform: skewX(30deg)
+        display: inline-block
+        margin: 0 4px
diff --git a/common/static/sass/_Modal.sass b/common/static/sass/_Modal.sass
index df146ca3..dda313d5 100644
--- a/common/static/sass/_Modal.sass
+++ b/common/static/sass/_Modal.sass
@@ -115,10 +115,12 @@
             &__content
                 word-break: break-all
 
+.add-to-list-modal
+    @include modal
 
 // Small devices (landscape phones, 576px and up)
 @media (max-width: $small-devices)
-    .mark-modal, .confirm-modal, .announcement-modal
+    .mark-modal, .confirm-modal, .announcement-modal .add-to-list-modal
         width: 100%
 // Medium devices (tablets, 768px and up)
 @media (max-width: $medium-devices)
diff --git a/common/static/sass/_Vendor.sass b/common/static/sass/_Vendor.sass
index b68099e6..bffa2a2f 100644
--- a/common/static/sass/_Vendor.sass
+++ b/common/static/sass/_Vendor.sass
@@ -50,4 +50,7 @@
 .tippy-content
 
 .tag-input input
-    flex-grow: 1
\ No newline at end of file
+    flex-grow: 1
+
+.tools-section-wrapper input, .tools-section-wrapper select
+    width: unset
diff --git a/common/templates/common/error.html b/common/templates/common/error.html
index e2c1b70c..f4290972 100644
--- a/common/templates/common/error.html
+++ b/common/templates/common/error.html
@@ -5,9 +5,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta http-equiv="refresh" content="3;url={% url 'common:home' %}">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
+    <meta http-equiv="refresh" content="5;url={% if url %}{{url}}{% else %}{% url 'common:home' %}{% endif %}">
+    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
     <title>{% trans '错误' %}</title>
diff --git a/common/templates/common/external_search_result.html b/common/templates/common/external_search_result.html
new file mode 100644
index 00000000..9d2132bb
--- /dev/null
+++ b/common/templates/common/external_search_result.html
@@ -0,0 +1,48 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+{% for item in external_items %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{{ item.link }}">
+            <img src="{{ item.cover_url }}" alt="" class="entity-list__entity-img">
+        </a>
+    </div>
+    <div class="entity-list__entity-text">
+        <div class="entity-list__entity-title" style="font-style:italic;">
+            <a href="{{ item.link }}" class="entity-list__entity-link">
+                {% if request.GET.q %}
+                    {{ item.title | highlight:request.GET.q }}
+                {% else %}
+                    {{ item.title }}
+                {% endif %}
+            </a>
+            
+            {% if not request.GET.c or not request.GET.c in categories %}
+            <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ item.source_url }}">
+                <span class="source-label source-label__{{ item.source_site }}">{{ item.source_site.label }}</span>
+            </a>
+        </div>
+
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+            {{item.subtitle}}
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ item.brief }}
+        </p>
+        <div class="tag-collection">
+        </div>
+    </div>
+
+</li>
+{% endfor %}
\ No newline at end of file
diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html
index cc420eed..0144093d 100644
--- a/common/templates/common/search_result.html
+++ b/common/templates/common/search_result.html
@@ -14,12 +14,14 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 搜索结果' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '搜索结果' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -43,399 +45,39 @@
                                     
                                 <ul class="entity-list__entities">
                                     {% for item in items %}
-                                    
-                                    {% if item.category_name|lower == 'book' %}
-                                    
-                                        {% with book=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                <a href="{% url 'books:retrieve' book.id %}">
-                                                    <img src="{{ book.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                </a>
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                        
-                                                <div class="entity-list__entity-title">
-                                        
-                                                    <a href="{% url 'books:retrieve' book.id %}" class="entity-list__entity-link">
-                                                        {% if request.GET.q %}
-                                                        {{ book.title | highlight:request.GET.q }}
-                                                        {% else %}
-                                                        {{ book.title }}
-                                                        {% endif %}
-                                                        
-                                                    </a>
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ book.source_url }}">
-                                                        <span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-                                        
-                                                {% if book.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ book.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-                                        
-                                                <span class="entity-list__entity-info">
-                                                    {% if book.pub_year %}
-                                                    {{ book.pub_year }}{% trans '年' %}
-                                                    {% if book.pub_month %}
-                                                    {{book.pub_month }}{% trans '月' %} /
-                                                    {% endif %}
-                                                    {% endif %}
-                                        
-                                                    {% if book.author %}
-                                                    {% trans '作者' %}
-                                                    {% for author in book.author %}
-                                                    {{ author }}{% if not forloop.last %},{% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                    {% if book.translator %}
-                                                    {% trans '译者' %}
-                                                    {% for translator in book.translator %}
-                                                    {{ translator }}{% if not forloop.last %},{% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                    {% if book.orig_title %}
-                                                    &nbsp;{% trans '原名' %}
-                                                    {{ book.orig_title }}
-                                                    {% endif %}
-                                                </span>
-                                                <p class="entity-list__entity-brief">
-                                                    {{ book.brief }}
-                                                </p>
-                                        
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in book.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        </li>
-                                        {% endwith %}   
-
-                                    {% elif item.category_name|lower == 'movie' %}    
-                                        
-                                        {% with movie=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                <a href="{% url 'movies:retrieve' movie.id %}">
-                                                    <img src="{{ movie.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                </a>
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                                <div class="entity-list__entity-title">
-                                                    <a href="{% url 'movies:retrieve' movie.id %}" class="entity-list__entity-link">
-                                                        {% if movie.season %}
-                                                        
-                                                            {% if request.GET.q %}
-                                                                {{ movie.title | highlight:request.GET.q }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
-                                                                {{ movie.orig_title | highlight:request.GET.q }} Season {{ movie.season }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% else %}
-                                                                {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
-                                                                {{ movie.orig_title }} Season {{ movie.season }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% endif %}
-                                        
-                                                        {% else %}
-                                                            {% if request.GET.q %}
-                                                                {{ movie.title | highlight:request.GET.q }} {{ movie.orig_title | highlight:request.GET.q }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% else %}
-                                                                {{ movie.title }} {{ movie.orig_title }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% endif %}
-                                                        {% endif %}
-                                                    </a>
-                                                    
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ movie.source_url }}">
-                                                        <span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-
-                                                {% if movie.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ movie.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-
-                                                <span class="entity-list__entity-info ">
-                                        
-                                        
-                                                    {% if movie.director %}{% trans '导演' %}
-                                                    {% for director in movie.director %}
-                                                    {{ director }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                    {% if movie.genre %}{% trans '类型' %}
-                                                    {% for genre in movie.get_genre_display %}
-                                                    {{ genre }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                </span>
-                                                <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                                    {% if movie.actor %}{% trans '主演' %}
-                                                    {% for actor in movie.actor %}
-                                                    <span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
-                                                    {% if forloop.counter <= 5 %}
-                                                        {% if not forloop.counter == 5 and not forloop.last %} {% endif %}
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endif %}
-                                                </span>
-                                                <p class="entity-list__entity-brief">
-                                                    {{ movie.brief }}
-                                                </p>
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in movie.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        
-                                        </li>
-                                        {% endwith %}
-
-                                    {% elif item.category_name|lower == 'game' %}    
-                                        
-                                        {% with game=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                <a href="{% url 'games:retrieve' game.id %}">
-                                                    <img src="{{ game.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                </a>
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                                <div class="entity-list__entity-title">
-                                                    <a href="{% url 'games:retrieve' game.id %}" class="entity-list__entity-link">
-                                                        {% if request.GET.q %}
-                                                            {{ game.title | highlight:request.GET.q }}
-                                                        {% else %}
-                                                            {{ game.title }}
-                                                        {% endif %}
-                                                    </a>
-                                                    
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ game.source_url }}">
-                                                        <span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-
-                                                {% if game.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ game.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-
-                                                <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                        
-                                                    {% if game.other_title %}{% trans '别名' %}
-                                                    {% for other_title in game.other_title %}
-                                                    {{ other_title }}{% 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.genre %}{% trans '类型' %}
-                                                    {% for genre in game.genre %}
-                                                    {{ genre }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-
-                                                    {% if game.platform %}{% trans '平台' %}
-                                                    {% for platform in game.platform %}
-                                                    {{ platform }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-
-                                                </span>
-                                                <p class="entity-list__entity-brief">
-                                                    {{ game.brief }}
-                                                </p>
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in game.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        
-                                        </li>
-                                        {% endwith %}
-
-                                    {% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %}    
-                                        
-                                        {% with music=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                
-                                                {% if item.category_name|lower == 'album' %}
-                                                    <a href="{% url 'music:retrieve_album' music.id %}">
-                                                        <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                    </a>
-                                                {% elif item.category_name|lower == 'song' %}
-                                                    <a href="{% url 'music:retrieve_song' music.id %}">
-                                                        <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                    </a>
-                                                {% endif %}
-                                                    
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                                <div class="entity-list__entity-title">
-                                                    
-                                                    {% if item.category_name|lower == 'album' %}
-                                                        <a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">                                                        
-                                                            {% if request.GET.q %}
-                                                            {{ music.title | highlight:request.GET.q }}
-                                                            {% else %}
-                                                            {{ music.title }}
-                                                            {% endif %}
-                                                        </a>
-                                                    {% elif item.category_name|lower == 'song' %}
-                                                        <a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">                                                        
-                                                            {% if request.GET.q %}
-                                                            {{ music.title | highlight:request.GET.q }}
-                                                            {% else %}
-                                                            {{ music.title }}
-                                                            {% endif %}
-                                                        </a>
-                                                    {% endif %}
-                                                        
-                                                    
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ music.source_url }}">
-                                                        <span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-
-                                                {% if music.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ music.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ music.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-
-                                                <span class="entity-list__entity-info ">
-                                                    {% if music.artist %}{% trans '艺术家' %}
-                                                    {% for artist in music.artist %}
-                                                    <span>{{ artist }}</span>
-                                                    {% if not forloop.last %} {% endif %}
-                                                    {% endfor %}
-                                                    {% endif %}
-                                                    
-                                                    {% if music.genre %}/ {% trans '流派' %}
-                                                    {{ music.genre }}
-                                                    {% endif %}
-
-                                                    {% if music.release_date %}/ {% trans '发行日期' %}
-                                                        {{ music.release_date }}
-                                                    {% endif %}
-                                                </span>
-                                                <span class="entity-list__entity-info entity-list__entity-info--full-length">
-
-                                                </span>
-                                                
-                                                {% if music.brief %}
-                                                <p class="entity-list__entity-brief">
-                                                    {{ music.brief }}
-                                                </p>
-                                                {% elif music.category_name|lower == 'album' %}
-                                                <p class="entity-list__entity-brief">
-                                                    {% trans '曲目:' %}{{ music.track_list }}
-                                                </p>
-                                                {% else %}
-                                                <!-- song -->
-                                                <p class="entity-list__entity-brief">
-                                                    {% trans '所属专辑:' %}{{ music.album }}
-                                                </p>
-                                                {% endif %}
-                                                    
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in music.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        
-                                        </li>
-                                        {% endwith %}
-                                                                                    
-                                    {% endif %}
-                                        
-                                        
-
+                                    {% include "partial/list_item.html" %}
                                     {% empty %}
-                                    {% trans '无结果' %}
+                                    <li class="entity-list__entity">
+                                    {% trans '无站内条目匹配' %}
+                                    </li>
                                     {% endfor %}
-                                        
+                                    {% if request.GET.q and user.is_authenticated %}
+                                    <li class="entity-list__entity" hx-get="{% url 'common:external_search' %}?q={{ request.GET.q }}&c={{ request.GET.c }}&page={% if pagination.current_page %}{{ pagination.current_page }}{% else %}1{% endif %}" hx-trigger="load" hx-swap="outerHTML">
+                                    {% trans '正在实时搜索站外条目' %}
+                                    </li>
+                                    {% endif %}
                                 </ul>
                             </div>
                             <div class="pagination" >
                                 
-                                {% if items.pagination.has_prev %}
-                                    <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                    <a href="?page={{ items.previous_page_number }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                {% if pagination.has_prev %}
+                                    <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                    <a href="?page={{ pagination.previous_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
                                 {% endif %}
                                     
-                                {% for page in items.pagination.page_range %}
+                                {% for page in pagination.page_range %}
                                     
-                                    {% if page == items.pagination.current_page %}
-                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                    {% if page == pagination.current_page %}
+                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
                                     {% else %}
-                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__page-link">{{ page }}</a>
+                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__page-link">{{ page }}</a>
                                     {% endif %}
                                         
                                 {% endfor %}
                                     
-                                {% if items.pagination.has_next %}
-                                    <a href="?page={{ items.next_page_number }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                    <a href="?page={{ items.pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link">&raquo;</a>
+                                {% if pagination.has_next %}
+                                    <a href="?page={{ pagination.next_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                    <a href="?page={{ pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link">&raquo;</a>
                                 {% endif %}           
                               
                             </div>            
@@ -500,7 +142,7 @@
                                         </a>
                                     {% endif %}
                                 </div>
-                                <div class="add-entity-entries__entry">
+                                <!-- div class="add-entity-entries__entry">
                                     {% if request.GET.c and request.GET.c in categories %}
                                     
                                         {% if request.GET.c|lower == 'book' %}
@@ -560,7 +202,7 @@
                                     </a>
 
                                     {% endif %}
-                                </div>
+                                </div -->
 
                             </div>
 
@@ -573,15 +215,11 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
     </script>
 </body>
 
diff --git a/common/templates/partial/_announcement.html b/common/templates/partial/_announcement.html
new file mode 100644
index 00000000..166d2856
--- /dev/null
+++ b/common/templates/partial/_announcement.html
@@ -0,0 +1,61 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<div id="modals">
+    <style>
+        .bottom-link {
+            margin-top: 30px; text-align: center; margin-bottom: 5px;
+        }
+        .bottom-link a {
+            color: #ccc;
+        }
+    </style>
+    <div class="announcement-modal modal">
+        <div class="announcement-modal__head">
+            <h4 class="announcement-modal__title">{% trans '公告' %}</h4>
+    
+            <span class="announcement-modal__close-button modal-close">
+                <span class="icon-cross">
+                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                        <polygon
+                            points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
+                        </polygon>
+                    </svg>
+                </span>
+            </span>
+        </div>
+        <div class="announcement-modal__body">
+            <ul>
+                {% for ann in unread_announcements %}
+                    <li class="announcement">
+                        <a href="{% url 'management:retrieve' ann.pk %}">
+                            <h5 class="announcement__title">{{ ann.title }}</h5>
+                        </a>
+                        <span class="announcement__datetime">{{ ann.created_time }}</span>
+                        <p class="announcement__content">{{ ann.get_plain_content | truncate:200 }}</p>
+                    </li>
+                    {% if not forloop.last %}
+                        <div class="dividing-line" style="border-top-style: dashed;"></div>
+                    {% endif %}
+                {% endfor %}
+            </ul>
+            <div class="bottom-link">
+                <a href="{% url 'management:list' %}">{% trans '查看全部公告' %}</a>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="bg-mask"></div>
+<script>
+    // because the modal and mask elements only exist when there are new announcements
+    $(".announcement-modal").show();
+    $(".bg-mask").show();
+    $(".modal-close").on('click', function () {
+        $(this).parents(".modal").hide();
+        $(".bg-mask").hide();
+    });
+</script>
diff --git a/common/templates/partial/_common_libs.html b/common/templates/partial/_common_libs.html
new file mode 100644
index 00000000..9460e2da
--- /dev/null
+++ b/common/templates/partial/_common_libs.html
@@ -0,0 +1,23 @@
+{% load static %}
+{% if sentry_dsn %}
+<script src="https://static.neodb.social/browser.sentry-cdn.com/7.7.0/bundle.min.js"></script>
+<script>
+    if (window.Sentry) Sentry.init({
+      dsn: "{{ sentry_dsn }}",
+      release: "NeoDB@{{ version_hash }}",
+      environment: "{{ settings_module }}",
+      tracesSampleRate: 1.0,
+    });
+</script>
+{% endif %}
+{% if jquery %}
+<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+{% else %}
+<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/cash/8.1.1/cash.min.js"></script>
+{% endif %}
+<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+<script src="https://static.neodb.social/unpkg.com/hyperscript.org@0.9.7.js"></script>
+<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
+<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+<link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+<link rel="search"type="application/opensearchdescription+xml" title="{{ site_name }}" href="{% static 'opensearch.xml' %}">
diff --git a/common/templates/partial/_footer.html b/common/templates/partial/_footer.html
index e5710473..412f00ad 100644
--- a/common/templates/partial/_footer.html
+++ b/common/templates/partial/_footer.html
@@ -1,13 +1,12 @@
 <footer class="footer">
     <div class="grid">
         <div class="footer__border">
-            <a class="footer__link" target="_blank" href="https://donotban.com/@whitiewhite">作者长毛象</a>
-            <a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic/issues">报告错误</a>
+            <a class="footer__link" target="_blank" href="https://donotban.com/@whitiewhite">原作者</a>
+            <a class="footer__link" target="_blank" href="{{ support_link }}">报告错误</a>
             <a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic" id="githubLink">Github</a>
-            <a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助项目</a>
+            <a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助上游项目</a>
             <a class="footer__link" target="_blank" href="/announcement/supported-sites/" id="supported-sites">支持的网站</a>
             <a class="footer__link" target="_blank" href="/announcement/" id="supported-sites">公告栏</a>
-            <a class="footer__link" href="javascript:void();" id="version">V0.4.4</a>
         </div>
     </div>
 </footer>
\ No newline at end of file
diff --git a/common/templates/partial/_navbar.html b/common/templates/partial/_navbar.html
index 171eda8d..90d7a459 100644
--- a/common/templates/partial/_navbar.html
+++ b/common/templates/partial/_navbar.html
@@ -1,24 +1,24 @@
 {% load static %}
 {% load i18n %}
 {% load admin_url %}
+<form method="get" action="{% url 'common:search' %}">
 <section id="navbar">
     <nav class="navbar">
         <div class="grid">
             <div class="navbar__wrapper">
-
                 <a href="{% url 'common:home' %}" class="navbar__logo">
                     <img src="{% static 'img/logo.svg' %}" alt="" class="navbar__logo-img">
                 </a>
                 <div class="navbar__search-box">
                     <!-- <input type="search" class="" name="q" id="searchInput" required="true" value="{% for v in request.GET.values %}{{ v }}{% endfor %}" -->
                     <input type="search" class="" name="q" id="searchInput" required="true" value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}"
-                        placeholder="搜索书影音">
-                    <select class="navbar__search-dropdown" id="searchCategory">
-                        <option value="all" {% if request.GET.c and request.GET.c != 'movie' and request.GET.c != 'book' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
-                        <option value="book" {% if request.GET.c and request.GET.c == 'book' %}selected{% endif %}>{% trans '书籍' %}</option>
-                        <option value="movie" {% if request.GET.c and request.GET.c == 'movie' %}selected{% endif %}>{% trans '电影' %}</option>
-                        <option value="music" {% if request.GET.c and request.GET.c == 'music' %}selected{% endif %}>{% trans '音乐' %}</option>
-                        <option value="game" {% if request.GET.c and request.GET.c == 'game' %}selected{% endif %}>{% trans '游戏' %}</option>
+                        placeholder="搜索书影音游戏,或输入站外条目链接如 https://movie.douban.com/subject/1297880/ 支持站点列表见页底公告栏">
+                    <select class="navbar__search-dropdown" id="searchCategory" name="c">
+                        <option value="all" {% if request.GET.c and request.GET.c == 'all' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
+                        <option value="book" {% if request.GET.c and request.GET.c == 'book' or '/books/' in request.path %}selected{% endif %}>{% trans '书籍' %}</option>
+                        <option value="movie" {% if request.GET.c and request.GET.c == 'movie' or '/movies/' in request.path  %}selected{% endif %}>{% trans '电影' %}</option>
+                        <option value="music" {% if request.GET.c and request.GET.c == 'music' or '/music/' in request.path  %}selected{% endif %}>{% trans '音乐' %}</option>
+                        <option value="game" {% if request.GET.c and request.GET.c == 'game' or '/games/' in request.path  %}selected{% endif %}>{% trans '游戏' %}</option>
                     </select>
                 </div>
                 <button class="navbar__dropdown-btn">• • •</button>
@@ -26,8 +26,11 @@
                     
                     {% if request.user.is_authenticated %}
          
+                        <a class="navbar__link" href="{% url 'users:home' request.user.mastodon_username %}">{% trans '主页' %}</a>
+                        <a class="navbar__link" href="{% url 'timeline:timeline' %}">{% trans '动态' %}</a>
+                        <a class="navbar__link" id="logoutLink" href="{% url 'users:data' %}">{% trans '数据' %}</a>
+                        <a class="navbar__link" id="logoutLink" href="{% url 'users:preferences' %}">{% trans '设置' %}</a>
                         <a class="navbar__link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
-                        <a class="navbar__link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
                         {% if request.user.is_staff %}
                         <a class="navbar__link" href="{% admin_url %}">{% trans '后台' %}</a>
                         {% endif %}
@@ -36,23 +39,9 @@
                         <a class="navbar__link" href="{% url 'users:login' %}?next={{ request.path }}">{% trans '登录' %}</a>
                     {% endif %}
                 </ul>
-
             </div>
 
         </div>
     </nav>
-    <script>
-            $("#searchInput").on('keyup', function (e) {
-                // e.preventDefault();
-                if (e.keyCode === 13) {
-                    let q = $(this).val();
-                    let c = $("#searchCategory").val();
-                    if (q) {
-                        let new_location = "{% url 'common:search' %}" + "?c=" + c + "&q=" + q;
-                        setTimeout(function () { document.location.href = new_location; }, 150);
-                    }
-                }
-            });
-   
-    </script>
-</section>
\ No newline at end of file
+</section>
+</form>
diff --git a/common/templates/partial/_sidebar.html b/common/templates/partial/_sidebar.html
new file mode 100644
index 00000000..90bc2c0d
--- /dev/null
+++ b/common/templates/partial/_sidebar.html
@@ -0,0 +1,186 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+{% load neo %}
+<div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
+    <div class="aside-section-wrapper aside-section-wrapper--no-margin">
+        <div class="user-profile" id="userInfoCard">
+            <div class="user-profile__header">
+                <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
+                <img src="{{ user.mastodon_account.avatar }}" class="user-profile__avatar mast-avatar">
+                <a href="{% url 'users:home' user.mastodon_username %}">
+                    <h5 class="user-profile__username mast-displayname">{{ user.mastodon_account.display_name }}</h5>
+                </a>
+            </div>
+            <p><a class="user-profile__link mast-acct" target="_blank" href="{{ user.mastodon_account.url }}">@{{ user.username }}@{{ user.mastodon_site }}</a>
+                {% current_user_relationship user as relationship %}
+                {% if relationship %}
+                <a class="user-profile__report-link">
+                    {{ relationship }}
+                </a>
+                {% endif %}
+            </p>
+            <p class="user-profile__bio mast-brief">{{ user.mastodon_account.note }}</p>
+
+            {% if request.user != user %}
+            <a href="{% url 'users:report' %}?user_id={{ user.id }}"
+                class="user-profile__report-link">{% trans '投诉用户' %}</a>
+            {% endif %}
+
+        </div>
+    </div>
+
+    <div class="relation-dropdown">
+        <div class="relation-dropdown__button">
+            <span class="icon-arrow">
+                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
+                    <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
+                </svg>
+            </span>
+        </div>
+        {% if user == request.user %}
+        <div class="relation-dropdown__body">
+            <div
+                class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
+
+                <div class="user-relation" id="followings">
+                    <h5 class="user-relation__label">
+                        {% trans '关注的人' %}
+                    </h5>
+                    <a href="{% url 'users:following' user.mastodon_username %}"
+                        class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
+                    <ul class="user-relation__related-user-list mast-following">
+                        <li class="user-relation__related-user">
+                            <a>
+                                <img src="" alt="" class="user-relation__related-user-avatar">
+                                <div class="user-relation__related-user-name mast-displayname">
+                                </div>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="user-relation" id="followers">
+                    <h5 class="user-relation__label">
+                        {% trans '被他们关注' %}
+                    </h5>
+                    <a href="{% url 'users:followers' user.mastodon_username %}"
+                        class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
+                    <ul class="user-relation__related-user-list mast-followers">
+                        <li class="user-relation__related-user">
+                            <a>
+                                <img src="" alt="" class="user-relation__related-user-avatar">
+                                <div class="user-relation__related-user-name mast-displayname">
+                                </div>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="user-relation">
+                    <h5 class="user-relation__label">
+                        {% trans '常用标签' %}
+                    </h5>
+                    <a href="{% url 'users:tag_list' user.mastodon_username %}">{% trans '更多' %}</a>
+                    <div class="tag-collection" style="margin-left: 0;">
+                        {% if book_tags %}
+                        <div>{% trans '书籍' %}</div>
+                        {% for v in book_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:book_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+
+                        {% if movie_tags %}
+                        <div>{% trans '电影和剧集' %}</div>
+                        {% for v in movie_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:movie_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+
+                        {% if music_tags %}
+                        <div>{% trans '音乐' %}</div>
+                        {% for v in music_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:music_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+
+                        {% if game_tags %}
+                        <div>{% trans '游戏' %}</div>
+                        {% for v in game_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:game_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+                    </div>
+                </div>
+
+            </div>
+           
+            <div
+                class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
+                {% if request.user.is_staff and request.user == user%}
+                <div class="report-panel">
+                    <h5 class="report-panel__label">{% trans '投诉信息' %}</h5>
+                    <a class="report-panel__all-link"
+                        href="{% url 'users:manage_report' %}">全部投诉</a>
+                    <div class="report-panel__body">
+                        <ul class="report-panel__report-list">
+                            {% for report in reports %}
+                            <li class="report-panel__report">
+                                <a href="{% url 'users:home' report.submit_user.mastodon_username %}"
+                                    class="report-panel__user-link">{{ report.submit_user }}</a>{% trans '已投诉' %}<a
+                                    href="{% url 'users:home' report.reported_user.mastodon_username %}"
+                                    class="report-panel__user-link">{{ report.reported_user }}</a>
+                            </li>
+                            {% empty %}
+                            <div>{% trans '暂无新投诉' %}</div>
+                            {% endfor %}
+
+                        </ul>
+                    </div>
+                </div>
+                {% endif %}
+            </div>
+        </div>
+        {% endif %}
+    </div>
+</div>
+
+{% if user == request.user %}
+<div id="oauth2Token" hidden="true">{{ request.user.mastodon_token }}</div>
+<div id="mastodonURI" hidden="true">{{ request.user.mastodon_site }}</div>
+<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
+<div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
+
+<div id="spinner" hidden>
+    <div class="spinner">
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+    </div>
+</div>
+{% endif %}
diff --git a/common/templates/partial/list_item.html b/common/templates/partial/list_item.html
new file mode 100644
index 00000000..9b4eb9ad
--- /dev/null
+++ b/common/templates/partial/list_item.html
@@ -0,0 +1,9 @@
+{% if item.category_name|lower == 'book' %}
+{% include "partial/list_item_book.html" with book=item %}
+{% elif item.category_name|lower == 'movie' %}    
+{% include "partial/list_item_movie.html" with movie=item %}
+{% elif item.category_name|lower == 'game' %}    
+{% include "partial/list_item_game.html" with game=item %}
+{% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %}    
+{% include "partial/list_item_music.html" with music=item %}
+{% endif %}
\ No newline at end of file
diff --git a/common/templates/partial/list_item_book.html b/common/templates/partial/list_item_book.html
new file mode 100644
index 00000000..3c5dc9eb
--- /dev/null
+++ b/common/templates/partial/list_item_book.html
@@ -0,0 +1,159 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load neo %}
+{% current_user_marked_item book as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{% url 'books:retrieve' book.id %}">
+            <img src="{{ book.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{% url 'books:wish' book.id %}" title="加入想读">➕</a>
+        {% endif %}
+    </div>
+
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+
+            <a href="{% url 'books:retrieve' book.id %}" class="entity-list__entity-link">
+                {% if request.GET.q %}
+                {{ book.title | highlight:request.GET.q }}
+                {% else %}
+                {{ book.title }}
+                {% endif %}
+
+            </a>
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{book.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ book.source_url }}">
+                <span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if book.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ book.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info">
+            {% if book.pub_year %} /
+            {{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{book.pub_month }}{% trans '月' %}{% endif %}
+            {% endif %}
+
+            {% if book.author %} /
+            {% for author in book.author %}
+            {% if request.GET.q %}
+            {{ author | highlight:request.GET.q }}
+            {% else %}
+            {{ author }}
+            {% endif %}
+            {% if not forloop.last %},{% endif %}
+            {% endfor %}
+            {% endif %}
+
+            {% if book.translator %} /
+            {% trans '翻译' %}:
+            {% for translator in book.translator %}
+            {% if request.GET.q %}
+            {{ translator | highlight:request.GET.q }}
+            {% else %}
+            {{ translator }}
+            {% endif %}
+            {% if not forloop.last %},{% endif %}
+            {% endfor %}
+            {% endif %}
+
+            {% if book.subtitle %} /
+            {% trans '副标题' %}:
+            {% if request.GET.q %}
+            {{ book.subtitle | highlight:request.GET.q }}
+            {% else %}
+            {{ book.subtitle }}
+            {% endif %}
+            {% endif %}
+
+            {% if book.orig_title %} /
+            {% trans '原名' %}:
+            {% if request.GET.q %}
+            {{ book.orig_title | highlight:request.GET.q }}
+            {% else %}
+            {{ book.orig_title }}
+            {% endif %}
+            {% endif %}
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ book.brief }}
+        </p>
+
+        <div class="tag-collection">
+            {% for tag_dict in book.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: <a href="{% url 'books:retrieve_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/list_item_game.html b/common/templates/partial/list_item_game.html
new file mode 100644
index 00000000..42346910
--- /dev/null
+++ b/common/templates/partial/list_item_game.html
@@ -0,0 +1,139 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load neo %}
+{% current_user_marked_item game as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{% url 'games:retrieve' game.id %}">
+            <img src="{{ game.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{% url 'games:wish' game.id %}" title="加入想玩">➕</a>
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+            <a href="{% url 'games:retrieve' game.id %}" class="entity-list__entity-link">
+                {% if request.GET.q %}
+                    {{ game.title | highlight:request.GET.q }}
+                {% else %}
+                    {{ game.title }}
+                {% endif %}
+            </a>
+            
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ game.source_url }}">
+                <span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if game.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ game.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+
+            {% if game.other_title %}{% trans '别名' %}:
+            {% for other_title in game.other_title %}
+            {{ other_title }}{% 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.genre %}{% trans '类型' %}:
+            {% for genre in game.genre %}
+            {{ genre }}{% if not forloop.last %} {% endif %}
+            {% endfor %}/
+            {% endif %}
+
+            {% if game.platform %}{% trans '平台' %}:
+            {% for platform in game.platform %}
+            {{ platform }}{% if not forloop.last %} {% endif %}
+            {% endfor %}
+            {% endif %}
+
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ game.brief }}
+        </p>
+
+        <div class="tag-collection">
+            {% for tag_dict in game.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: <a href="{% url 'games:retrieve_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/list_item_movie.html b/common/templates/partial/list_item_movie.html
new file mode 100644
index 00000000..f20f40f5
--- /dev/null
+++ b/common/templates/partial/list_item_movie.html
@@ -0,0 +1,164 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load neo %}
+{% current_user_marked_item movie as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{% url 'movies:retrieve' movie.id %}">
+            <img src="{{ movie.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{% url 'movies:wish' movie.id %}" title="加入想看">➕</a>
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+            <a href="{% url 'movies:retrieve' movie.id %}" class="entity-list__entity-link">
+                {% if movie.season %}
+                
+                    {% if request.GET.q %}
+                        {{ movie.title | highlight:request.GET.q }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
+                        {{ movie.orig_title | highlight:request.GET.q }} Season {{ movie.season }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% else %}
+                        {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
+                        {{ movie.orig_title }} Season {{ movie.season }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% endif %}
+
+                {% else %}
+                    {% if request.GET.q %}
+                        {{ movie.title | highlight:request.GET.q }} {{ movie.orig_title | highlight:request.GET.q }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% else %}
+                        {{ movie.title }} {{ movie.orig_title }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% endif %}
+                {% endif %}
+            </a>
+            
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{movie.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ movie.source_url }}">
+                <span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if movie.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ movie.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info ">
+
+            {% if movie.director %}{% trans '导演' %}:
+            {% for director in movie.director %}
+            {% if request.GET.q %}
+            {{ director | highlight:request.GET.q }}
+            {% else %}
+            {{ director }}
+            {% endif %}
+            {% if not forloop.last %},{% endif %}
+            {% endfor %}/
+            {% endif %}
+
+            {% if movie.genre %}{% trans '类型' %}:
+            {% for genre in movie.get_genre_display %}
+            {{ genre }}{% if not forloop.last %} {% endif %}
+            {% endfor %}/
+            {% endif %}
+
+        </span>
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+            {% if movie.actor %}{% trans '主演' %}:
+            {% for actor in movie.actor %}
+            <span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
+            {% if request.GET.q %}
+            {{ actor | highlight:request.GET.q }}
+            {% else %}
+            {{ actor }}
+            {% endif %}
+            </span>
+            {% if forloop.counter <= 5 %}
+                {% if not forloop.counter == 5 and not forloop.last %} {% endif %}
+            {% endif %}
+            {% endfor %}
+            {% endif %}
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ movie.brief }}
+        </p>
+        <div class="tag-collection">
+            {% for tag_dict in movie.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: <a href="{% url 'movies:retrieve_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/list_item_music.html b/common/templates/partial/list_item_music.html
new file mode 100644
index 00000000..bcf0d29a
--- /dev/null
+++ b/common/templates/partial/list_item_music.html
@@ -0,0 +1,171 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load neo %}
+{% current_user_marked_item music as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        
+        {% if music.category_name|lower == 'album' %}
+            <a href="{% url 'music:retrieve_album' music.id %}">
+                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+            </a>
+            {% if not marked %}
+            <a class="entity-list__entity-action-icon" hx-post="{% url 'music:wish_album' music.id %}" title="加入想听">➕</a>
+            {% endif %}
+        {% elif music.category_name|lower == 'song' %}
+            <a href="{% url 'music:retrieve_song' music.id %}">
+                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+            </a>
+            {% if not marked %}
+            <a class="entity-list__entity-action-icon" hx-post="{% url 'music:wish_song' music.id %}" title="加入想听">➕</a>
+            {% endif %}
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+            
+            {% if music.category_name|lower == 'album' %}
+                <a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">                                                        
+                    {% if request.GET.q %}
+                    {{ music.title | highlight:request.GET.q }}
+                    {% else %}
+                    {{ music.title }}
+                    {% endif %}
+                </a>
+            {% elif music.category_name|lower == 'song' %}
+                <a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">                                                        
+                    {% if request.GET.q %}
+                    {{ music.title | highlight:request.GET.q }}
+                    {% else %}
+                    {{ music.title }}
+                    {% endif %}
+                </a>
+            {% endif %}
+                
+            
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{music.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ music.source_url }}">
+                <span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if music.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ music.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ music.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info ">
+            {% if music.artist %}{% trans '艺术家' %}:
+            {% for artist in music.artist %}
+            <span>{{ artist }}</span>
+            {% if not forloop.last %} {% endif %}
+            {% endfor %}
+            {% endif %}
+            
+            {% if music.genre %}/ {% trans '流派' %}:
+            {{ music.genre }}
+            {% endif %}
+
+            {% if music.release_date %}/ {% trans '发行日期' %}:
+                {{ music.release_date }}
+            {% endif %}
+        </span>
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+
+        </span>
+        
+        {% if music.brief %}
+        <p class="entity-list__entity-brief">
+            {{ music.brief }}
+        </p>
+        {% elif music.category_name|lower == 'album' %}
+        <p class="entity-list__entity-brief">
+            {% trans '曲目:' %}{{ music.track_list }}
+        </p>
+        {% else %}
+        <!-- song -->
+        <p class="entity-list__entity-brief">
+            {% trans '所属专辑:' %}{{ music.album }}
+        </p>
+        {% endif %}
+            
+        <div class="tag-collection">
+            {% for tag_dict in music.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: 
+                        {% if music.category_name|lower == 'album' %}
+                        <a href="{% url 'music:retrieve_album_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        <a href="{% url 'music:retrieve_song_review' mark.id %}">{{ mark.title }}</a>
+                        {% endif %}
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+    </div>
+
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/mark_list.html b/common/templates/partial/mark_list.html
new file mode 100644
index 00000000..71c4102a
--- /dev/null
+++ b/common/templates/partial/mark_list.html
@@ -0,0 +1,37 @@
+{% load i18n %}
+
+<ul class="entity-marks__mark-list">
+{% for others_mark in mark_list %}
+<li class="entity-marks__mark">
+    <a href="{% url 'users:home' others_mark.owner.mastodon_username %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
+
+    <span>{{ others_mark.get_status_display }}</span>
+
+    {% if others_mark.rating %}
+    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
+    {% endif %}
+
+    {% if others_mark.visibility > 0 %}
+    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
+    {% endif %}
+
+    {% if others_mark.shared_link %}
+    <a href="{{ others_mark.shared_link }}" target="_blank"><span class="entity-marks__mark-time">{{ others_mark.created_time }}</span></a>
+    {% else %}
+    <span class="entity-marks__mark-time">{{ others_mark.created_time }}</span>
+    {% endif %}
+
+    {% if current_item and others_mark.item != current_item %}
+    <span class="entity-marks__mark-time source-label"><a class="entity-marks__mark-time" href="{% url 'books:retrieve' others_mark.item.id %}">{{ others_mark.item.get_source_site_display }}</a></span>
+    {% endif %}
+
+    {% if others_mark.text %}
+    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
+    {% endif %}
+</li>
+{% empty %}
+
+<div> {% trans '暂无标记' %} </div>
+
+{% endfor %}
+</ul>
\ No newline at end of file
diff --git a/common/templatetags/highlight.py b/common/templatetags/highlight.py
index a2276800..02d79bbb 100644
--- a/common/templatetags/highlight.py
+++ b/common/templatetags/highlight.py
@@ -1,17 +1,19 @@
 from django import template
 from django.utils.safestring import mark_safe
 from django.template.defaultfilters import stringfilter
-from django.utils.html import format_html
+from opencc import OpenCC
 
-import re
 
+cc = OpenCC('t2s')
 register = template.Library()
 
+
 @register.filter
 @stringfilter
 def highlight(text, search):
-    to_be_replaced_words = set(re.findall(search, text, flags=re.IGNORECASE))
-
-    for word in to_be_replaced_words:
-        text = text.replace(word, f'<span class="highlight">{word}</span>')
+    for s in cc.convert(search.strip().lower()).split(' '):
+        if s:
+            p = cc.convert(text.lower()).find(s)
+            if p != -1:
+                text = f'{text[0:p]}<span class="highlight">{text[p:p+len(s)]}</span>{text[p+len(s):]}'
     return mark_safe(text)
diff --git a/common/templatetags/neo.py b/common/templatetags/neo.py
new file mode 100644
index 00000000..b915b7b9
--- /dev/null
+++ b/common/templatetags/neo.py
@@ -0,0 +1,48 @@
+from django import template
+import datetime
+from django.utils import timezone
+from collection.models import Collection
+
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def current_user_marked_item(context, item):
+    user = context['request'].user
+    if user and user.is_authenticated:
+        if isinstance(item, Collection) and item.owner == user:
+            return item
+        else:
+            return context['request'].user.get_mark_for_item(item)
+    return None
+
+
+@register.simple_tag(takes_context=True)
+def current_user_relationship(context, user):
+    current_user = context['request'].user
+    if current_user and current_user.is_authenticated:
+        if current_user.is_following(user):
+            if current_user.is_followed_by(user):
+                return '互相关注'
+            else:
+                return '已关注'
+        elif current_user.is_followed_by(user):
+            return '被ta关注'
+    return None
+
+
+@register.filter
+def prettydate(d):
+    diff = timezone.now() - d
+    s = diff.seconds
+    if diff.days > 14 or diff.days < 0:
+        return d.strftime('%Y年%m月%d日')
+    elif diff.days >= 1:
+        return '{} 天前'.format(diff.days)
+    elif s < 120:
+        return '刚刚'
+    elif s < 3600:
+        return '{} 分钟前'.format(s // 60)
+    else:
+        return '{} 小时前'.format(s // 3600)
diff --git a/common/templatetags/oauth_token.py b/common/templatetags/oauth_token.py
index 7aac83a1..b2f24677 100644
--- a/common/templatetags/oauth_token.py
+++ b/common/templatetags/oauth_token.py
@@ -7,7 +7,7 @@ register = template.Library()
 class OAuthTokenNode(template.Node):
     def render(self, context):
         request = context.get('request')
-        oauth_token = request.session.get('oauth_token', default='')
+        oauth_token = request.user.mastodon_token
         return format_html(oauth_token)
 
 
diff --git a/common/templatetags/thumb.py b/common/templatetags/thumb.py
index 2e21c6ca..aa698abb 100644
--- a/common/templatetags/thumb.py
+++ b/common/templatetags/thumb.py
@@ -12,4 +12,7 @@ def thumb(source, alias):
     if source.url.endswith('.svg'):
         return source.url
     else:
-        return thumbnail_url(source, alias)
\ No newline at end of file
+        try:
+            return thumbnail_url(source, alias)
+        except Exception as e:
+            return ''
diff --git a/common/urls.py b/common/urls.py
index 6de4f66a..b803a47d 100644
--- a/common/urls.py
+++ b/common/urls.py
@@ -6,4 +6,6 @@ urlpatterns = [
     path('', home),
     path('home/', home, name='home'),
     path('search/', search, name='search'),
+    path('search.json/', search, name='search.json'),
+    path('external_search/', external_search, name='external_search'),
 ]
diff --git a/common/utils.py b/common/utils.py
index 5d26a3d9..4d51ca16 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -10,6 +10,8 @@ class PageLinksGenerator:
     def __init__(self, length, current_page, total_pages):
         current_page = int(current_page)
         self.current_page = current_page
+        self.previous_page = current_page - 1 if current_page > 1 else None
+        self.next_page = current_page + 1 if current_page < total_pages else None
         self.start_page = None
         self.end_page = None
         self.page_range = None
diff --git a/common/views.py b/common/views.py
index c028184a..e527e09a 100644
--- a/common/views.py
+++ b/common/views.py
@@ -17,21 +17,93 @@ from music.models import Album, Song, AlbumMark, SongMark
 from users.models import Report, User, Preference
 from mastodon.decorators import mastodon_request_included
 from users.views import home as user_home
+from timeline.views import timeline as user_timeline
 from common.models import MarkStatusEnum
 from common.utils import PageLinksGenerator
-from common.scraper import scraper_registry
+from common.scraper import get_scraper_by_url, get_normalized_url
 from common.config import *
+from common.searcher import ExternalSources
 from management.models import Announcement
+from django.conf import settings
+from common.index import Indexer
+from django.http import JsonResponse
+from django.db.utils import IntegrityError
+
 
 logger = logging.getLogger(__name__)
 
+
 @login_required
 def home(request):
-    return user_home(request, request.user.id)
+    if request.user.get_preference().classic_homepage:
+        return redirect(reverse("users:home", args=[request.user.mastodon_username]))
+    else:
+        return redirect(reverse("timeline:timeline"))
 
 
 @login_required
+def external_search(request):
+    category = request.GET.get("c", default='all').strip().lower()
+    if category == 'all':
+        category = None
+    keywords = request.GET.get("q", default='').strip()
+    page_number = int(request.GET.get('page', default=1))
+    return render(
+        request,
+        "common/external_search_result.html",
+        {
+            "external_items": ExternalSources.search(category, keywords, page_number) if keywords else [],
+        }
+    )
+
+
 def search(request):
+    if settings.SEARCH_BACKEND is None:
+        return search2(request)
+    category = request.GET.get("c", default='all').strip().lower()
+    if category == 'all':
+        category = None
+    keywords = request.GET.get("q", default='').strip()
+    tag = request.GET.get("tag", default='').strip()
+    p = request.GET.get('page', default='1')
+    page_number = int(p) if p.isdigit() else 1
+    if not (keywords or tag):
+        return render(
+            request,
+            "common/search_result.html",
+            {
+                "items": None,
+            }
+        )
+    if request.user.is_authenticated:
+        url_validator = URLValidator()
+        try:
+            url_validator(keywords)
+            # validation success
+            return jump_or_scrape(request, keywords)
+        except ValidationError as e:
+            pass
+    
+    result = Indexer.search(keywords, page=page_number, category=category, tag=tag)
+    for item in result.items:
+        item.tag_list = item.all_tag_list[:TAG_NUMBER_ON_LIST]
+    if request.path.endswith('.json/'):
+        return JsonResponse({
+            'num_pages': result.num_pages,
+            'items':list(map(lambda i:i.get_json(), result.items))
+            })
+    return render(
+        request,
+        "common/search_result.html",
+        {
+            "items": result.items,
+            "pagination": PageLinksGenerator(PAGE_LINK_NUMBER, page_number, result.num_pages),
+            "categories": ['book', 'movie', 'music', 'game'],
+        }
+    )
+
+
+def search2(request):
     if request.method == 'GET':
 
         # test if input serach string is empty or not excluding param ?c=
@@ -109,7 +181,7 @@ def search(request):
             else:
                 ordered_queryset = list(queryset)
             return ordered_queryset
-            
+
         def movie_param_handler(**kwargs):
             # keywords
             keywords = kwargs.get('keywords')
@@ -240,7 +312,7 @@ def search(request):
                         elif music.__class__ == Song:
                             similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
                                 + 1/6 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() \
-                                + 1/6 * SequenceMatcher(None, keyword, music.album.title).quick_ratio()
+                                + 1/6 * (SequenceMatcher(None, keyword, music.album.title).quick_ratio() if music.album is not None else 0)
                         n += 1
                     music.similarity = similarity / n
                 elif tag:
@@ -322,32 +394,50 @@ def jump_or_scrape(request, url):
     if this_site in url:
         return redirect(url)
 
-    # match url to registerd sites
-    matched_host = None
-    for host in scraper_registry:
-        if host in url:
-            matched_host = host
-            break
-
-    if matched_host is None:
+    url = get_normalized_url(url)
+    scraper = get_scraper_by_url(url)
+    if scraper is None:
         # invalid url
-        return render(request, 'common/error.html', {'msg': _("链接非法,查询失败")})
+        return render(request, 'common/error.html', {'msg': _("链接无效,查询失败")})
     else:
-        scraper = scraper_registry[matched_host]
+        try:
+            effective_url = scraper.get_effective_url(url)
+        except ValueError:
+            return render(request, 'common/error.html', {'msg': _("链接无效,查询失败")})
         try:
             # raise ObjectDoesNotExist
-            effective_url = scraper.get_effective_url(url)
             entity = scraper.data_class.objects.get(source_url=effective_url)
             # if exists then jump to detail page
+            if request.path.endswith('.json/'):
+                return JsonResponse({
+                    'num_pages': 1,
+                    'items': [entity.get_json()]
+                    })
             return redirect(entity)
         except ObjectDoesNotExist:
             # scrape if not exists
             try:
                 scraper.scrape(url)
                 form = scraper.save(request_user=request.user)
+            except IntegrityError as ie:  # duplicate key on source_url may be caused by user's double submission
+                try:
+                    entity = scraper.data_class.objects.get(source_url=effective_url)
+                    return redirect(entity)
+                except Exception as e:
+                    logger.error(f"Scrape Failed URL: {url}\n{e}")
+                    if settings.DEBUG:
+                        logger.error("Expections during saving scraped data:", exc_info=e)
+                    return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
             except Exception as e:
-                logger.error(f"Scrape Failed URL: {url}")
-                logger.error("Expections during saving scraped data:", exc_info=e)
+                logger.error(f"Scrape Failed URL: {url}\n{e}")
+                if settings.DEBUG:
+                    logger.error("Expections during saving scraped data:", exc_info=e)
                 return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
             return redirect(form.instance)
 
+
+def go_relogin(request):
+    return render(request, 'common/error.html', {
+        'url': reverse("users:connect") + '?domain=' + request.user.mastodon_site,
+        'msg': _("信息已保存,但是未能分享到联邦网络"),
+        'secondary_msg': _("可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼")})
diff --git a/doc/GUIDE.md b/doc/GUIDE.md
new file mode 100644
index 00000000..055ca2bb
--- /dev/null
+++ b/doc/GUIDE.md
@@ -0,0 +1,106 @@
+NiceDB / NeoDB - Getting Start
+==============================
+This is a very basic guide with limited detail, contributions welcomed
+
+Install
+-------
+Install PostgreSQL, Redis and Python if not yet
+
+Setup database
+```
+CREATE DATABASE neodb ENCODING 'UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8' TEMPLATE template0;
+\c neodb;
+CREATE EXTENSION hstore WITH SCHEMA public;
+CREATE ROLE neodb with LOGIN ENCRYPTED PASSWORD 'abadface';
+GRANT ALL ON DATABASE neodb TO neodb;
+```
+
+Create and edit your own configuration file (optional but very much recommended)
+```
+mkdir mysite && cp boofilsic/settings.py mysite/
+export DJANGO_SETTINGS_MODULE=mysite.settings
+```
+
+Create and use `venv` as you normally would, then install packages 
+```
+python3 -m pip install -r requirements.txt
+```
+
+Quick check
+```
+python3 manage.py check
+```
+
+Initialize database
+```
+python3 manage.py makemigrations users books movies games music sync mastodon management collection
+python3 manage.py migrate users
+python3 manage.py migrate
+```
+
+Build static assets
+```
+python3 manage.py collectstatic
+```
+
+
+Start services
+--------------
+Make sure PostgreSQL and Redis are running
+
+Start job queue server
+```
+export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES  # required and only for macOS, otherwise it may crash
+python3 manage.py rqworker --with-scheduler doufen export mastodon
+```
+
+Run web server in dev mode
+```
+python3 manage.py runserver 0.0.0.0:80
+```
+
+It should be ready to serve from here, to run web server for production, consider `gunicorn -w 8 boofilsic.wsgi` in systemd or sth similar
+
+
+Migrate from an earlier version
+-------------------------------
+Update database
+```
+python3 manage.py makemigrations
+python3 manage.py migrate
+```
+
+Rebuild static assets
+```
+python3 manage.py sass common/static/sass/boofilsic.sass common/static/css/boofilsic.min.css -t compressed
+python3 manage.py sass common/static/sass/boofilsic.sass common/static/css/boofilsic.css
+python3 manage.py collectstatic
+```
+
+Add Cron Jobs
+-------------
+add `python manage.py refresh_mastodon` to crontab to run hourly, it will refresh cached users' follow/mute/block from mastodon
+
+Index and Search
+----------------
+Install TypeSense or Meilisearch, change `SEARCH_BACKEND` and coniguration for search server in `settings.py`
+
+Build initial index, it may take a few minutes or hours
+```
+python3 manage.py init_index
+python3 manage.py reindex
+```
+
+Other maintenance tasks
+-----------------------
+Requeue failed jobs
+```
+rq requeue --all --queue doufen
+```
+
+Run in Docker
+```
+docker-compose build
+docker-compose up db && docker exec -it app_db_1 psql -U postgres postgres -c 'CREATE EXTENSION hstore WITH SCHEMA public;'  # first time only
+docker-compose up
+```
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..86f8c09f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+version: '3'
+
+services:
+  redis:
+    image: redis:alpine
+
+  db:
+    image: postgres:14-alpine
+    volumes:
+      - /tmp/data/db:/var/lib/postgresql/data
+    environment:
+      - POSTGRES_DB=postgres
+      - POSTGRES_USER=postgres
+      - POSTGRES_PASSWORD=postgres
+
+  web:
+    build: .
+    command: python manage.py runserver 0.0.0.0:8000
+    volumes:
+      - .:/code
+    ports:
+      - "8000:8000"
+    environment:
+      - DB_HOST=db
+      - DB_NAME=postgres
+      - DB_USER=postgres
+      - DB_PASSWORD=postgres
+      - REDIS_HOST=redis
+      - DJANGO_SETTINGS_MODULE=neodb.dev
+    depends_on:
+      - db
+      - redis
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100755
index 00000000..5feb0354
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -o errexit  
+set -o pipefail  
+set -o nounset
+
+python manage.py collectstatic --noinput  
+python manage.py makemigrations users books movies games music sync mastodon management collection
+python manage.py makemigrations  
+python manage.py migrate users
+python manage.py migrate
+
+exec "$@"
diff --git a/docker/start.sh b/docker/start.sh
new file mode 100755
index 00000000..a54a76c7
--- /dev/null
+++ b/docker/start.sh
@@ -0,0 +1,36 @@
+#!/bin/bash  
+
+cd /app  
+
+if [ $# -eq 0 ]; then  
+    echo "Usage: start.sh <server|rq>"  
+    exit 1  
+fi  
+
+PROCESS_TYPE=$1  
+
+if [ "$PROCESS_TYPE" = "server" ]; then  
+    if [ "$DJANGO_DEBUG" = "true" ]; then  
+        gunicorn \  
+            --reload \  
+            --bind 0.0.0.0:8000 \  
+            --workers 2 \  
+            --worker-class eventlet \  
+            --log-level DEBUG \  
+            --access-logfile "-" \  
+            --error-logfile "-" \  
+            boofilsic.wsgi 
+    else  
+        gunicorn \  
+            --bind 0.0.0.0:8000 \  
+            --workers 2 \  
+            --worker-class eventlet \  
+            --log-level DEBUG \  
+            --access-logfile "-" \  
+            --error-logfile "-" \  
+            boofilsic.wsgi
+    fi  
+elif [ "$PROCESS_TYPE" = "rq" ]; then
+    rqworker --with-scheduler doufen export mastodon
+fi
+
diff --git a/games/admin.py b/games/admin.py
index fe72bd9a..36cc44fa 100644
--- a/games/admin.py
+++ b/games/admin.py
@@ -1,7 +1,8 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Game)
+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
index b74f62c9..a204c094 100644
--- a/games/apps.py
+++ b/games/apps.py
@@ -3,3 +3,8 @@ 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
index 5aa53330..e758f9fd 100644
--- a/games/forms.py
+++ b/games/forms.py
@@ -1,28 +1,24 @@
 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
+from .models import Game, GameMark, GameReview, GameMarkStatusTranslation
 from common.models import MarkStatusEnum
 from common.forms import *
 
 
 def GameMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在玩"),
-        MarkStatusEnum.WISH.value: _("想玩"),
-        MarkStatusEnum.COLLECT.value: _("玩过")
-    }
-    return trans_dict[status]
+    return GameMarkStatusTranslation[status]
 
 
 class GameForm(forms.ModelForm):
-    # id = forms.IntegerField(required=False, widget=forms.HiddenInput())
+    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',
@@ -66,11 +62,8 @@ class GameMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'game': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -85,14 +78,8 @@ class GameReviewForm(ReviewForm):
             'game',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'book': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'game': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/games/models.py b/games/models.py
index a2614f0a..fba9e639 100644
--- a/games/models.py
+++ b/games/models.py
@@ -1,17 +1,25 @@
 import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
 from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
-from boofilsic.settings import GAME_MEDIA_PATH_ROOT, DEFAULT_GAME_IMAGE
 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, GAME_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.GAME_MEDIA_PATH_ROOT)
 
 
 class Game(Entity):
@@ -53,7 +61,7 @@ class Game(Entity):
     )
 
     genre = postgres.ArrayField(
-        models.CharField(blank=True, default='', max_length=50),
+        models.CharField(blank=True, default='', max_length=200),
         null=True,
         blank=True,
         default=list,
@@ -61,23 +69,39 @@ class Game(Entity):
     )
 
     platform = postgres.ArrayField(
-        models.CharField(blank=True, default='', max_length=50),
+        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=DEFAULT_GAME_IMAGE, blank=True)
-
+    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 wish_url(self):
+        return reverse("games:wish", args=[self.id])
+
     def get_tags_manager(self):
         return self.game_tags
 
@@ -85,6 +109,14 @@ class Game(Entity):
     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(
@@ -96,6 +128,10 @@ class GameMark(Mark):
                 fields=['owner', 'game'], name='unique_game_mark')
         ]
 
+    @property
+    def translated_status(self):
+        return GameMarkStatusTranslation[self.status]
+
 
 class GameReview(Review):
     game = models.ForeignKey(
@@ -107,6 +143,14 @@ class GameReview(Review):
                 fields=['owner', 'game'], name='unique_game_review')
         ]
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + reverse("games:retrieve_review", args=[self.id])
+
+    @property
+    def item(self):
+        return self.game
+
 
 class GameTag(Tag):
     game = models.ForeignKey(
@@ -119,3 +163,7 @@ class GameTag(Tag):
             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
index 40a768e2..178089c4 100644
--- a/games/templates/games/create_update.html
+++ b/games/templates/games/create_update.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -22,9 +22,24 @@
 
             <section id="content" class="container">
                 <div class="grid">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'games:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'games:scrape' %}"
-                            class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                        {% comment %} <a href="{% url 'games:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -55,12 +70,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/games/templates/games/create_update_review.html b/games/templates/games/create_update_review.html
index 54391b6d..cc9f8a2c 100644
--- a/games/templates/games/create_update_review.html
+++ b/games/templates/games/create_update_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -94,7 +94,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -114,12 +114,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/games/templates/games/delete.html b/games/templates/games/delete.html
index 008a561b..a7a2adde 100644
--- a/games/templates/games/delete.html
+++ b/games/templates/games/delete.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除电影/剧集' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if game.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' game.last_editor.id %}">
+                                    <a href="{% url 'users:home' game.last_editor.mastodon_username %}">
                                         <span>{{ game.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/games/templates/games/delete_review.html b/games/templates/games/delete_review.html
index b24fc5d2..f39cdf62 100644
--- a/games/templates/games/delete_review.html
+++ b/games/templates/games/delete_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/games/templates/games/detail.html b/games/templates/games/detail.html
index e32ebf66..fed9eb7a 100644
--- a/games/templates/games/detail.html
+++ b/games/templates/games/detail.html
@@ -14,21 +14,20 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB游戏 - {{ game.title }}">
+    <meta property="og:title" content="{{ site_name }}游戏 - {{ game.title }}">
     <meta property="og:type" content="game">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ game.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ game.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ game.brief }}">
     
-    <title>{% trans 'NiceDB - 游戏详情' %} | {{ game.title }}</title>
+    <title>{{ site_name }} - {% trans '游戏详情' %} | {{ game.title }}</title>
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -54,11 +53,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if game.rating %}
+                                            {% if game.rating and game.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ game.rating }} </span>
+                                            <small>({{ game.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
 
@@ -72,7 +72,7 @@
                                             {% if game.other_title|length > 5 %}
                                             <a href="javascript:void(0);" id="otherTitleMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#otherTitleMore").click(function (e) {
+                                                $("#otherTitleMore").on('click', function (e) {
                                                     $("span.other_title:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -111,7 +111,15 @@
                                             {% trans '发行日期:' %}{{ game.release_date }}
                                             {% endif %}
                                         </div>
-                             
+                                        
+                                        {% if game.other_info %}                                  
+                                        {% for k, v in game.other_info.items %}
+                                        <div>
+                                            {{ k }}:{{ v  | urlize }}
+                                        </div>
+                                        {% endfor %}
+                                        {% endif %}
+                                        
                                     </div>
                                     <div class="entity-detail__fields">
                                         
@@ -123,17 +131,10 @@
                                             {% endif %}
                                         </div>
 
-                                        {% if game.other_info %}                                  
-                                        {% for k, v in game.other_info.items %}
-                                        <div>
-                                            {{ k }}:{{ v  | urlize }}
-                                        </div>
-                                        {% endfor %}
-                                        {% endif %}
-                                        
+
                                     
                                         {% if game.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.id %}">{{ game.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.mastodon_username %}">{{ game.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -178,43 +179,23 @@
                                 
                                 <h5 class="entity-marks__title">{% trans '这个游戏的标记' %}</h5>
                                 {% if mark_list_more %}
-                                <a href="{% url 'games:retrieve_mark_list' game.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'games:retrieve_mark_list' game.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'games:retrieve_mark_list' game.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=game %}    
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这个游戏的评论' %}</h5>
 
                                 {% if review_list_more %}
-                                <a href="{% url 'games:retrieve_review_list' game.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'games:retrieve_review_list' game.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
@@ -242,7 +223,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -254,7 +235,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -286,7 +267,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -315,6 +296,24 @@
 
                             {% endif %}
                         </div>
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'game' game.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
                                 
                     </div>
                 </div>
@@ -377,8 +376,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/games/templates/games/mark_list.html b/games/templates/games/mark_list.html
index 45071f89..6f0acffc 100644
--- a/games/templates/games/mark_list.html
+++ b/games/templates/games/mark_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ game.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ game.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,37 +35,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'games:retrieve' game.id %}">{{ game.title }}</a>{% trans ' 的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-        
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                    </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-                                        
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=game %}
                             </div>
                             <div class="pagination">
                             
@@ -149,12 +119,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
   
diff --git a/games/templates/games/review_detail.html b/games/templates/games/review_detail.html
index 1ffab003..6b0c357f 100644
--- a/games/templates/games/review_detail.html
+++ b/games/templates/games/review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB影评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}游戏评论 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ game.cover|thumb:'normal' }}">
+    <title>{{ site_name }}游戏评论 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -73,6 +74,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -134,16 +136,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/games/templates/games/review_list.html b/games/templates/games/review_list.html
index 10b01d43..8b05318d 100644
--- a/games/templates/games/review_list.html
+++ b/games/templates/games/review_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ game.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ game.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -41,8 +41,8 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
@@ -137,12 +137,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/games/templates/games/scrape.html b/games/templates/games/scrape.html
index 87f2efa8..aaaeb99b 100644
--- a/games/templates/games/scrape.html
+++ b/games/templates/games/scrape.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/games/urls.py b/games/urls.py
index ff984632..88ce1629 100644
--- a/games/urls.py
+++ b/games/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -8,9 +8,10 @@ urlpatterns = [
     path('<int:id>/', retrieve, name='retrieve'),
     path('update/<int:id>/', update, name='update'),
     path('delete/<int:id>/', delete, name='delete'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('mark/', create_update_mark, name='create_update_mark'),
-    path('<int:game_id>/mark/list/',
-         retrieve_mark_list, name='retrieve_mark_list'),
+    path('wish/<int:id>/', wish, name='wish'),
+    re_path('(?P<game_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
     path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
     path('<int:game_id>/review/create/', create_review, name='create_review'),
     path('review/update/<int:id>/', update_review, name='update_review'),
diff --git a/games/views.py b/games/views.py
index f69e4918..0f0fc166 100644
--- a/games/views.py
+++ b/games/views.py
@@ -2,21 +2,23 @@ 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
+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.api import check_visibility, post_toot, TootVisibilityEnum
-from mastodon.utils import rating_to_emoji
+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
+from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
 from common.models import SourceSiteEnum
 from .models import *
 from .forms import *
-from boofilsic.settings import MASTODON_TAGS
+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__)
@@ -87,6 +89,18 @@ def create(request):
         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':
@@ -98,6 +112,7 @@ def update(request, id):
             'games/create_update.html',
             {
                 'form': form,
+                'is_update': True,
                 'title': page_title,
                 'submit_url': reverse("games:update", args=[game.id]),
                 # provided for frontend js
@@ -127,6 +142,7 @@ def update(request, id):
                 'games/create_update.html',
                 {
                     'form': form,
+                    'is_update': True,
                     'title': page_title,
                     'submit_url': reverse("games:update", args=[game.id]),
                     # provided for frontend js
@@ -167,6 +183,7 @@ def retrieve(request, id):
         else:
             mark_form = GameMarkForm(initial={
                 'game': game,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -186,10 +203,8 @@ def retrieve(request, id):
             mark_list_more = None
             review_list_more = None
         else:
-            mark_list = GameMark.get_available(
-                game, request.user, request.session['oauth_token'])
-            review_list = GameReview.get_available(
-                game, request.user, request.session['oauth_token'])
+            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:
@@ -197,6 +212,7 @@ def retrieve(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(game=game)))
 
         # def strip_html_tags(text):
         #     import re
@@ -221,6 +237,7 @@ def retrieve(request, id):
                 'review_list_more': review_list_more,
                 'game_tag_list': game_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -265,12 +282,19 @@ def create_update_mark(request):
         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:
@@ -278,7 +302,7 @@ def create_update_mark(request):
             form = GameMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -306,28 +330,10 @@ def create_update_mark(request):
                 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("games:retrieve",
-                                                                args=[game.id])
-                words = GameMarkStatusTranslator(form.cleaned_data['status']) +\
-                    f"《{game.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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("games:retrieve", args=[form.instance.game.id]))
     else:
@@ -336,11 +342,30 @@ def create_update_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_mark_list(request, game_id):
+def wish(request, id):
+    if request.method == 'POST':
+        game = get_object_or_404(Game, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            '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, request.session['oauth_token'])
+        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)
@@ -401,23 +426,8 @@ def create_review(request, game_id):
             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("games:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.game.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("games:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -453,23 +463,8 @@ def update_review(request, id):
             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("games:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.game.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("games:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -504,11 +499,10 @@ def delete_review(request, id):
 
 
 @mastodon_request_included
-@login_required
 def retrieve_review(request, id):
     if request.method == 'GET':
         review = get_object_or_404(GameReview, pk=id)
-        if not check_visibility(review, request.session['oauth_token'], request.user):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -543,8 +537,7 @@ def retrieve_review(request, id):
 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, request.session['oauth_token'])
+        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)
diff --git a/management/models.py b/management/models.py
index 9816781e..c5bf2f64 100644
--- a/management/models.py
+++ b/management/models.py
@@ -1,7 +1,7 @@
 import re
 from django.db import models
 from django.shortcuts import reverse
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 from markdownx.models import MarkdownxField
 from markdown import markdown
 
diff --git a/management/templates/management/create_update.html b/management/templates/management/create_update.html
index 9ffd568c..692dc126 100644
--- a/management/templates/management/create_update.html
+++ b/management/templates/management/create_update.html
@@ -4,8 +4,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/skeleton.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
     <title>Create/Update Announcement</title>
 </head>
 <body>
diff --git a/management/templates/management/delete.html b/management/templates/management/delete.html
index 11f08209..d48b740d 100644
--- a/management/templates/management/delete.html
+++ b/management/templates/management/delete.html
@@ -4,8 +4,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/skeleton.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
     <title>Delete Announcement</title>
 </head>
 <body>
diff --git a/management/templates/management/detail.html b/management/templates/management/detail.html
index a7cf64e5..76150b1d 100644
--- a/management/templates/management/detail.html
+++ b/management/templates/management/detail.html
@@ -6,9 +6,9 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <title>NiceDB - {{ object.title }}</title>
+    <title>{{ site_name }} - {{ object.title }}</title>
 </head>
 
 <body>
diff --git a/management/templates/management/list.html b/management/templates/management/list.html
index c8199e1f..ab5a2b24 100644
--- a/management/templates/management/list.html
+++ b/management/templates/management/list.html
@@ -7,10 +7,10 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 
-    <title>{% trans 'NiceDB - 公告栏' %}</title>
+    <title>{{ site_name }} - {% trans '公告栏' %}</title>
 </head>
 
 <body>
diff --git a/mastodon/admin.py b/mastodon/admin.py
index b29c2a3c..e81bc74b 100644
--- a/mastodon/admin.py
+++ b/mastodon/admin.py
@@ -38,7 +38,7 @@ class MastodonApplicationModelAdmin(admin.ModelAdmin):
                         try:
                             response = create_app(request.POST.get('domain_name'))
                         except (Timeout, ConnectionError):
-                            request.POST['domain_name'] = _("长毛象请求超时。")
+                            request.POST['domain_name'] = _("联邦网络请求超时。")
                         except Exception as e:
                             request.POST['domain_name'] = str(e)
                         else:
diff --git a/mastodon/api.py b/mastodon/api.py
index 5e48c944..1919f099 100644
--- a/mastodon/api.py
+++ b/mastodon/api.py
@@ -2,10 +2,16 @@ import requests
 import string
 import random
 import functools
+import logging
 from django.core.exceptions import ObjectDoesNotExist
-from boofilsic.settings import MASTODON_TIMEOUT
-from boofilsic.settings import CLIENT_NAME, APP_WEBSITE, REDIRECT_URIS
-from .models import CrossSiteUserInfo
+from django.conf import settings
+from django.shortcuts import reverse
+from urllib.parse import quote
+from .models import CrossSiteUserInfo, MastodonApplication
+from mastodon.utils import rating_to_emoji
+
+
+logger = logging.getLogger(__name__)
 
 # See https://docs.joinmastodon.org/methods/accounts/
 
@@ -46,39 +52,76 @@ API_CREATE_APP = '/api/v1/apps'
 # GET
 API_SEARCH = '/api/v2/search'
 
+TWITTER_DOMAIN = 'twitter.com'
 
-get = functools.partial(requests.get, timeout=MASTODON_TIMEOUT)
-post = functools.partial(requests.post, timeout=MASTODON_TIMEOUT)
+TWITTER_API_ME = 'https://api.twitter.com/2/users/me'
+
+TWITTER_API_POST = 'https://api.twitter.com/2/tweets'
+
+TWITTER_API_TOKEN = 'https://api.twitter.com/2/oauth2/token'
+
+USER_AGENT = f"{settings.CLIENT_NAME}/1.0"
+
+get = functools.partial(requests.get, timeout=settings.MASTODON_TIMEOUT)
+post = functools.partial(requests.post, timeout=settings.MASTODON_TIMEOUT)
 
 
 # low level api below
-def get_relationships(site, id_list, token):
+def get_relationships(site, id_list, token):  # no longer in use
     url = 'https://' + site + API_GET_RELATIONSHIPS
     payload = {'id[]': id_list}
     headers = {
+        'User-Agent': USER_AGENT,
         'Authorization': f'Bearer {token}'
     }
-    response = get(url, headers=headers, data=payload)
+    response = get(url, headers=headers, params=payload)
     return response.json()
 
 
 def post_toot(site, content, visibility, token, local_only=False):
-    url = 'https://' + site + API_PUBLISH_TOOT
     headers = {
+        'User-Agent': USER_AGENT,
         'Authorization': f'Bearer {token}',
         'Idempotency-Key': random_string_generator(16)
     }
-    payload = {
-        'status': content,
-        'visibility': visibility,
-        'local_only': True,
-    }
-    if not local_only:
-        del payload['local_only']
-    response = post(url, headers=headers, data=payload)
+    if site == TWITTER_DOMAIN:
+        url = TWITTER_API_POST
+        payload = {
+            'text': content if len(content) <= 150 else content[0:150] + '...'
+        }
+        response = post(url, headers=headers, json=payload)
+    else:
+        url = 'https://' + site + API_PUBLISH_TOOT
+        payload = {
+            'status': content,
+            'visibility': visibility,
+        }
+        if local_only:
+            payload['local_only'] = True
+        try:
+            response = post(url, headers=headers, data=payload)
+            if response.status_code == 201:
+                response.status_code = 200
+            if response.status_code != 200:
+                logger.error(f"Error {url} {response.status_code}")
+        except Exception:
+            response = None
     return response
 
 
+def get_instance_info(domain_name):
+    if domain_name.lower().strip() == TWITTER_DOMAIN:
+        return TWITTER_DOMAIN, ''
+    try:
+        url = f'https://{domain_name}/api/v1/instance'
+        response = get(url, headers={'User-Agent': USER_AGENT})
+        j = response.json()
+        return j['uri'].lower().split('//')[-1].split('/')[0], j['version']
+    except Exception:
+        logger.error(f"Error {url}")
+        return domain_name, ''
+
+
 def create_app(domain_name):
     # naive protocal strip
     is_http = False
@@ -96,18 +139,13 @@ def create_app(domain_name):
         url = 'http://' + domain_name + API_CREATE_APP
 
     payload = {
-        'client_name': CLIENT_NAME,
-        'scopes': 'read write follow',
-        'redirect_uris': REDIRECT_URIS,
-        'website': APP_WEBSITE
+        'client_name': settings.CLIENT_NAME,
+        'scopes': settings.MASTODON_CLIENT_SCOPE,
+        'redirect_uris': settings.REDIRECT_URIS,
+        'website': settings.APP_WEBSITE
     }
 
-    from boofilsic.settings import DEBUG
-    if DEBUG:
-        payload['redirect_uris'] = 'http://localhost/users/OAuth2_login/\nurn:ietf:wg:oauth:2.0:oob'
-        payload['client_name'] = 'test_do_not_authorise'
-
-    response = post(url, data=payload)
+    response = post(url, data=payload, headers={'User-Agent': USER_AGENT})
     return response
 
 
@@ -116,26 +154,35 @@ def get_site_id(username, user_site, target_site, token):
     payload = {
         'limit': 1,
         'type': 'accounts',
+        'resolve': True,
         'q': f"{username}@{user_site}"
     }
     headers = {
+        'User-Agent': USER_AGENT,
         'Authorization': f'Bearer {token}'
     }
-    response = get(url, data=payload, headers=headers)
-    data = response.json()
-    if not data['accounts']:
+    response = get(url, params=payload, headers=headers)
+    try:
+        data = response.json()
+    except Exception:
+        logger.error(f"Error parsing JSON from {url}")
+        return None
+    if 'accounts' not in data:
+        return None
+    elif len(data['accounts']) == 0:  # target site may return empty if no cache of this user
+        return None
+    elif data['accounts'][0]['acct'] != f"{username}@{user_site}":  # or return another user with a similar id which needs to be skipped
         return None
     else:
         return data['accounts'][0]['id']
 
 
 # high level api below
-def get_relationship(request_user, target_user, token):
-    if request_user.mastodon_site == target_user.mastodon_site:
-        return get_relationships(request_user.mastodon_site, target_user.mastodon_id, token)
-    else:
-        cross_site_id = get_cross_site_id(target_user, request_user.mastodon_site, token)
-        return get_relationships(request_user.mastodon_site, [cross_site_id,], token)
+def get_relationship(request_user, target_user, useless_token=None):
+    return [{
+        'blocked_by': target_user.is_blocking(request_user),
+        'following': request_user.is_following(target_user),
+    }]
 
 
 def get_cross_site_id(target_user, target_site, token):
@@ -147,6 +194,8 @@ def get_cross_site_id(target_user, target_site, token):
     """
     if target_site == target_user.mastodon_site:
         return target_user.mastodon_id
+    if target_site == TWITTER_DOMAIN:
+        return None
 
     try:
         cross_site_info = CrossSiteUserInfo.objects.get(
@@ -157,6 +206,7 @@ def get_cross_site_id(target_user, target_site, token):
         cross_site_id = get_site_id(
             target_user.username, target_user.mastodon_site, target_site, token)
         if not cross_site_id:
+            logger.error(f'unable to find cross_site_id for {target_user} on {target_site}')
             return None
         cross_site_info = CrossSiteUserInfo.objects.create(
             uid=f"{target_user.username}@{target_user.mastodon_site}",
@@ -167,30 +217,251 @@ def get_cross_site_id(target_user, target_site, token):
     return cross_site_info.site_id
 
 
-def check_visibility(user_owned_entity, token, visitor):
-    """
-    check if given user can see the user owned entity
-    """
-    if not visitor == user_owned_entity.owner:
-        # mastodon request
-        relationship = get_relationship(visitor, user_owned_entity.owner, token)[0]
-        if relationship['blocked_by']:
-            return False
-        if not relationship['following'] and user_owned_entity.is_private:
-            return False
-        return True
-    else:
-        return True
-
-
 # utils below
 def random_string_generator(n):
     s = string.ascii_letters + string.punctuation + string.digits
     return ''.join(random.choice(s) for i in range(n))
 
 
+def verify_account(site, token):
+    if site == TWITTER_DOMAIN:
+        url = TWITTER_API_ME + '?user.fields=id,username,name,description,profile_image_url,created_at,protected'
+        try:
+            response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
+            if response.status_code != 200:
+                logger.error(f"Error {url} {response.status_code}")
+                return response.status_code, None
+            r = response.json()['data']
+            r['display_name'] = r['name']
+            r['note'] = r['description']
+            r['avatar'] = r['profile_image_url']
+            r['avatar_static'] = r['profile_image_url']
+            r['locked'] = r['protected']
+            r['url'] = f'https://{TWITTER_DOMAIN}/{r["username"]}'
+            return 200, r
+        except Exception:
+            return -1, None
+    url = 'https://' + site + API_VERIFY_ACCOUNT
+    try:
+        response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
+        return response.status_code, (response.json() if response.status_code == 200 else None)
+    except Exception:
+        return -1, None
+
+
+def get_related_acct_list(site, token, api):
+    if site == TWITTER_DOMAIN:
+        return []
+    url = 'https://' + site + api
+    results = []
+    while url:
+        response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
+        url = None
+        if response.status_code == 200:
+            results.extend(map(lambda u: (u['acct'] if u['acct'].find('@') != -1 else u['acct'] + '@' + site) if 'acct' in u else u, response.json()))
+            if 'Link' in response.headers:
+                for ls in response.headers['Link'].split(','):
+                    li = ls.strip().split(';')
+                    if li[1].strip() == 'rel="next"':
+                        url = li[0].strip().replace('>', '').replace('<', '')
+    return results
+
+
 class TootVisibilityEnum:
     PUBLIC = 'public'
     PRIVATE = 'private'
     DIRECT = 'direct'
     UNLISTED = 'unlisted'
+
+
+def get_mastodon_application(domain):
+    app = MastodonApplication.objects.filter(domain_name=domain).first()
+    if app is not None:
+        return app, ''
+    if domain == TWITTER_DOMAIN:
+        return None, 'Twitter未配置'
+    error_msg = None
+    try:
+        response = create_app(domain)
+    except (requests.exceptions.Timeout, ConnectionError):
+        error_msg = "联邦网络请求超时。"
+        logger.error(f'Error creating app for {domain}: Timeout')
+    except Exception as e:
+        error_msg = "联邦网络请求失败 " + str(e)
+        logger.error(f'Error creating app for {domain}: {e}')
+    else:
+        # fill the form with returned data
+        if response.status_code != 200:
+            error_msg = "实例连接错误,代码: " + str(response.status_code)
+            logger.error(f'Error creating app for {domain}: {response.status_code}')
+        else:
+            try:
+                data = response.json()
+            except Exception:
+                error_msg = "实例返回内容无法识别"
+                logger.error(f'Error creating app for {domain}: unable to parse response')
+            else:
+                if settings.MASTODON_ALLOW_ANY_SITE:
+                    app = MastodonApplication.objects.create(domain_name=domain, app_id=data['id'], client_id=data['client_id'],
+                    client_secret=data['client_secret'], vapid_key=data['vapid_key'] if 'vapid_key' in data else '')
+                else:
+                    error_msg = "不支持其它实例登录"
+                    logger.error(f'Disallowed to create app for {domain}')
+    return app, error_msg
+
+
+def get_mastodon_login_url(app, login_domain, version, request):
+    url = request.scheme + "://" + request.get_host() + reverse('users:OAuth2_login')
+    if login_domain == TWITTER_DOMAIN:
+        return f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={app.client_id}&redirect_uri={quote(url)}&scope={quote(settings.TWITTER_CLIENT_SCOPE)}&state=state&code_challenge=challenge&code_challenge_method=plain"
+    scope = settings.MASTODON_LEGACY_CLIENT_SCOPE if 'Pixelfed' in version else settings.MASTODON_CLIENT_SCOPE
+    return "https://" + login_domain + "/oauth/authorize?client_id=" + app.client_id + "&scope=" + quote(scope) + "&redirect_uri=" + url + "&response_type=code"
+
+
+def obtain_token(site, request, code):
+    """ Returns token if success else None. """
+    mast_app = MastodonApplication.objects.get(domain_name=site)
+    redirect_uri = request.scheme + "://" + request.get_host() + reverse('users:OAuth2_login')
+    payload = {
+        'client_id': mast_app.client_id,
+        'client_secret': mast_app.client_secret,
+        'redirect_uri': redirect_uri,
+        'grant_type': 'authorization_code',
+        'code': code
+    }
+    headers = {'User-Agent': USER_AGENT}
+    auth = None
+    if mast_app.is_proxy:
+        url = 'https://' + mast_app.proxy_to + API_OBTAIN_TOKEN
+    elif site == TWITTER_DOMAIN:
+        url = TWITTER_API_TOKEN
+        auth = (mast_app.client_id, mast_app.client_secret)
+        del payload['client_secret']
+        payload['code_verifier'] = 'challenge'
+    else:
+        url = 'https://' + mast_app.domain_name + API_OBTAIN_TOKEN
+    try:
+        response = post(url, data=payload, headers=headers, auth=auth)
+        # {"token_type":"bearer","expires_in":7200,"access_token":"VGpkOEZGR3FQRDJ5NkZ0dmYyYWIwS0dqeHpvTnk4eXp0NV9nWDJ2TEpmM1ZTOjE2NDg3ODMxNTU4Mzc6MToxOmF0OjE","scope":"block.read follows.read offline.access tweet.write users.read mute.read","refresh_token":"b1pXbGEzeUF1WE5yZHJOWmxTeWpvMTBrQmZPd0czLU0tQndZQTUyU3FwRDVIOjE2NDg3ODMxNTU4Mzg6MToxOnJ0OjE"}
+        if response.status_code != 200:
+            logger.error(f"Error {url} {response.status_code}")
+            return None, None
+    except Exception as e:
+        logger.error(f"Error {url} {e}")
+        return None, None
+    data = response.json()
+    return data.get('access_token'), data.get('refresh_token', '')
+
+
+def refresh_access_token(site, refresh_token):
+    if site != TWITTER_DOMAIN:
+        return None
+    mast_app = MastodonApplication.objects.get(domain_name=site)
+    url = TWITTER_API_TOKEN
+    payload = {
+        'client_id': mast_app.client_id,
+        'refresh_token': refresh_token,
+        'grant_type': 'refresh_token',
+    }
+    headers = {'User-Agent': USER_AGENT}
+    auth = (mast_app.client_id, mast_app.client_secret)
+    response = post(url, data=payload, headers=headers, auth=auth)
+    if response.status_code != 200:
+        logger.error(f"Error {url} {response.status_code}")
+        return None
+    data = response.json()
+    return data.get('access_token')
+
+
+def revoke_token(site, token):
+    mast_app = MastodonApplication.objects.get(domain_name=site)
+
+    payload = {
+        'client_id': mast_app.client_id,
+        'client_secret': mast_app.client_secret,
+        'token': token
+    }
+
+    if mast_app.is_proxy:
+        url = 'https://' + mast_app.proxy_to + API_REVOKE_TOKEN
+    else:
+        url = 'https://' + site + API_REVOKE_TOKEN
+    post(url, data=payload, headers={'User-Agent': USER_AGENT})
+
+
+def share_mark(mark):
+    user = mark.owner
+    if mark.visibility == 2:
+        visibility = TootVisibilityEnum.DIRECT
+    elif mark.visibility == 1:
+        visibility = TootVisibilityEnum.PRIVATE
+    elif user.get_preference().mastodon_publish_public:
+        visibility = TootVisibilityEnum.PUBLIC
+    else:
+        visibility = TootVisibilityEnum.UNLISTED
+    tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(mark.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
+    stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode)
+    content = f"{mark.translated_status}《{mark.item.title}》{stars}\n{mark.item.url}\n{mark.text}{tags}"
+    response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
+    if response and response.status_code in [200, 201]:
+        j = response.json()
+        if 'url' in j:
+            mark.shared_link = j['url']
+        elif 'data' in j:
+            mark.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
+        if mark.shared_link:
+            mark.save(update_fields=['shared_link'])
+        return True
+    else:
+        return False
+
+
+def share_review(review):
+    user = review.owner
+    if review.visibility == 2:
+        visibility = TootVisibilityEnum.DIRECT
+    elif review.visibility == 1:
+        visibility = TootVisibilityEnum.PRIVATE
+    elif user.get_preference().mastodon_publish_public:
+        visibility = TootVisibilityEnum.PUBLIC
+    else:
+        visibility = TootVisibilityEnum.UNLISTED
+    tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(review.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
+    content = f"发布了关于《{review.item.title}》的评论\n{review.url}\n{review.title}{tags}"
+    response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
+    if response and response.status_code in [200, 201]:
+        j = response.json()
+        if 'url' in j:
+            review.shared_link = j['url']
+        elif 'data' in j:
+            review.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
+        if review.shared_link:
+            review.save(update_fields=['shared_link'])
+        return True
+    else:
+        return False
+
+
+def share_collection(collection, comment, user, visibility_no):
+    if visibility_no == 2:
+        visibility = TootVisibilityEnum.DIRECT
+    elif visibility_no == 1:
+        visibility = TootVisibilityEnum.PRIVATE
+    elif user.get_preference().mastodon_publish_public:
+        visibility = TootVisibilityEnum.PUBLIC
+    else:
+        visibility = TootVisibilityEnum.UNLISTED
+    tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', '收藏单') if user.get_preference().mastodon_append_tag else ''
+    content = f"分享收藏单《{collection.title}》\n{collection.url}\n{comment}{tags}"
+    response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
+    if response and response.status_code in [200, 201]:
+        j = response.json()
+        if 'url' in j:
+            shared_link = j['url']
+        elif 'data' in j:
+            shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
+        if shared_link:
+            pass
+        return True
+    else:
+        return False
diff --git a/mastodon/auth.py b/mastodon/auth.py
index 29a691e8..02d7703f 100644
--- a/mastodon/auth.py
+++ b/mastodon/auth.py
@@ -1,74 +1,5 @@
 from django.contrib.auth.backends import ModelBackend, UserModel
-from django.shortcuts import reverse
-from .api import *
-from .models import MastodonApplication
-
-
-def obtain_token(site, request, code):
-    """ Returns token if success else None. """
-    mast_app = MastodonApplication.objects.get(domain_name=site)
-    payload = {
-        'client_id': mast_app.client_id,
-        'client_secret': mast_app.client_secret,
-        'redirect_uri': f"https://{request.get_host()}{reverse('users:OAuth2_login')}",
-        'grant_type': 'authorization_code',
-        'code': code,
-        'scope': 'read write'
-    }
-    from boofilsic.settings import DEBUG
-    if DEBUG:
-        payload['redirect_uri']= f"http://{request.get_host()}{reverse('users:OAuth2_login')}",
-    if mast_app.is_proxy:
-        url = 'https://' + mast_app.proxy_to + API_OBTAIN_TOKEN
-    else:
-        url = 'https://' + mast_app.domain_name + API_OBTAIN_TOKEN
-    response = post(url, data=payload)
-    if response.status_code != 200:
-        return
-    data = response.json()
-    return data.get('access_token')
-
-
-def get_user_data(site, token):
-    url = 'https://' + site + API_VERIFY_ACCOUNT
-    headers = {
-        'Authorization': f'Bearer {token}'
-    }
-    response = get(url, headers=headers)
-    if response.status_code != 200:
-        return None
-    return response.json()
-
-
-def revoke_token(site, token):
-    mast_app = MastodonApplication.objects.get(domain_name=site)
-
-    payload = {
-        'client_id': mast_app.client_id,
-        'client_secret': mast_app.client_secret,
-        'scope': token
-    }
-
-    if mast_app.is_proxy:
-        url = 'https://' + mast_app.proxy_to + API_REVOKE_TOKEN
-    else:
-        url = 'https://' + site + API_REVOKE_TOKEN
-    response = post(url, data=payload)
-
-
-def verify_token(site, token):
-    """ Check if the token is valid and is of local instance. """
-    url = 'https://' + site + API_VERIFY_ACCOUNT
-    headers = {
-        'Authorization': f'Bearer {token}'
-    }
-    response = get(url, headers=headers)
-    if response.status_code == 200:
-        res_data = response.json()
-        # check if is local instance user
-        if res_data['acct'] == res_data['username']:
-            return True
-    return False
+from .api import verify_account
 
 
 class OAuth2Backend(ModelBackend):
@@ -76,22 +7,23 @@ class OAuth2Backend(ModelBackend):
     # "authenticate() should check the credentials it gets and returns
     #  a user object that matches those credentials."
     # arg request is an interface specification, not used in this implementation
-    def authenticate(self, request, token=None, username=None, site=None,  **kwargs):
+
+    def authenticate(self, request, token=None, username=None, site=None, **kwargs):
         """ when username is provided, assume that token is newly obtained and valid """
         if token is None or site is None:
             return
 
         if username is None:
-            user_data = get_user_data(site, token)
-            if user_data:
-                username = user_data['username']
+            code, user_data = verify_account(site, token)
+            if code == 200:
+                userid = user_data['id']
             else:
                 # aquiring user data fail means token is invalid thus auth fail
                 return None
 
         # when username is provided, assume that token is newly obtained and valid
         try:
-            user = UserModel._default_manager.get_by_natural_key(user_data['username'])
+            user = UserModel._default_manager.get(mastodon_id=userid, mastodon_site=site)
         except UserModel.DoesNotExist:
             return None
         else:
diff --git a/mastodon/decorators.py b/mastodon/decorators.py
index 3f2ec5cd..ef7a31d1 100644
--- a/mastodon/decorators.py
+++ b/mastodon/decorators.py
@@ -16,7 +16,7 @@ def mastodon_request_included(func):
                 args[0],
                 'common/error.html',
                 {
-                    'msg': _("长毛象请求超时叻_(´ཀ`」 ∠)__ ")
+                    'msg': _("联邦网络请求超时叻_(´ཀ`」 ∠)__ ")
                 }
             )
     return wrapper
diff --git a/mastodon/management/commands/wrong_sites.py b/mastodon/management/commands/wrong_sites.py
new file mode 100644
index 00000000..f985a1e7
--- /dev/null
+++ b/mastodon/management/commands/wrong_sites.py
@@ -0,0 +1,21 @@
+from django.core.management.base import BaseCommand
+from mastodon.models import MastodonApplication
+from django.conf import settings
+from mastodon.api import get_instance_info
+from users.models import User
+
+
+class Command(BaseCommand):
+    help = 'Find wrong sites'
+
+    def handle(self, *args, **options):
+        for site in MastodonApplication.objects.all():
+            d = site.domain_name
+            login_domain = d.strip().lower().split('//')[-1].split('/')[0].split('@')[-1]
+            domain, version = get_instance_info(login_domain)
+            if d != domain:
+                print(f'{d} should be {domain}')
+                for u in User.objects.filter(mastodon_site=d, is_active=True):
+                    u.mastodon_site = domain
+                    print(f'fixing {u}')
+                    u.save()
diff --git a/mastodon/models.py b/mastodon/models.py
index b6ac1207..49346810 100644
--- a/mastodon/models.py
+++ b/mastodon/models.py
@@ -1,14 +1,16 @@
 from django.db import models
 from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 
 
 class MastodonApplication(models.Model):
     domain_name = models.CharField(_('site domain name'), max_length=100, unique=True)
-    app_id = models.PositiveIntegerField(_('in-site app id'))
+    app_id = models.PositiveIntegerField(_('in-site app id'))  # TODO Remove? bc 1) it seems useless 2) GoToSocial returns a hash text id
     client_id = models.CharField(_('client id'), max_length=100)
     client_secret = models.CharField(_('client secret'), max_length=100)
-    vapid_key = models.CharField(_('vapid key'), max_length=200)
+    vapid_key = models.CharField(_('vapid key'), max_length=200, null=True, blank=True)
+    star_mode = models.PositiveIntegerField(_('0: custom emoji; 1: unicode moon; 2: text'), blank=False, default=0)
+    max_status_len = models.PositiveIntegerField(_('max toot len'), blank=False, default=500)
 
     is_proxy = models.BooleanField(default=False, blank=True)
     proxy_to = models.CharField(max_length=100, blank=True, default='')
@@ -27,7 +29,7 @@ class CrossSiteUserInfo(models.Model):
     # target site domain name
     target_site = models.CharField(_("target site domain name"), max_length=100)
     # target site id
-    site_id = models.PositiveIntegerField()
+    site_id = models.CharField(max_length=100, blank=False)
 
     class Meta:
         constraints = [
diff --git a/mastodon/utils.py b/mastodon/utils.py
index 7da7f94e..8bada43a 100644
--- a/mastodon/utils.py
+++ b/mastodon/utils.py
@@ -1,14 +1,17 @@
-from boofilsic.settings import STAR_EMPTY, STAR_HALF, STAR_SOLID
+from django.conf import settings
 
 
-def rating_to_emoji(score):
+def rating_to_emoji(score, star_mode = 0):
     """ convert score to mastodon star emoji code """
     if score is None or score == '' or score == 0:
         return ''
     solid_stars = score // 2
     half_star = int(bool(score % 2))
     empty_stars = 5 - solid_stars if not half_star else 5 - solid_stars - 1
-    emoji_code = STAR_SOLID * solid_stars + STAR_HALF * half_star + STAR_EMPTY * empty_stars
+    if star_mode == 1:
+        emoji_code = "🌕" * solid_stars + "🌗" * half_star + "🌑" * empty_stars
+    else:
+        emoji_code = settings.STAR_SOLID * solid_stars + settings.STAR_HALF * half_star + settings.STAR_EMPTY * empty_stars
     emoji_code = emoji_code.replace("::", ": :")
     emoji_code = ' ' + emoji_code + ' '
     return emoji_code
\ No newline at end of file
diff --git a/movies/admin.py b/movies/admin.py
index ca4d1b0e..bb2b1a54 100644
--- a/movies/admin.py
+++ b/movies/admin.py
@@ -1,7 +1,8 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Movie)
+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
index bda16f08..3b025da4 100644
--- a/movies/apps.py
+++ b/movies/apps.py
@@ -3,3 +3,8 @@ 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
index aa320bd5..587d92e9 100644
--- a/movies/forms.py
+++ b/movies/forms.py
@@ -1,18 +1,13 @@
 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
+from .models import Movie, MovieMark, MovieReview, MovieGenreEnum, MovieMarkStatusTranslation
 from common.models import MarkStatusEnum
 from common.forms import *
 
 
 def MovieMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在看"),
-        MarkStatusEnum.WISH.value: _("想看"),
-        MarkStatusEnum.COLLECT.value: _("看过")
-    }
-    return trans_dict[status]
+    return MovieMarkStatusTranslation[status]
 
 
 class MovieForm(forms.ModelForm):
@@ -119,11 +114,8 @@ class MovieMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'movie': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -138,14 +130,8 @@ class MovieReviewForm(ReviewForm):
             'movie',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'book': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'movie': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/movies/management/commands/fix-movie-poster.py b/movies/management/commands/fix-movie-poster.py
new file mode 100644
index 00000000..b8a35f85
--- /dev/null
+++ b/movies/management/commands/fix-movie-poster.py
@@ -0,0 +1,203 @@
+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('不存在[^<]+</title>', 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 = '<html />'
+        # 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
index be6d4ba3..ae3a9990 100644
--- a/movies/models.py
+++ b/movies/models.py
@@ -1,17 +1,27 @@
 import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
 from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
-from boofilsic.settings import MOVIE_MEDIA_PATH_ROOT, DEFAULT_MOVIE_IMAGE
 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, MOVIE_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.MOVIE_MEDIA_PATH_ROOT)
 
 
 class MovieGenreEnum(models.TextChoices):
@@ -47,6 +57,10 @@ class MovieGenreEnum(models.TextChoices):
     REALITY_TV = 'Reality-TV', _('真人秀')
     FAMILY = 'Family', _('家庭')
     TALK_SHOW = 'Talk-Show', _('脱口秀')
+    NEWS = 'News', _('新闻')
+    SOAP = 'Soap', _('肥皂剧')
+    TV_MOVIE = 'TV Movie', _('电视电影')
+    THEATRE = 'Theatre', _('舞台艺术')
     OTHER = 'Other', _('其他')
 
 
@@ -58,13 +72,13 @@ class Movie(Entity):
     Can either be movie or series.
     '''
     # widely recognized name, usually in Chinese
-    title = models.CharField(_("title"), max_length=200)
+    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=200)
+        _("original title"), blank=True, default='', max_length=500)
     other_title = postgres.ArrayField(
         models.CharField(_("other title"), blank=True,
-                         default='', max_length=300),
+                         default='', max_length=500),
         null=True,
         blank=True,
         default=list,
@@ -73,21 +87,21 @@ class Movie(Entity):
         blank=True, max_length=10, null=False, db_index=True, default='')
     director = postgres.ArrayField(
         models.CharField(_("director"), blank=True,
-                         default='', max_length=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
     )
     playwright = postgres.ArrayField(
         models.CharField(_("playwright"), blank=True,
-                         default='', max_length=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
     )
     actor = postgres.ArrayField(
         models.CharField(_("actor"), blank=True,
-                         default='', max_length=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
@@ -112,7 +126,7 @@ class Movie(Entity):
         default=list,
     )
     site = models.URLField(_('site url'), blank=True, default='', max_length=200)
-    
+
     # country or region
     area = postgres.ArrayField(
         models.CharField(
@@ -140,7 +154,7 @@ class Movie(Entity):
     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=DEFAULT_MOVIE_IMAGE, blank=True)
+    cover = models.ImageField(_("poster"), upload_to=movie_cover_path, default=settings.DEFAULT_MOVIE_IMAGE, blank=True)
 
     ############################################
     # exclusive fields to series
@@ -157,27 +171,67 @@ class Movie(Entity):
     ############################################
     is_series = models.BooleanField(default=False)
 
+    history = HistoricalRecords()
 
     def __str__(self):
         if self.year:
-            return self.title + f"({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:
@@ -185,23 +239,45 @@ class Movie(Entity):
         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 settings.APP_WEBSITE + 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)
@@ -211,3 +287,7 @@ class MovieTag(Tag):
             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
index f55c026b..497c9bce 100644
--- a/movies/templates/movies/create_update.html
+++ b/movies/templates/movies/create_update.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -21,10 +21,25 @@
             {% include "partial/_navbar.html" %}
 
             <section id="content" class="container">
-                <div class="grid">
+                <div class="grid" class="single-section-wrapper">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'movies:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'movies:scrape' %}"
-                            class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                   {% comment %} <a href="{% url 'movies:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -53,12 +68,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/movies/templates/movies/create_update_review.html b/movies/templates/movies/create_update_review.html
index 1f4e5b34..87046277 100644
--- a/movies/templates/movies/create_update_review.html
+++ b/movies/templates/movies/create_update_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -61,7 +61,7 @@
                                 {% if movie.director|length > 5 %}
                                 <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                 <script>
-                                    $("#directorMore").click(function (e) {
+                                    $("#directorMore").on('click', function (e) {
                                         $("span.director:not(:visible)").each(function (e) {
                                             $(this).parent().removeAttr('style');
                                         });
@@ -89,7 +89,7 @@
                                     {% if movie.actor|length > 5 %}
                                     <a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
                                     <script>
-                                        $("#actorMore").click(function (e) {
+                                        $("#actorMore").on('click', function (e) {
                                             $("span.actor:not(:visible)").each(function (e) {
                                                 $(this).parent().removeAttr('style');
                                             });
@@ -138,7 +138,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -158,12 +158,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/movies/templates/movies/delete.html b/movies/templates/movies/delete.html
index 5aa651ac..94dfb2ce 100644
--- a/movies/templates/movies/delete.html
+++ b/movies/templates/movies/delete.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除电影/剧集' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -62,7 +62,7 @@
                                 {% if movie.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' movie.last_editor.id %}">
+                                    <a href="{% url 'users:home' movie.last_editor.mastodon_username %}">
                                         <span>{{ movie.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -96,12 +96,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/movies/templates/movies/delete_review.html b/movies/templates/movies/delete_review.html
index a16e6c49..db3d3fe0 100644
--- a/movies/templates/movies/delete_review.html
+++ b/movies/templates/movies/delete_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html
index 8dbb4a09..72788ad0 100644
--- a/movies/templates/movies/detail.html
+++ b/movies/templates/movies/detail.html
@@ -14,12 +14,12 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB电影 - {{ movie.title }}">
+    <meta property="og:title" content="{{ site_name }}电影 - {{ movie.title }}">
     <meta property="og:type" content="video.movie">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ movie.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ movie.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ movie.brief }}">
     <!-- 
     video:actor - profile array - Actors in the movie.
     video:actor:role - string - The role they played.
@@ -31,17 +31,15 @@
     -->
     
     {% if movie.is_series %}
-    <title>{% trans 'NiceDB - 剧集详情' %} | {{ movie.title }}</title>
+    <title>{{ site_name }} - {% trans '剧集详情' %} | {{ movie.title }}</title>
     {% else %}
-    <title>{% trans 'NiceDB - 电影详情' %} | {{ movie.title }}</title>
+    <title>{{ site_name }} - {% trans '电影详情' %} | {{ movie.title }}</title>
     {% endif %}
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -78,11 +76,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if movie.rating %}
+                                            {% if movie.rating and movie.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ movie.rating }} </span>
+                                            <small>({{ movie.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if movie.imdb_code %}
@@ -99,7 +98,7 @@
                                             {% if movie.director|length > 5 %}
                                             <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#directorMore").click(function (e) {
+                                                $("#directorMore").on('click', function (e) {
                                                     $("span.director:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -118,7 +117,7 @@
                                             {% if movie.playwright|length > 5 %}
                                             <a href="javascript:void(0);" id="playwrightMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#playwrightMore").click(function (e) {
+                                                $("#playwrightMore").on('click', function (e) {
                                                     $("span.playwright:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -138,7 +137,7 @@
                                             {% if movie.actor|length > 5 %}
                                                 <a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
                                                 <script>
-                                                    $("#actorMore").click(function(e) {
+                                                    $("#actorMore").on('click', function(e) {
                                                         $("span.actor:not(:visible)").each(function(e){
                                                             $(this).parent().removeAttr('style');
                                                         });
@@ -197,7 +196,7 @@
                                         
                                     
                                         {% if movie.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.id %}">{{ movie.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.mastodon_username %}">{{ movie.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -249,30 +248,10 @@
                                 <h5 class="entity-marks__title">{% trans '这部电影的标记' %}</h5>
                                 {% endif %}
                                 {% if mark_list_more %}
-                                <a href="{% url 'movies:retrieve_mark_list' movie.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'movies:retrieve_mark_list' movie.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'movies:retrieve_mark_list' movie.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=movie %}    
                             </div>
                             <div class="entity-reviews">
                                 {% if movie.is_series %}
@@ -282,17 +261,21 @@
                                 {% endif %}
 
                                 {% if review_list_more %}
-                                <a href="{% url 'movies:retrieve_review_list' movie.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'movies:retrieve_review_list' movie.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
+                                    {% if others_review.movie != movie %}
+                                    <span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'movies:retrieve' others_review.movie.id %}">{{ others_review.movie.get_source_site_display }}</a></span>
+                                    {% endif %}
+
                                     <span class="entity-reviews__review-title"> <a href="{% url 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
                                     <span>{{ others_review.get_plain_content | truncate:100 }}</span>
                                 </li>
@@ -317,7 +300,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -329,7 +312,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -365,7 +348,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -394,7 +377,42 @@
 
                             {% endif %}
                         </div>
-                                
+
+                        {% if movie.get_related_movies.count > 0 %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关条目' %}</div>
+                                <div >
+                                    {% for m in movie.get_related_movies %}
+                                    <p>
+                                        <a href="{% url 'movies:retrieve' m.id %}">{{ m.title }}</a>
+                                        {% if movie.source_site != m.source_site %}
+                                        <span class="source-label source-label__{{ m.source_site }}">{{ m.get_source_site_display }}</span>
+                                        {% endif %}
+                                    </p>
+                                    {% endfor %}
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'movie' movie.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
                     </div>
                 </div>
             </section>
@@ -464,8 +482,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/movies/templates/movies/mark_list.html b/movies/templates/movies/mark_list.html
index 23e57b28..27cc0e1e 100644
--- a/movies/templates/movies/mark_list.html
+++ b/movies/templates/movies/mark_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ movie.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ movie.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,37 +35,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a>{% trans ' 的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-        
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                    </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-                                        
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=movie %}
                             </div>
                             <div class="pagination">
                             
@@ -129,7 +99,7 @@
                                 {% if movie.director|length > 5 %}
                                 <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                 <script>
-                                    $("#directorMore").click(function (e) {
+                                    $("#directorMore").on('click', function (e) {
                                         $("span.director:not(:visible)").each(function (e) {
                                             $(this).parent().removeAttr('style');
                                         });
@@ -168,12 +138,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
   
diff --git a/movies/templates/movies/review_detail.html b/movies/templates/movies/review_detail.html
index b52c7b54..4a83227e 100644
--- a/movies/templates/movies/review_detail.html
+++ b/movies/templates/movies/review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB影评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}影评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ movie.cover|thumb:'normal' }}">
+    <title>{{ site_name }}影评 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -73,6 +74,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -108,7 +110,7 @@
                                     {% if movie.director|length > 5 %}
                                     <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                     <script>
-                                        $("#directorMore").click(function (e) {
+                                        $("#directorMore").on('click', function (e) {
                                             $("span.director:not(:visible)").each(function (e) {
                                                 $(this).parent().removeAttr('style');
                                             });
@@ -148,16 +150,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/movies/templates/movies/review_list.html b/movies/templates/movies/review_list.html
index ed32cfe0..daedf296 100644
--- a/movies/templates/movies/review_list.html
+++ b/movies/templates/movies/review_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ movie.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ movie.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -41,11 +41,14 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
+                                            {% if review.movie != movie %}
+                                            <span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'movies:retrieve' review.movie.id %}">{{ review.movie.get_source_site_display }}</a></span>
+                                            {% endif %}
                                         
         
                                         <span href="{% url 'movies:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'movies:retrieve_review' review.id %}">{{ review.title }}</a></span>
@@ -116,7 +119,7 @@
                                     {% if movie.director|length > 5 %}
                                     <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                     <script>
-                                        $("#directorMore").click(function (e) {
+                                        $("#directorMore").on('click', function (e) {
                                             $("span.director:not(:visible)").each(function (e) {
                                                 $(this).parent().removeAttr('style');
                                             });
@@ -156,12 +159,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/movies/templates/movies/scrape.html b/movies/templates/movies/scrape.html
index 91cd90c9..0387cc10 100644
--- a/movies/templates/movies/scrape.html
+++ b/movies/templates/movies/scrape.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/movies/urls.py b/movies/urls.py
index 9f359e9e..0495a9f0 100644
--- a/movies/urls.py
+++ b/movies/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -8,8 +8,10 @@ urlpatterns = [
     path('<int:id>/', retrieve, name='retrieve'),
     path('update/<int:id>/', update, name='update'),
     path('delete/<int:id>/', delete, name='delete'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('mark/', create_update_mark, name='create_update_mark'),
-    path('<int:movie_id>/mark/list/', retrieve_mark_list, name='retrieve_mark_list'),
+    path('wish/<int:id>/', wish, name='wish'),
+    re_path('(?P<movie_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
     path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
     path('<int:movie_id>/review/create/', create_review, name='create_review'),
     path('review/update/<int:id>/', update_review, name='update_review'),
diff --git a/movies/views.py b/movies/views.py
index f63cdeef..d4fc21f5 100644
--- a/movies/views.py
+++ b/movies/views.py
@@ -2,21 +2,23 @@ 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
+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.api import check_visibility, post_toot, TootVisibilityEnum
-from mastodon.utils import rating_to_emoji
+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
+from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
 from common.models import SourceSiteEnum
 from .models import *
 from .forms import *
-from boofilsic.settings import MASTODON_TAGS
+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__)
@@ -87,6 +89,18 @@ def create(request):
         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':
@@ -98,6 +112,7 @@ def update(request, id):
             'movies/create_update.html',
             {
                 'form': form,
+                'is_update': True,
                 'title': page_title,
                 'submit_url': reverse("movies:update", args=[movie.id]),
                 # provided for frontend js
@@ -127,6 +142,7 @@ def update(request, id):
                 'movies/create_update.html',
                 {
                     'form': form,
+                    'is_update': True,
                     'title': page_title,
                     'submit_url': reverse("movies:update", args=[movie.id]),
                     # provided for frontend js
@@ -167,6 +183,7 @@ def retrieve(request, id):
         else:
             mark_form = MovieMarkForm(initial={
                 'movie': movie,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -185,10 +202,8 @@ def retrieve(request, id):
             mark_list_more = None
             review_list_more = None
         else:
-            mark_list = MovieMark.get_available(
-                movie, request.user, request.session['oauth_token'])
-            review_list = MovieReview.get_available(
-                movie, request.user, request.session['oauth_token'])
+            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:
@@ -196,6 +211,7 @@ def retrieve(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(movie=movie)))
 
         # def strip_html_tags(text):
         #     import re
@@ -220,6 +236,7 @@ def retrieve(request, id):
                 'review_list_more': review_list_more,
                 'movie_tag_list': movie_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -264,12 +281,19 @@ def create_update_mark(request):
         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:
@@ -277,7 +301,7 @@ def create_update_mark(request):
             form = MovieMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -305,28 +329,10 @@ def create_update_mark(request):
                 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("movies:retrieve",
-                                                                args=[movie.id])
-                words = MovieMarkStatusTranslator(form.cleaned_data['status']) +\
-                    f"《{movie.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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("movies:retrieve", args=[form.instance.movie.id]))
     else:
@@ -335,11 +341,30 @@ def create_update_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_mark_list(request, movie_id):
+def wish(request, id):
+    if request.method == 'POST':
+        movie = get_object_or_404(Movie, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            '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(
-            movie, request.user, request.session['oauth_token'])
+        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)
@@ -400,23 +425,8 @@ def create_review(request, movie_id):
             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("movies:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.movie.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("movies:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -452,23 +462,8 @@ def update_review(request, id):
             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("movies:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.movie.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("movies:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -503,11 +498,10 @@ def delete_review(request, id):
 
 
 @mastodon_request_included
-@login_required
 def retrieve_review(request, id):
     if request.method == 'GET':
         review = get_object_or_404(MovieReview, pk=id)
-        if not check_visibility(review, request.session['oauth_token'], request.user):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -542,8 +536,7 @@ def retrieve_review(request, id):
 def retrieve_review_list(request, movie_id):
     if request.method == 'GET':
         movie = get_object_or_404(Movie, pk=movie_id)
-        queryset = MovieReview.get_available(
-            movie, request.user, request.session['oauth_token'])
+        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)
diff --git a/music/admin.py b/music/admin.py
index eb770458..33eb056d 100644
--- a/music/admin.py
+++ b/music/admin.py
@@ -1,11 +1,12 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Song)
+admin.site.register(Song, SimpleHistoryAdmin)
 admin.site.register(SongMark)
 admin.site.register(SongReview)
 admin.site.register(SongTag)
-admin.site.register(Album)
+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
index d909c7fb..6fb97b37 100644
--- a/music/apps.py
+++ b/music/apps.py
@@ -3,3 +3,9 @@ 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
index 3eec5646..9e487592 100644
--- a/music/forms.py
+++ b/music/forms.py
@@ -7,12 +7,7 @@ from common.forms import *
 
 
 def MusicMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在听"),
-        MarkStatusEnum.WISH.value: _("想听"),
-        MarkStatusEnum.COLLECT.value: _("听过")
-    }
-    return trans_dict[status]
+    return MusicMarkStatusTranslation[status]
 
 
 class SongForm(forms.ModelForm):
@@ -65,11 +60,8 @@ class SongMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'song': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -84,14 +76,8 @@ class SongReviewForm(ReviewForm):
             'song',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'song': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'song': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -148,11 +134,8 @@ class AlbumMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'album': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -167,14 +150,8 @@ class AlbumReviewForm(ReviewForm):
             'album',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'album': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'album': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/music/management/commands/fix-album-cover.py b/music/management/commands/fix-album-cover.py
new file mode 100644
index 00000000..9b9f4d3e
--- /dev/null
+++ b/music/management/commands/fix-album-cover.py
@@ -0,0 +1,199 @@
+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('不存在[^<]+</title>', 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.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}', 60)
+                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 = '<html />'
+        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}'
+            # 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
index c738db5b..131f9a14 100644
--- a/music/models.py
+++ b/music/models.py
@@ -1,21 +1,29 @@
 import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, SourceSiteEnum, MarkStatusEnum
 from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
-from boofilsic.settings import SONG_MEDIA_PATH_ROOT, DEFAULT_SONG_IMAGE, ALBUM_MEDIA_PATH_ROOT, DEFAULT_ALBUM_IMAGE
 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, SONG_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.SONG_MEDIA_PATH_ROOT)
 
 
 def album_cover_path(instance, filename):
-    return GenerateDateUUIDMediaFilePath(instance, filename, ALBUM_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.ALBUM_MEDIA_PATH_ROOT)
 
 
 class Album(Entity):
@@ -23,11 +31,11 @@ class Album(Entity):
     release_date = models.DateField(
         _('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
     cover = models.ImageField(
-        _("封面"), upload_to=album_cover_path, default=DEFAULT_ALBUM_IMAGE, blank=True)
+        _("封面"), 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=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
@@ -45,12 +53,36 @@ class Album(Entity):
     )
     track_list = models.TextField(_("曲目"), blank=True, default="")
 
+    history = HistoricalRecords()
+
     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
 
@@ -58,6 +90,14 @@ class Album(Entity):
     def verbose_category_name(self):
         return _("专辑")
 
+    @property
+    def mark_class(self):
+        return AlbumMark
+
+    @property
+    def tag_class(self):
+        return AlbumTag
+
 
 class Song(Entity):
     '''
@@ -70,7 +110,7 @@ class Song(Entity):
     # duration in ms
     duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
     cover = models.ImageField(
-        _("封面"), upload_to=song_cover_path, default=DEFAULT_SONG_IMAGE, blank=True)
+        _("封面"), upload_to=song_cover_path, default=settings.DEFAULT_SONG_IMAGE, blank=True)
     artist = postgres.ArrayField(
         models.CharField(blank=True,
                          default='', max_length=100),
@@ -84,19 +124,46 @@ class Song(Entity):
     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)
@@ -107,6 +174,10 @@ class SongMark(Mark):
                 fields=['owner', 'song'], name='unique_song_mark')
         ]
 
+    @property
+    def translated_status(self):
+        return MusicMarkStatusTranslation[self.status]
+
 
 class SongReview(Review):
     song = models.ForeignKey(
@@ -118,6 +189,14 @@ class SongReview(Review):
                 fields=['owner', 'song'], name='unique_song_review')
         ]
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + reverse("music:retrieve_song_review", args=[self.id])
+
+    @property
+    def item(self):
+        return self.song
+
 
 class SongTag(Tag):
     song = models.ForeignKey(
@@ -131,6 +210,10 @@ class SongTag(Tag):
                 fields=['content', 'mark'], name="unique_songmark_tag")
         ]
 
+    @property
+    def item(self):
+        return self.song
+
 
 class AlbumMark(Mark):
     album = models.ForeignKey(
@@ -142,6 +225,10 @@ class AlbumMark(Mark):
                 fields=['owner', 'album'], name='unique_album_mark')
         ]
 
+    @property
+    def translated_status(self):
+        return MusicMarkStatusTranslation[self.status]
+
 
 class AlbumReview(Review):
     album = models.ForeignKey(
@@ -153,6 +240,14 @@ class AlbumReview(Review):
                 fields=['owner', 'album'], name='unique_album_review')
         ]
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + reverse("music:retrieve_album_review", args=[self.id])
+
+    @property
+    def item(self):
+        return self.album
+
 
 class AlbumTag(Tag):
     album = models.ForeignKey(
@@ -165,3 +260,7 @@ class AlbumTag(Tag):
             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
index 68fe1143..7de3d0ae 100644
--- a/music/templates/music/album_detail.html
+++ b/music/templates/music/album_detail.html
@@ -6,6 +6,7 @@
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load strip_scheme %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -13,21 +14,19 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB音乐 - {{ album.title }}">
+    <meta property="og:title" content="{{ site_name }}音乐 - {{ album.title }}">
     <meta property="og:type" content="music.album">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ album.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ album.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ album.brief }}">
 
-    <title>{% trans 'NiceDB - 音乐详情' %} | {{ album.title }}</title>
+    <title>{{ site_name }} - {% trans '音乐详情' %} | {{ album.title }}</title>
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -55,11 +54,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if album.rating %}
+                                            {% if album.rating and album.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ album.rating }} </span>
+                                            <small>({{ album.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if album.artist %}{% trans '艺术家:' %}
@@ -72,7 +72,7 @@
                                             {% if album.artist|length > 5 %}
                                             <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#artistMore").click(function (e) {
+                                                $("#artistMore").on('click', function (e) {
                                                     $("span.artist:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -91,7 +91,7 @@
                                             {% if album.company|length > 5 %}
                                             <a href="javascript:void(0);" id="companyMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#companyMore").click(function (e) {
+                                                $("#companyMore").on('click', function (e) {
                                                     $("span.company:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -126,7 +126,7 @@
                                         
                                     
                                         {% if album.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.id %}">{{ album.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.mastodon_username %}">{{ album.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -213,43 +213,23 @@
                                 
                                 <h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
                                 {% if mark_list_more %}
-                                <a href="{% url 'music:retrieve_album_mark_list' album.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'music:retrieve_album_mark_list' album.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'music:retrieve_album_mark_list' album.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=album %}    
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
 
                                 {% if review_list_more %}
-                                <a href="{% url 'music:retrieve_album_review_list' album.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'music:retrieve_album_review_list' album.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
@@ -277,7 +257,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -289,7 +269,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -320,7 +300,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -349,7 +329,28 @@
 
                             {% endif %}
                         </div>
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'album' album.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
                                 
+                        {% if album.get_embed_link %}
+                        <iframe src="{{ album.get_embed_link }}"  height="320" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
+                        {% endif %}
                     </div>
                 </div>
             </section>
@@ -411,8 +412,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/music/templates/music/album_mark_list.html b/music/templates/music/album_mark_list.html
index cc9c7821..49e4b670 100644
--- a/music/templates/music/album_mark_list.html
+++ b/music/templates/music/album_mark_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ album.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ album.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,38 +35,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'music:retrieve_album' album.id %}">{{ album.title }}</a>{% trans '的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-
-                                    {% for mark in marks %}
-
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
-                                                viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=album %}
                             </div>
                             <div class="pagination">
 
@@ -124,7 +93,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -154,12 +123,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/album_review_detail.html b/music/templates/music/album_review_detail.html
index fb3b7b5a..3e6e8fdd 100644
--- a/music/templates/music/album_review_detail.html
+++ b/music/templates/music/album_review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}乐评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ album.cover|thumb:'normal' }}">
+    <title>{{ site_name }}乐评 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -77,6 +78,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
 
@@ -109,7 +111,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -139,16 +141,8 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/music/templates/music/album_review_list.html b/music/templates/music/album_review_list.html
index a4e4f04a..40ce5b6d 100644
--- a/music/templates/music/album_review_list.html
+++ b/music/templates/music/album_review_list.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ album.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ album.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -40,9 +40,9 @@
 
                                     <li class="entity-reviews__review entity-reviews__review--wider">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                        {% if review.is_private %}
+                                        {% if review.visibility > 0 %}
                                         <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                                 viewBox="0 0 20 20">
                                                     <path
@@ -120,7 +120,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -150,12 +150,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/create_update_album.html b/music/templates/music/create_update_album.html
index 841eab79..111c60a1 100644
--- a/music/templates/music/create_update_album.html
+++ b/music/templates/music/create_update_album.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -22,9 +22,24 @@
 
             <section id="content" class="container">
                 <div class="grid">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'music:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'music:scrape_album' %}"
-                            class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                        {% comment %} <a href="{% url 'music:scrape_album' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -54,12 +69,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/music/templates/music/create_update_album_review.html b/music/templates/music/create_update_album_review.html
index db1f11f8..440bce52 100644
--- a/music/templates/music/create_update_album_review.html
+++ b/music/templates/music/create_update_album_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -52,7 +52,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -96,7 +96,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -116,12 +116,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html
index a694c891..4a4e8b49 100644
--- a/music/templates/music/create_update_song.html
+++ b/music/templates/music/create_update_song.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -57,12 +57,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/music/templates/music/create_update_song_review.html b/music/templates/music/create_update_song_review.html
index 03e8271f..ec74d0bb 100644
--- a/music/templates/music/create_update_song_review.html
+++ b/music/templates/music/create_update_song_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -52,7 +52,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -100,7 +100,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -120,12 +120,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/delete_album.html b/music/templates/music/delete_album.html
index bbc15964..8539d736 100644
--- a/music/templates/music/delete_album.html
+++ b/music/templates/music/delete_album.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除音乐' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除音乐' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if album.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' album.last_editor.id %}">
+                                    <a href="{% url 'users:home' album.last_editor.mastodon_username %}">
                                         <span>{{ album.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html
index 7deaaa49..b67d065e 100644
--- a/music/templates/music/delete_album_review.html
+++ b/music/templates/music/delete_album_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/delete_song.html b/music/templates/music/delete_song.html
index 4d3825af..1be0112c 100644
--- a/music/templates/music/delete_song.html
+++ b/music/templates/music/delete_song.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除音乐' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除音乐' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if song.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' song.last_editor.id %}">
+                                    <a href="{% url 'users:home' song.last_editor.mastodon_username %}">
                                         <span>{{ song.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html
index 9e515b02..d2d610f3 100644
--- a/music/templates/music/delete_song_review.html
+++ b/music/templates/music/delete_song_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html
index 8079aa9e..b4fc9aba 100644
--- a/music/templates/music/scrape_album.html
+++ b/music/templates/music/scrape_album.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/templates/music/scrape_song.html b/music/templates/music/scrape_song.html
index e2cd4730..2ffa1b3b 100644
--- a/music/templates/music/scrape_song.html
+++ b/music/templates/music/scrape_song.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html
index 6aa9b87b..48a8c7be 100644
--- a/music/templates/music/song_detail.html
+++ b/music/templates/music/song_detail.html
@@ -6,6 +6,7 @@
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load strip_scheme %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -13,21 +14,19 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB音乐 - {{ song.title }}">
+    <meta property="og:title" content="{{ site_name }}音乐 - {{ song.title }}">
     <meta property="og:type" content="music.song">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ song.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ song.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ song.brief }}">
 
-    <title>{% trans 'NiceDB - 音乐详情' %} | {{ song.title }}</title>
+    <title>{{ site_name }} - {% trans '音乐详情' %} | {{ song.title }}</title>
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -54,11 +53,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if song.rating %}
+                                            {% if song.rating and song.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ song.rating }} </span>
+                                            <small>({{ song.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if song.artist %}{% trans '艺术家:' %}
@@ -71,7 +71,7 @@
                                             {% if song.artist|length > 5 %}
                                             <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#artistMore").click(function (e) {
+                                                $("#artistMore").on('click', function (e) {
                                                     $("span.artist:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -114,7 +114,7 @@
                                         
                                     
                                         {% if song.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.id %}">{{ song.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.mastodon_username %}">{{ song.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -159,43 +159,23 @@
                                 
                                 <h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
                                 {% if mark_list_more %}
-                                <a href="{% url 'music:retrieve_song_mark_list' song.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'music:retrieve_song_mark_list' song.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'music:retrieve_song_mark_list' song.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=song %}    
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
 
                                 {% if review_list_more %}
-                                <a href="{% url 'music:retrieve_song_review_list' song.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'music:retrieve_song_review_list' song.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
@@ -223,7 +203,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -235,7 +215,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -266,7 +246,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -295,7 +275,28 @@
 
                             {% endif %}
                         </div>
-                                
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'song' song.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if song.source_site == "spotify" %}
+                        <iframe src="{{ song.get_embed_link }}"  height="80" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
+                        {% endif %}
                     </div>
                 </div>
             </section>
@@ -357,8 +358,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html
index 6314df96..a5fcf18b 100644
--- a/music/templates/music/song_mark_list.html
+++ b/music/templates/music/song_mark_list.html
@@ -6,6 +6,7 @@
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load highlight %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -13,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ song.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -32,41 +33,9 @@
                         <div class="main-section-wrapper">
                             <div class="entity-marks">
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
-                                    <a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans '
-                                    的标记' %}
+                                    <a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans '的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-
-                                    {% for mark in marks %}
-
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
-                                                viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=song %}
                             </div>
                             <div class="pagination">
 
@@ -110,8 +79,7 @@
                                             {{ song.title }}
                                         </a>
                                         <a href="{{ song.source_url }}"><span
-                                                class="source-label source-label__{{ song.source_site }}">{{
-                                                song.get_source_site_display }}</span></a>
+                                                class="source-label source-label__{{ song.source_site }}">{{song.get_source_site_display }}</span></a>
                                     </h5>
 
                                     <div>{% if song.artist %}{% trans '艺术家:' %}
@@ -124,7 +92,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -159,12 +127,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html
index ee7c2704..e261fa91 100644
--- a/music/templates/music/song_review_detail.html
+++ b/music/templates/music/song_review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}乐评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ song.cover|thumb:'normal' }}">
+    <title>{{ site_name }}乐评 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -73,6 +74,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -101,7 +103,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -135,16 +137,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html
index b8078275..5926eba6 100644
--- a/music/templates/music/song_review_list.html
+++ b/music/templates/music/song_review_list.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ song.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -40,8 +40,8 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
@@ -108,7 +108,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -142,12 +142,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/urls.py b/music/urls.py
index 4acab46d..09b180ca 100644
--- a/music/urls.py
+++ b/music/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -9,6 +9,7 @@ urlpatterns = [
     path('song/update/<int:id>/', update_song, name='update_song'),
     path('song/delete/<int:id>/', delete_song, name='delete_song'),
     path('song/mark/', create_update_song_mark, name='create_update_song_mark'),
+    path('song/wish/<int:id>/', wish_song, name='wish_song'),
     path('song/<int:song_id>/mark/list/',
          retrieve_song_mark_list, name='retrieve_song_mark_list'),
     path('song/mark/delete/<int:id>/', delete_song_mark, name='delete_song_mark'),
@@ -16,18 +17,18 @@ urlpatterns = [
     path('song/review/update/<int:id>/', update_song_review, name='update_song_review'),
     path('song/review/delete/<int:id>/', delete_song_review, name='delete_song_review'),
     path('song/review/<int:id>/', retrieve_song_review, name='retrieve_song_review'),
-    path('song/<int:song_id>/review/list/',
-         retrieve_song_review_list, name='retrieve_song_review_list'),
+    re_path('song/(?P<song_id>[0-9]+)/mark/list/(?:(?P<following_only>\\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/<int:id>/', retrieve_album, name='retrieve_album'),
     path('album/update/<int:id>/', update_album, name='update_album'),
     path('album/delete/<int:id>/', delete_album, name='delete_album'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('album/mark/', create_update_album_mark, name='create_update_album_mark'),
-    path('album/<int:album_id>/mark/list/',
-         retrieve_album_mark_list, name='retrieve_album_mark_list'),
+    path('album/wish/<int:id>/', wish_album, name='wish_album'),
+    re_path('album/(?P<album_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_album_mark_list, name='retrieve_album_mark_list'),
     path('album/mark/delete/<int:id>/', delete_album_mark, name='delete_album_mark'),
     path('album/<int:album_id>/review/create/', create_album_review, name='create_album_review'),
     path('album/review/update/<int:id>/', update_album_review, name='update_album_review'),
diff --git a/music/views.py b/music/views.py
index 993dae35..af37c2f2 100644
--- a/music/views.py
+++ b/music/views.py
@@ -1,24 +1,24 @@
-# 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.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
 from common.utils import PageLinksGenerator
-from mastodon.utils import rating_to_emoji
-from mastodon.api import check_visibility, post_toot, TootVisibilityEnum
+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
+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__)
@@ -100,6 +100,7 @@ def update_song(request, id):
             '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
@@ -129,6 +130,7 @@ def update_song(request, id):
                 '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
@@ -187,6 +189,7 @@ def retrieve_song(request, id):
         else:
             mark_form = SongMarkForm(initial={
                 'song': song,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -206,10 +209,8 @@ def retrieve_song(request, id):
             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 = 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:
@@ -217,6 +218,7 @@ def retrieve_song(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(song=song)))
 
         # def strip_html_tags(text):
         #     import re
@@ -241,6 +243,7 @@ def retrieve_song(request, id):
                 'review_list_more': review_list_more,
                 'song_tag_list': song_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -285,12 +288,19 @@ def create_update_song_mark(request):
         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:
@@ -298,7 +308,7 @@ def create_update_song_mark(request):
             form = SongMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -326,28 +336,10 @@ def create_update_song_mark(request):
                 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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("music:retrieve_song", args=[form.instance.song.id]))
     else:
@@ -356,11 +348,30 @@ def create_update_song_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_song_mark_list(request, song_id):
+def wish_song(request, id):
+    if request.method == 'POST':
+        song = get_object_or_404(Song, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            'song': song,
+        }
+        try:
+            SongMark.objects.create(**params)
+        except Exception:
+            pass
+        return HttpResponse("✔️")
+    else:
+        return HttpResponseBadRequest("invalid method")
+
+
+@mastodon_request_included
+@login_required
+def retrieve_song_mark_list(request, song_id, following_only=False):
     if request.method == 'GET':
         song = get_object_or_404(Song, pk=song_id)
-        queryset = SongMark.get_available(
-            song, request.user, request.session['oauth_token'])
+        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)
@@ -421,23 +432,8 @@ def create_song_review(request, song_id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_song_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -473,23 +469,8 @@ def update_song_review(request, id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_song_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -524,11 +505,10 @@ def delete_song_review(request, id):
 
 
 @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):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -563,8 +543,7 @@ def retrieve_song_review(request, id):
 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'])
+        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)
@@ -661,6 +640,18 @@ def create_album(request):
         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':
@@ -672,6 +663,7 @@ def update_album(request, id):
             '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
@@ -701,6 +693,7 @@ def update_album(request, id):
                 '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
@@ -758,6 +751,7 @@ def retrieve_album(request, id):
         else:
             mark_form = AlbumMarkForm(initial={
                 'album': album,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -777,10 +771,8 @@ def retrieve_album(request, id):
             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 = 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:
@@ -788,6 +780,7 @@ def retrieve_album(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(album=album)))
 
         # def strip_html_tags(text):
         #     import re
@@ -812,6 +805,7 @@ def retrieve_album(request, id):
                 'review_list_more': review_list_more,
                 'album_tag_list': album_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -856,12 +850,19 @@ def create_update_album_mark(request):
         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:
@@ -869,7 +870,7 @@ def create_update_album_mark(request):
             form = AlbumMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -897,28 +898,10 @@ def create_update_album_mark(request):
                 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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("music:retrieve_album", args=[form.instance.album.id]))
     else:
@@ -927,11 +910,30 @@ def create_update_album_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_album_mark_list(request, album_id):
+def wish_album(request, id):
+    if request.method == 'POST':
+        album = get_object_or_404(Album, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            'album': album,
+        }
+        try:
+            AlbumMark.objects.create(**params)
+        except Exception:
+            pass
+        return HttpResponse("✔️")
+    else:
+        return HttpResponseBadRequest("invalid method")
+
+
+@mastodon_request_included
+@login_required
+def retrieve_album_mark_list(request, album_id, following_only=False):
     if request.method == 'GET':
         album = get_object_or_404(Album, pk=album_id)
-        queryset = AlbumMark.get_available(
-            album, request.user, request.session['oauth_token'])
+        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)
@@ -992,23 +994,8 @@ def create_album_review(request, album_id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_album_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -1044,23 +1031,8 @@ def update_album_review(request, id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_album_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -1095,11 +1067,10 @@ def delete_album_review(request, id):
 
 
 @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):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -1134,8 +1105,7 @@ def retrieve_album_review(request, id):
 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'])
+        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)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..d3177ab4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,26 @@
+dateparser
+django~=3.2.14
+django-hstore
+django-markdownx @ git+https://github.com/alphatownsman/django-markdownx.git@e69480c64ad9c5d0499f4a8625da78cf2bb7691b
+django-sass
+django-rq
+django-simple-history
+django-hijack
+django-user-messages
+django-slack
+meilisearch
+easy-thumbnails
+lxml
+openpyxl
+psycopg2
+requests
+filetype
+setproctitle
+tqdm
+opencc
+dnspython
+typesense
+markdownify
+sentry-sdk
+gitpython
+igdb-api-v4
diff --git a/sync/apps.py b/sync/apps.py
index 6e952eab..84c86ccf 100644
--- a/sync/apps.py
+++ b/sync/apps.py
@@ -1,9 +1,6 @@
 from django.apps import AppConfig
+from django.conf import settings
 
 
 class SyncConfig(AppConfig):
     name = 'sync'
-
-    def ready(self):
-        from sync.jobs import sync_task_manager
-        sync_task_manager.start()
\ No newline at end of file
diff --git a/sync/jobs.py b/sync/jobs.py
index 0b43c902..e2ce4759 100644
--- a/sync/jobs.py
+++ b/sync/jobs.py
@@ -1,12 +1,8 @@
 import logging
 import pytz
-import signal
-import sys
-import queue
-import threading
-import time
 from dataclasses import dataclass
 from datetime import datetime
+from django.conf import settings
 from django.utils import timezone
 from django.core.exceptions import ObjectDoesNotExist
 from openpyxl import load_workbook
@@ -18,69 +14,17 @@ from common.scraper import DoubanAlbumScraper, DoubanBookScraper, DoubanGameScra
 from common.models import MarkStatusEnum
 from .models import SyncTask
 
-__all__ = ['sync_task_manager']
 
 logger = logging.getLogger(__name__)
 
 
-class SyncTaskManger:
+def __import_should_stop():
+    # TODO: using queue.connection.set(job.key + b':should_stop', 1, ex=30) on the caller side and connection.get(job.key + b':should_stop') on the worker side.
+    pass
 
-    # in seconds
-    __CHECK_NEW_TASK_TIME_INTERVAL = 0.05
-    MAX_WORKERS = 256
 
-    def __init__(self):
-        self.__task_queue = queue.Queue(0)
-        self.__stop_event = threading.Event()
-        self.__worker_threads = []
-
-    def __listen_for_new_task(self):
-        while not self.__stop_event.is_set():
-            time.sleep(self.__CHECK_NEW_TASK_TIME_INTERVAL)
-            while not self.__task_queue.empty() and not self.is_full():
-                task = self.__task_queue.get_nowait()
-                self.__start_new_worker(task)
-
-    def __start_new_worker(self, task):
-        new_worker = threading.Thread(
-            target=sync_doufen_job, args=[task, self.is_stopped], daemon=True
-        )
-        self.__worker_threads.append(new_worker)
-        new_worker.start()
-
-    def __enqueue_existing_tasks(self):
-        for task in SyncTask.objects.filter(is_finished=False):
-            self.__task_queue.put_nowait(task)
-
-    def is_full(self):
-        return len(self.__worker_threads) >= self.MAX_WORKERS
-
-    def add_task(self, task):
-        self.__task_queue.put_nowait(task)
-
-    def stop(self, signum, frame):
-        print('rceived signal ', signum)
-        logger.info(f'rceived signal {signum}')
-
-        self.__stop_event.set()
-        # for worker_thread in self.__worker_threads:
-        #     worker_thread.join()
-
-        print("stopped")
-        logger.info(f'stopped')
-
-    def is_stopped(self):
-        return self.__stop_event.is_set()
-
-    def start(self):
-        self.__enqueue_existing_tasks()  # enqueue
-
-        listen_new_task_thread = threading.Thread(
-            target=self.__listen_for_new_task, daemon=True)
-
-        self.__worker_threads.append(listen_new_task_thread)
-
-        listen_new_task_thread.start()
+def import_doufen_task(synctask):
+    sync_doufen_job(synctask, __import_should_stop)
 
 
 class DoufenParser:
@@ -96,7 +40,7 @@ class DoufenParser:
         self.__file_path = task.file.path
         self.__progress_sheet, self.__progress_row = task.get_breakpoint()
         self.__is_new_task = True
-        if not self.__progress_sheet is None:
+        if self.__progress_sheet is not None:
             self.__is_new_task = False
         if self.__progress_row is None:
             self.__progress_row = 2
@@ -157,10 +101,14 @@ class DoufenParser:
 
         is_first_sheet = True
         for mapping in item_classes_mappings:
+            if mapping['sheet'] not in self.__wb:
+                print(f"Sheet not found: {mapping['sheet']}")
+                continue
             ws = self.__wb[mapping['sheet']]
 
+            max_row = ws.max_row
             # empty sheet
-            if ws.max_row <= 1:
+            if max_row <= 1:
                 continue
 
             # decide starting position
@@ -169,29 +117,26 @@ class DoufenParser:
                 start_row_index = self.__progress_row
 
             # parse data
-            for i in range(start_row_index, ws.max_row + 1):
-                # url definitely exists
-                url = ws.cell(row=i, column=self.URL_INDEX).value
-
-                tags = ws.cell(row=i, column=self.TAG_INDEX).value
-                tags = tags.split(',') if tags else None
-
-                time = ws.cell(row=i, column=self.TIME_INDEX).value
-                if time:
+            tz = pytz.timezone('Asia/Shanghai')
+            i = start_row_index
+            for row in ws.iter_rows(min_row=start_row_index, max_row=max_row, values_only=True):
+                cells = [cell for cell in row]
+                url = cells[self.URL_INDEX - 1]
+                tags = cells[self.TAG_INDEX - 1]
+                tags = list(set(tags.lower().split(','))) if tags else None
+                time = cells[self.TIME_INDEX - 1]
+                if time and type(time) == str:
                     time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S")
-                    tz = pytz.timezone('Asia/Shanghai')
+                    time = time.replace(tzinfo=tz)
+                elif time and type(time) == datetime:
                     time = time.replace(tzinfo=tz)
                 else:
                     time = None
-
-                content = ws.cell(row=i, column=self.CONTENT_INDEX).value
+                content = cells[self.CONTENT_INDEX - 1]
                 if not content:
                     content = ""
-
-                rating = ws.cell(row=i, column=self.RATING_INDEX).value
+                rating = cells[self.RATING_INDEX - 1]
                 rating = int(rating) * 2 if rating else None
-
-                # store result
                 self.items.append({
                     'data': DoufenRowData(url, tags, time, content, rating),
                     'entity_class': mapping['entity_class'],
@@ -201,18 +146,20 @@ class DoufenParser:
                     'sheet': mapping['sheet'],
                     'row_index': i,
                 })
+                i = i + 1
 
             # set first sheet flag
             is_first_sheet = False
 
     def __get_item_number(self):
-        assert not self.__wb is None, 'workbook not found'
-        assert not self.__mappings is None, 'mappings not found'
+        assert self.__wb is not None, 'workbook not found'
+        assert self.__mappings is not None, 'mappings not found'
 
         sheets = [mapping['sheet'] for mapping in self.__mappings]
         item_number = 0
         for sheet in sheets:
-            item_number += self.__wb[sheet].max_row - 1
+            if sheet in self.__wb:
+                item_number += self.__wb[sheet].max_row - 1
 
         return item_number
 
@@ -229,13 +176,12 @@ class DoufenParser:
                 self.__update_total_items()
             self.__close_file()
             return self.items
-
         except Exception as e:
-            logger.error(e)
-            raise e
-
+            logger.error(f'Error parsing {self.__file_path} {e}')
+            self.task.is_failed = True
         finally:
             self.__close_file()
+        return []
 
 
 @dataclass
@@ -247,7 +193,7 @@ class DoufenRowData:
     rating: int
 
 
-def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet, is_private):
+def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet, default_public):
     params = {
         'owner': user,
         'created_time': data.time,
@@ -255,7 +201,7 @@ def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet,
         'rating': data.rating,
         'text': data.content,
         'status': translate_status(sheet),
-        'is_private': not is_private,
+        'visibility': 0 if default_public else 1,
         entity_class.__name__.lower(): entity,
     }
     mark = mark_class.objects.create(**params)
@@ -267,12 +213,15 @@ def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet,
                 entity_class.__name__.lower(): entity,
                 'mark': mark
             }
-            tag_class.objects.create(**params)
+            try:
+                tag_class.objects.create(**params)
+            except Exception as e:
+                logger.error(f'Error creating tag {tag} {mark}: {e}')
 
 
 def overwrite_mark(entity, entity_class, mark, mark_class, tag_class, data, sheet):
     old_rating = mark.rating
-    old_tags = getattr(mark, mark_class.__name__.lower()+'_tags').all()
+    old_tags = getattr(mark, mark_class.__name__.lower() + '_tags').all()
     # update mark logic
     mark.created_time = data.time
     mark.edited_time = data.time
@@ -291,7 +240,10 @@ def overwrite_mark(entity, entity_class, mark, mark_class, tag_class, data, shee
                 entity_class.__name__.lower(): entity,
                 'mark': mark
             }
-            tag_class.objects.create(**params)
+            try:
+                tag_class.objects.create(**params)
+            except Exception as e:
+                logger.error(f'Error creating tag {tag} {mark}: {e}')
 
 
 def sync_doufen_job(task, stop_check_func):
@@ -302,6 +254,7 @@ def sync_doufen_job(task, stop_check_func):
     if task.is_finished:
         return
 
+    print(f'Task {task.pk}: loading')
     parser = DoufenParser(task)
     items = parser.parse()
 
@@ -322,15 +275,17 @@ def sync_doufen_job(task, stop_check_func):
         # scrape the entity if not exists
         try:
             entity = entity_class.objects.get(source_url=data.url)
+            print(f'Task {task.pk}: {len(items)+1} remaining; matched {data.url}')
         except ObjectDoesNotExist:
             try:
+                print(f'Task {task.pk}: {len(items)+1} remaining; scraping {data.url}')
                 scraper.scrape(data.url)
                 form = scraper.save(request_user=task.user)
                 entity = form.instance
             except Exception as e:
-                logger.error(f"Scrape Failed URL: {data.url}")
-                logger.error(
-                    "Expections during scraping data:", exc_info=e)
+                logger.error(f"Task {task.pk}: scrape failed: {data.url} {e}")
+                if settings.DEBUG:
+                    logger.error("Expections during scraping data:", exc_info=e)
                 task.failed_urls.append(data.url)
                 task.finished_items += 1
                 task.save(update_fields=['failed_urls', 'finished_items'])
@@ -360,7 +315,7 @@ def sync_doufen_job(task, stop_check_func):
 
         except Exception as e:
             logger.error(
-                "Unknown exception when syncing marks", exc_info=e)
+                f"Task {task.pk}: error when syncing marks", exc_info=e)
             task.failed_urls.append(data.url)
             task.finished_items += 1
             task.save(update_fields=['failed_urls', 'finished_items'])
@@ -371,6 +326,7 @@ def sync_doufen_job(task, stop_check_func):
         task.save(update_fields=['success_items', 'finished_items'])
 
     # if task finish
+    print(f'Task {task.pk}: stopping')
     if len(items) == 0:
         task.is_finished = True
         task.clear_breakpoint()
@@ -386,13 +342,3 @@ def translate_status(sheet_name):
         return MarkStatusEnum.COLLECT
 
     raise ValueError("Not valid status")
-
-
-sync_task_manager = SyncTaskManger()
-
-# sync_task_manager.start()
-
-signal.signal(signal.SIGTERM, sync_task_manager.stop)
-if sys.platform.startswith('linux'):
-    signal.signal(signal.SIGHUP, sync_task_manager.stop)
-signal.signal(signal.SIGINT, sync_task_manager.stop)
diff --git a/sync/management/commands/resync.py b/sync/management/commands/resync.py
new file mode 100644
index 00000000..a5a0a73c
--- /dev/null
+++ b/sync/management/commands/resync.py
@@ -0,0 +1,91 @@
+from django.core.management.base import BaseCommand
+from common.scraper import get_scraper_by_url, get_normalized_url
+import pprint
+from sync.models import SyncTask
+from users.models import User
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
+from tqdm import tqdm
+from django.conf import settings
+import requests
+import os
+
+
+class Command(BaseCommand):
+    help = 'Re-scrape failed urls (via local proxy)'
+
+    def add_arguments(self, parser):
+        parser.add_argument('action', type=str, help='list/download')
+
+    def handle(self, *args, **options):
+        if options['action'] == 'list':
+            self.do_list()
+        else:
+            self.do_download()
+
+    def do_list(self):
+        tasks = SyncTask.objects.filter(failed_urls__isnull=False)
+        urls = []
+        for task in tqdm(tasks):
+            for url in task.failed_urls:
+                if url not in urls and url not in urls:
+                    url = get_normalized_url(str(url))
+                    scraper = get_scraper_by_url(url)
+                    if scraper is not None:
+                        try:
+                            url = scraper.get_effective_url(url)
+                            entity = scraper.data_class.objects.get(source_url=url)
+                        except ObjectDoesNotExist:
+                            urls.append(url)
+        f = open("/tmp/resync_todo.txt", "w")
+        f.write("\n".join(urls))
+        f.close()
+
+    def do_download(self):
+        self.stdout.write(f'Checking local proxy...{settings.LOCAL_PROXY}')
+        url = f'{settings.LOCAL_PROXY}?url=https://www.douban.com/doumail/'
+        try:
+            r = requests.get(url, timeout=settings.SCRAPING_TIMEOUT)
+        except Exception as e:
+            self.stdout.write(self.style.ERROR(e))
+            return
+        content = r.content.decode('utf-8')
+        if content.find('我的豆邮') == -1:
+            self.stdout.write(self.style.ERROR(f'Proxy check failed.'))
+            return
+
+        self.stdout.write(f'Loading urls...')
+        with open("/tmp/resync_todo.txt") as file:
+            todos = file.readlines()
+            todos = [line.strip() for line in todos]
+        with open("/tmp/resync_success.txt") as file:
+            skips = file.readlines()
+            skips = [line.strip() for line in skips]
+        f_f = open("/tmp/resync_failed.txt", "a")
+        f_i = open("/tmp/resync_ignore.txt", "a")
+        f_s = open("/tmp/resync_success.txt", "a")
+        user = User.objects.get(id=1)
+
+        for url in tqdm(todos):
+            scraper = get_scraper_by_url(url)
+            url = scraper.get_effective_url(url)
+            if url in skips:
+                self.stdout.write(f'Skip {url}')
+            elif scraper is None:
+                self.stdout.write(self.style.ERROR(f'Unable to find scraper for {url}'))
+                f_i.write(url + '\n')
+            else:
+                try:
+                    entity = scraper.data_class.objects.get(source_url=url)
+                    f_i.write(url + '\n')
+                except ObjectDoesNotExist:
+                    try:
+                        # self.stdout.write(f'Fetching {url} via {scraper.__name__}')
+                        scraper.scrape(url)
+                        form = scraper.save(request_user=user)
+                        f_s.write(url + '\n')
+                        f_s.flush()
+                        os.fsync(f_s.fileno())
+                        self.stdout.write(self.style.SUCCESS(f'Saved {url}'))
+                    except Exception as e:
+                        f_f.write(url + '\n')
+                        self.stdout.write(self.style.ERROR(f'Error {url}'))
diff --git a/sync/models.py b/sync/models.py
index e5264477..ae3993e7 100644
--- a/sync/models.py
+++ b/sync/models.py
@@ -1,13 +1,13 @@
 from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 import django.contrib.postgres.fields as postgres
 from users.models import User
-from boofilsic.settings import SYNC_FILE_PATH_ROOT
 from common.utils import GenerateDateUUIDMediaFilePath
+from django.conf import settings
 
 
 def sync_file_path(instance, filename):
-    return GenerateDateUUIDMediaFilePath(instance, filename, SYNC_FILE_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.SYNC_FILE_PATH_ROOT)
 
 
 class SyncTask(models.Model):
@@ -69,7 +69,7 @@ class SyncTask(models.Model):
 
     def __str__(self):
         """Unicode representation of SyncTask."""
-        return str(self.user.username) + '@' + str(self.started_time) + self.get_status_emoji()
+        return f'{self.id} {self.user} {self.file} {self.get_status_emoji()} {self.success_items}/{self.finished_items}/{self.total_items}'
 
     def get_status_emoji(self):
         return ("❌" if self.is_failed else "✔") if self.is_finished else "⚡"
diff --git a/sync/views.py b/sync/views.py
index 69aa7de6..aecd96e8 100644
--- a/sync/views.py
+++ b/sync/views.py
@@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required
 from django.http import HttpResponseBadRequest, JsonResponse, HttpResponse
 from .models import SyncTask
 from .forms import SyncTaskForm
-from .jobs import sync_task_manager
+from .jobs import import_doufen_task
 import tempfile
 import os
 from threading import Thread
@@ -11,6 +11,7 @@ import openpyxl
 from django.utils.datastructures import MultiValueDictKeyError
 from openpyxl.utils.exceptions import InvalidFileException
 from zipfile import BadZipFile
+import django_rq
 
 
 @login_required
@@ -25,7 +26,7 @@ def sync_douban(request):
             wb = openpyxl.open(uploaded_file, read_only=True,
                                data_only=True, keep_links=False)
             wb.close()
-        except (MultiValueDictKeyError, InvalidFileException, BadZipFile) as e :
+        except (MultiValueDictKeyError, InvalidFileException, BadZipFile) as e:
             # raise e
             return HttpResponseBadRequest(content="invalid excel file")
 
@@ -35,8 +36,7 @@ def sync_douban(request):
             # stop all preivous task
             SyncTask.objects.filter(user=request.user, is_finished=False).update(is_finished=True)
             form.save()
-            sync_task_manager.add_task(form.instance)
-
+            django_rq.get_queue('doufen').enqueue(import_doufen_task, form.instance, job_id=f'SyncTask_{form.instance.id}')
             return HttpResponse(status=204)
         else:
             return HttpResponseBadRequest()
@@ -55,6 +55,7 @@ def query_progress(request):
         return JsonResponse()
 
 
+@login_required
 def query_last_task(request):
     task = request.user.user_synctasks.order_by('-id').first()
     if task is not None:
diff --git a/timeline/__init__.py b/timeline/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/timeline/admin.py b/timeline/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/timeline/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/timeline/apps.py b/timeline/apps.py
new file mode 100644
index 00000000..00df970d
--- /dev/null
+++ b/timeline/apps.py
@@ -0,0 +1,15 @@
+from django.apps import AppConfig
+
+
+class TimelineConfig(AppConfig):
+    name = 'timeline'
+
+    def ready(self):
+        from .models import init_post_save_handler
+        from books.models import BookMark, BookReview
+        from movies.models import MovieMark, MovieReview
+        from games.models import GameMark, GameReview
+        from music.models import AlbumMark, AlbumReview, SongMark, SongReview
+        from collection.models import Collection, CollectionMark
+        for m in [BookMark, BookReview, MovieMark, MovieReview, GameMark, GameReview, AlbumMark, AlbumReview, SongMark, SongReview, Collection, CollectionMark]:
+            init_post_save_handler(m)
diff --git a/timeline/management/commands/regen_activity.py b/timeline/management/commands/regen_activity.py
new file mode 100644
index 00000000..4dbb702c
--- /dev/null
+++ b/timeline/management/commands/regen_activity.py
@@ -0,0 +1,20 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+from timeline.models import Activity
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, AlbumReview, SongMark, SongReview
+from collection.models import Collection, CollectionMark
+from tqdm import tqdm
+
+
+class Command(BaseCommand):
+    help = 'Re-populating activity for timeline'
+
+    def handle(self, *args, **options):
+        for cl in [BookMark, BookReview, MovieMark, MovieReview, GameMark, GameReview, AlbumMark, AlbumReview, SongMark, SongReview, Collection, CollectionMark]:
+            for a in tqdm(cl.objects.filter(created_time__gt='2022-1-1 00:00+0800'), desc=f'Populating {cl.__name__}'):
+                Activity.upsert_item(a)
diff --git a/timeline/models.py b/timeline/models.py
new file mode 100644
index 00000000..d7959b04
--- /dev/null
+++ b/timeline/models.py
@@ -0,0 +1,63 @@
+from django.db import models
+from common.models import UserOwnedEntity
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, AlbumReview, SongMark, SongReview
+from collection.models import Collection, CollectionMark
+from django.db.models.signals import post_save, post_delete
+
+
+class Activity(UserOwnedEntity):
+    bookmark = models.ForeignKey(BookMark, models.CASCADE, null=True)
+    bookreview = models.ForeignKey(BookReview, models.CASCADE, null=True)
+    moviemark = models.ForeignKey(MovieMark, models.CASCADE, null=True)
+    moviereview = models.ForeignKey(MovieReview, models.CASCADE, null=True)
+    gamemark = models.ForeignKey(GameMark, models.CASCADE, null=True)
+    gamereview = models.ForeignKey(GameReview, models.CASCADE, null=True)
+    albummark = models.ForeignKey(AlbumMark, models.CASCADE, null=True)
+    albumreview = models.ForeignKey(AlbumReview, models.CASCADE, null=True)
+    songmark = models.ForeignKey(SongMark, models.CASCADE, null=True)
+    songreview = models.ForeignKey(SongReview, models.CASCADE, null=True)
+    collection = models.ForeignKey(Collection, models.CASCADE, null=True)
+    collectionmark = models.ForeignKey(CollectionMark, models.CASCADE, null=True)
+
+    @property
+    def target(self):
+        items = [self.bookmark, self.bookreview, self.moviemark, self.moviereview, self.gamemark, self.gamereview,
+                 self.songmark, self.songreview, self.albummark, self.albumreview, self.collection, self.collectionmark]
+        return next((x for x in items if x is not None), None)
+
+    @property
+    def mark(self):
+        items = [self.bookmark, self.moviemark, self.gamemark, self.songmark, self.albummark]
+        return next((x for x in items if x is not None), None)
+
+    @property
+    def review(self):
+        items = [self.bookreview, self.moviereview, self.gamereview, self.songreview, self.albumreview]
+        return next((x for x in items if x is not None), None)
+
+    @classmethod
+    def upsert_item(self, item):
+        attr = item.__class__.__name__.lower()
+        f = {'owner': item.owner, attr: item}
+        activity = Activity.objects.filter(**f).first()
+        if not activity:
+            activity = Activity.objects.create(**f)
+        activity.created_time = item.created_time
+        activity.visibility = item.visibility
+        activity.save()
+
+
+def _post_save_handler(sender, instance, created, **kwargs):
+    Activity.upsert_item(instance)
+
+
+# def activity_post_delete_handler(sender, instance, **kwargs):
+#     pass
+
+
+def init_post_save_handler(model):
+    post_save.connect(_post_save_handler, sender=model)
+    # post_delete.connect(activity_post_delete_handler, sender=model)  # delete handled by database
diff --git a/timeline/templates/timeline.html b/timeline/templates/timeline.html
new file mode 100644
index 00000000..17e89fd8
--- /dev/null
+++ b/timeline/templates/timeline.html
@@ -0,0 +1,83 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }}</title>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script>
+        $(document).ready( function() {
+        let render = function() {
+            let ratingLabels = $(".rating-star");
+            $(ratingLabels).each( function(index, value) {
+                let ratingScore = $(this).data("rating-score") / 2;
+                $(this).starRating({
+                    initialRating: ratingScore,
+                    readOnly: true,
+                    starSize: 16,
+                });
+            });
+        };
+        document.body.addEventListener('htmx:load', function(evt) {
+            render();
+        });
+        render();
+        });
+    </script>
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content" class="container">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            <div class="entity-list">
+
+                                <!-- <div class="set">
+                                    <h5 class="entity-list__title">
+                                        我的时间轴
+                                    </h5>
+                                </div> -->
+                                <ul class="entity-list__entities">
+                                    <div hx-get="{% url 'timeline:data' %}" hx-trigger="revealed" hx-swap="outerHTML"></div>
+                                </ul>
+                            </div>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+
+{% if unread_announcements %}
+{% include "partial/_announcement.html" %}
+{% endif %}
+</body>
+</html>
diff --git a/timeline/templates/timeline_data.html b/timeline/templates/timeline_data.html
new file mode 100644
index 00000000..5b014f39
--- /dev/null
+++ b/timeline/templates/timeline_data.html
@@ -0,0 +1,124 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+{% load neo %}
+
+{% for activity in activities %}
+{% current_user_marked_item activity.target.item as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{{ activity.target.item.url }}">
+            <img src="{{ activity.target.item.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img" style="min-width:80px;max-width:80px">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{{ activity.target.item.wish_url }}">➕</a>
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        <div class="collection-item-position-edit">
+            <span class="entity-marks__mark-time">
+                {% if activity.target.shared_link %}
+                <a href="{{ activity.target.shared_link }}" target="_blank">
+                    <img src="{% static 'img/fediverse.svg' %}" style="filter: invert(93%) sepia(1%) saturate(53%) hue-rotate(314deg) brightness(95%) contrast(80%); vertical-align:text-top; max-width:14px; margin-right:6px;" />
+                    <span class="entity-marks__mark-time">{{ activity.target.created_time|prettydate }}</span></a>
+                {% else %}
+                <a><span class="entity-marks__mark-time">{{ activity.target.created_time|prettydate }}</span></a>
+                {% endif %}
+            </span>
+        </div>
+        <span class="entity-list__entity-info" style="top:0px;">
+            <a href="{% url 'users:home' activity.owner.mastodon_username %}">{{ activity.owner.display_name }}</a> {{ activity.target.translated_status }}
+        </span>
+        <div class="entity-list__entity-title">
+            <a href="{{ activity.target.item.url }}" class="entity-list__entity-link" style="font-weight:bold;">{{ activity.target.item.title }}
+            {% if activity.target.item.year %}<small style="font-weight: lighter">({{ activity.target.item.year }})</small>{% endif %}
+            </a>
+            {% if activity.target.item.source_url %}
+            <a href="{{ activity.target.item.source_url }}">
+                <span class="source-label source-label__{{ activity.target.item.source_site }}" style="font-size:xx-small;">{{ activity.target.item.get_source_site_display }}</span>
+            </a>
+            {% endif %}
+        </div>
+        <p class="entity-list__entity-brief">
+            {% if activity.review %}
+                <a href="{{ activity.review.url }}">{{ activity.review.title }}</a>
+            {% endif %}
+            {% if activity.mark %}
+                {% if activity.mark.rating %}
+                <span class="entity-marks__rating-star rating-star" data-rating-score="{{ activity.mark.rating | floatformat:"0" }}" style=""></span>
+                {% endif %}
+
+                {% if activity.mark.text %}
+                <p class="entity-marks__mark-content">{{ activity.mark.text }}</p>
+                {% endif %}
+            {% endif %}
+        </p>
+    </div>
+</li>
+{% if forloop.last %}
+<div class="htmx-indicator" style="margin-left: 60px;" 
+    hx-get="{% url 'timeline:data' %}?last={{ activity.created_time|date:'Y-m-d H:i:s.uO'|urlencode }}"
+    hx-trigger="revealed"
+    hx-swap="outerHTML">
+<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#ccc">
+    <rect y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.5s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.5s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="30" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.25s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.25s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="60" width="15" height="140" rx="6">
+        <animate attributeName="height"
+             begin="0s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="90" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.25s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.25s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="120" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.5s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.5s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+</svg>
+</div>
+{% endif %}
+{% empty %}
+<div>{% trans '目前没有更多内容了' %}</div>
+{% endfor %}
\ No newline at end of file
diff --git a/timeline/tests.py b/timeline/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/timeline/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/timeline/urls.py b/timeline/urls.py
new file mode 100644
index 00000000..b501f1e8
--- /dev/null
+++ b/timeline/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path, re_path
+from .views import *
+
+
+app_name = 'timeline'
+urlpatterns = [
+    path('', timeline, name='timeline'),
+    path('data', data, name='data'),
+]
diff --git a/timeline/views.py b/timeline/views.py
new file mode 100644
index 00000000..0d9142e0
--- /dev/null
+++ b/timeline/views.py
@@ -0,0 +1,71 @@
+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
+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 post_toot, TootVisibilityEnum
+from common.utils import PageLinksGenerator
+from .models import *
+from books.models import BookTag
+from movies.models import MovieTag
+from games.models import GameTag
+from music.models import AlbumTag
+from django.conf import settings
+import re
+from users.models import User
+from django.http import HttpResponseRedirect
+from django.db.models import Q
+import time
+from management.models import Announcement
+
+
+logger = logging.getLogger(__name__)
+mastodon_logger = logging.getLogger("django.mastodon")
+PAGE_SIZE = 20
+
+
+@login_required
+def timeline(request):
+    if request.method != 'GET':
+        return
+    user = request.user
+    unread = Announcement.objects.filter(pk__gt=user.read_announcement_index).order_by('-pk')
+    if unread:
+        user.read_announcement_index = Announcement.objects.latest('pk').pk
+        user.save(update_fields=['read_announcement_index'])
+    return render(
+        request,
+        'timeline.html',
+        {
+            'book_tags': BookTag.all_by_user(user)[:10],
+            'movie_tags': MovieTag.all_by_user(user)[:10],
+            'music_tags': AlbumTag.all_by_user(user)[:10],
+            'game_tags': GameTag.all_by_user(user)[:10],
+            'unread_announcements': unread,
+        }
+    )
+
+
+@login_required
+def data(request):
+    if request.method != 'GET':
+        return
+    q = Q(owner_id__in=request.user.following, visibility__lt=2) | Q(owner_id=request.user.id)
+    last = request.GET.get('last')
+    if last:
+        q = q & Q(created_time__lt=last)
+    activities = Activity.objects.filter(q).order_by('-created_time')[:PAGE_SIZE]
+    return render(
+        request,
+        'timeline_data.html',
+        {
+            'activities': activities,
+        }
+    )
diff --git a/users/account.py b/users/account.py
new file mode 100644
index 00000000..6bc38e71
--- /dev/null
+++ b/users/account.py
@@ -0,0 +1,255 @@
+from django.shortcuts import reverse, redirect, render, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import auth
+from django.contrib.auth import authenticate
+from django.core.paginator import Paginator
+from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Count
+from .models import User, Report, Preference
+from .forms import ReportForm
+from mastodon.api import *
+from mastodon import mastodon_request_included
+from common.config import *
+from common.models import MarkStatusEnum
+from common.utils import PageLinksGenerator
+from management.models import Announcement
+from books.models import *
+from movies.models import *
+from music.models import *
+from games.models import *
+from books.forms import BookMarkStatusTranslator
+from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
+from games.forms import GameMarkStatusTranslator
+from mastodon.models import MastodonApplication
+from mastodon.api import verify_account
+from django.conf import settings
+from urllib.parse import quote
+import django_rq
+from .account import *
+from .tasks import *
+from datetime import timedelta
+from django.utils import timezone
+import json
+from django.contrib import messages
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, SongMark, AlbumReview, SongReview
+from collection.models import Collection, CollectionMark
+from common.importers.goodreads import GoodreadsImporter
+from common.importers.douban import DoubanImporter
+
+
+# the 'login' page that user can see
+def login(request):
+    if request.method == 'GET':
+        selected_site = request.GET.get('site', default='')
+
+        sites = MastodonApplication.objects.all().order_by("domain_name")
+
+        # store redirect url in the cookie
+        if request.GET.get('next'):
+            request.session['next_url'] = request.GET.get('next')
+
+        return render(
+            request,
+            'users/login.html',
+            {
+                'sites': sites,
+                'scope': quote(settings.MASTODON_CLIENT_SCOPE),
+                'selected_site': selected_site,
+                'allow_any_site': settings.MASTODON_ALLOW_ANY_SITE,
+            }
+        )
+    else:
+        return HttpResponseBadRequest()
+
+
+# connect will redirect to mastodon server
+def connect(request):
+    login_domain = request.session['swap_domain'] if request.session.get('swap_login') else request.GET.get('domain')
+    if not login_domain:
+        return render(request, 'common/error.html', {'msg': '未指定实例域名', 'secondary_msg': "", })
+    login_domain = login_domain.strip().lower().split('//')[-1].split('/')[0].split('@')[-1]
+    domain, version = get_instance_info(login_domain)
+    app, error_msg = get_mastodon_application(domain)
+    if app is None:
+        return render(request, 'common/error.html', {'msg': error_msg, 'secondary_msg': "", })
+    else:
+        login_url = get_mastodon_login_url(app, login_domain, version, request)
+        resp = redirect(login_url)
+        resp.set_cookie('mastodon_domain', domain)
+        return resp
+
+
+# mastodon server redirect back to here
+@mastodon_request_included
+def OAuth2_login(request):
+    if request.method != 'GET':
+        return HttpResponseBadRequest()
+
+    code = request.GET.get('code')
+    site = request.COOKIES.get('mastodon_domain')
+    try:
+        token, refresh_token = obtain_token(site, request, code)
+    except ObjectDoesNotExist:
+        return HttpResponseBadRequest("Mastodon site not registered")
+    if not token:
+        return render(
+            request,
+            'common/error.html',
+            {
+                'msg': _("认证失败😫")
+            }
+        )
+
+    if request.session.get('swap_login', False) and request.user.is_authenticated:  # swap login for existing user
+        return swap_login(request, token, site, refresh_token)
+
+    user = authenticate(request, token=token, site=site)
+    if user:  # existing user
+        user.mastodon_token = token
+        user.mastodon_refresh_token = refresh_token
+        user.save(update_fields=['mastodon_token', 'mastodon_refresh_token'])
+        auth_login(request, user)
+        if request.session.get('next_url') is not None:
+            response = redirect(request.session.get('next_url'))
+            del request.session['next_url']
+        else:
+            response = redirect(reverse('common:home'))
+        return response
+    else:  # newly registered user
+        code, user_data = verify_account(site, token)
+        if code != 200 or user_data is None:
+            return render(
+                request,
+                'common/error.html',
+                {
+                    'msg': _("联邦网络访问失败😫")
+                }
+            )
+        new_user = User(
+            username=user_data['username'],
+            mastodon_id=user_data['id'],
+            mastodon_site=site,
+            mastodon_token=token,
+            mastodon_refresh_token=refresh_token,
+            mastodon_account=user_data,
+        )
+        new_user.save()
+        Preference.objects.create(user=new_user)
+        request.session['new_user'] = True
+        auth_login(request, new_user)
+        return redirect(reverse('users:register'))
+
+
+@mastodon_request_included
+@login_required
+def logout(request):
+    if request.method == 'GET':
+        # revoke_token(request.user.mastodon_site, request.user.mastodon_token)
+        auth_logout(request)
+        return redirect(reverse("users:login"))
+    else:
+        return HttpResponseBadRequest()
+
+
+@mastodon_request_included
+@login_required
+def reconnect(request):
+    if request.method == 'POST':
+        request.session['swap_login'] = True
+        request.session['swap_domain'] = request.POST['domain']
+        return connect(request)
+    else:
+        return HttpResponseBadRequest()
+
+
+@mastodon_request_included
+def register(request):
+    if request.session.get('new_user'):
+        del request.session['new_user']
+        return render(request, 'users/register.html')
+    else:
+        return redirect(reverse('common:home'))
+
+
+def swap_login(request, token, site, refresh_token):
+    del request.session['swap_login']
+    del request.session['swap_domain']
+    code, data = verify_account(site, token)
+    current_user = request.user
+    if code == 200 and data is not None:
+        username = data['username']
+        if username == current_user.username and site == current_user.mastodon_site:
+            messages.add_message(request, messages.ERROR, _(f'该身份 {username}@{site} 与当前账号相同。'))
+        else:
+            try:
+                existing_user = User.objects.get(username=username, mastodon_site=site)
+                messages.add_message(request, messages.ERROR, _(f'该身份 {username}@{site} 已被用于其它账号。'))
+            except ObjectDoesNotExist:
+                current_user.username = username
+                current_user.mastodon_id = data['id']
+                current_user.mastodon_site = site
+                current_user.mastodon_token = token
+                current_user.mastodon_refresh_token = refresh_token
+                current_user.mastodon_account = data
+                current_user.save(update_fields=['username', 'mastodon_id', 'mastodon_site', 'mastodon_token', 'mastodon_refresh_token', 'mastodon_account'])
+                django_rq.get_queue('mastodon').enqueue(refresh_mastodon_data_task, current_user, token)
+                messages.add_message(request, messages.INFO, _(f'账号身份已更新为 {username}@{site}。'))
+    else:
+        messages.add_message(request, messages.ERROR, _('连接联邦网络获取身份信息失败。'))
+    return redirect(reverse('users:data'))
+
+
+def auth_login(request, user):
+    """ Decorates django ``login()``. Attach token to session."""
+    auth.login(request, user)
+    if user.mastodon_last_refresh < timezone.now() - timedelta(hours=1) or user.mastodon_account == {}:
+        django_rq.get_queue('mastodon').enqueue(refresh_mastodon_data_task, user)
+
+
+def auth_logout(request):
+    """ Decorates django ``logout()``. Release token in session."""
+    auth.logout(request)
+
+
+@login_required
+def clear_data(request):
+    if request.method == 'POST':
+        if request.POST.get('verification') == request.user.mastodon_username:
+            BookMark.objects.filter(owner=request.user).delete()
+            MovieMark.objects.filter(owner=request.user).delete()
+            GameMark.objects.filter(owner=request.user).delete()
+            AlbumMark.objects.filter(owner=request.user).delete()
+            SongMark.objects.filter(owner=request.user).delete()
+            BookReview.objects.filter(owner=request.user).delete()
+            MovieReview.objects.filter(owner=request.user).delete()
+            GameReview.objects.filter(owner=request.user).delete()
+            AlbumReview.objects.filter(owner=request.user).delete()
+            SongReview.objects.filter(owner=request.user).delete()
+            CollectionMark.objects.filter(owner=request.user).delete()
+            Collection.objects.filter(owner=request.user).delete()
+            request.user.first_name = request.user.username
+            request.user.last_name = request.user.mastodon_site
+            request.user.is_active = False
+            request.user.username = 'removed_' + str(request.user.id)
+            request.user.mastodon_id = 0
+            request.user.mastodon_site = 'removed'
+            request.user.mastodon_token = ''
+            request.user.mastodon_locked = False
+            request.user.mastodon_followers = []
+            request.user.mastodon_following = []
+            request.user.mastodon_mutes = []
+            request.user.mastodon_blocks = []
+            request.user.mastodon_domain_blocks = []
+            request.user.mastodon_account = {}
+            request.user.save()
+            auth_logout(request)
+            return redirect(reverse("users:login"))
+        else:
+            messages.add_message(request, messages.ERROR, _('验证信息不符。'))
+    return redirect(reverse("users:data"))
diff --git a/users/data.py b/users/data.py
new file mode 100644
index 00000000..505344c8
--- /dev/null
+++ b/users/data.py
@@ -0,0 +1,142 @@
+from django.shortcuts import reverse, redirect, render, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import auth
+from django.contrib.auth import authenticate
+from django.core.paginator import Paginator
+from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Count
+from .models import User, Report, Preference
+from .forms import ReportForm
+from mastodon.api import *
+from mastodon import mastodon_request_included
+from common.config import *
+from common.models import MarkStatusEnum
+from common.utils import PageLinksGenerator
+from management.models import Announcement
+from books.models import *
+from movies.models import *
+from music.models import *
+from games.models import *
+from books.forms import BookMarkStatusTranslator
+from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
+from games.forms import GameMarkStatusTranslator
+from mastodon.models import MastodonApplication
+from mastodon.api import verify_account
+from django.conf import settings
+from urllib.parse import quote
+import django_rq
+from .account import *
+from .tasks import *
+from datetime import timedelta
+from django.utils import timezone
+import json
+from django.contrib import messages
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, SongMark, AlbumReview, SongReview
+from timeline.models import Activity
+from collection.models import Collection
+from common.importers.goodreads import GoodreadsImporter
+from common.importers.douban import DoubanImporter
+
+
+@mastodon_request_included
+@login_required
+def preferences(request):
+    preference = request.user.get_preference()
+    if request.method == 'POST':
+        preference.default_visibility = int(request.POST.get('default_visibility'))
+        preference.classic_homepage = bool(request.POST.get('classic_homepage'))
+        preference.mastodon_publish_public = bool(request.POST.get('mastodon_publish_public'))
+        preference.mastodon_append_tag = request.POST.get('mastodon_append_tag', '').strip()
+        preference.save(update_fields=['default_visibility', 'classic_homepage', 'mastodon_publish_public', 'mastodon_append_tag'])
+    return render(request, 'users/preferences.html')
+
+
+@mastodon_request_included
+@login_required
+def data(request):
+    return render(request, 'users/data.html', {
+        'allow_any_site': settings.MASTODON_ALLOW_ANY_SITE,
+        'latest_task': request.user.user_synctasks.order_by("-id").first(),
+        'import_status': request.user.get_preference().import_status,
+        'export_status': request.user.get_preference().export_status
+    })
+
+
+@mastodon_request_included
+@login_required
+def export_reviews(request):
+    if request.method != 'POST':
+        return redirect(reverse("users:data"))
+    return render(request, 'users/data.html')
+
+
+@mastodon_request_included
+@login_required
+def export_marks(request):
+    if request.method == 'POST':
+        if not request.user.preference.export_status.get('marks_pending'):
+            django_rq.get_queue('export').enqueue(export_marks_task, request.user)
+            request.user.preference.export_status['marks_pending'] = True
+            request.user.preference.save()
+        messages.add_message(request, messages.INFO, _('导出已开始。'))
+        return redirect(reverse("users:data"))
+    else:
+        try:
+            with open(request.user.preference.export_status['marks_file'], 'rb') as fh:
+                response = HttpResponse(fh.read(), content_type="application/vnd.ms-excel")
+                response['Content-Disposition'] = 'attachment;filename="marks.xlsx"'
+                return response
+        except Exception:
+            messages.add_message(request, messages.ERROR, _('导出文件已过期,请重新导出'))
+            return redirect(reverse("users:data"))
+
+
+@login_required
+def sync_mastodon(request):
+    if request.method == 'POST':
+        django_rq.get_queue('mastodon').enqueue(refresh_mastodon_data_task, request.user)
+        messages.add_message(request, messages.INFO, _('同步已开始。'))
+    return redirect(reverse("users:data"))
+
+
+@login_required
+def reset_visibility(request):
+    if request.method == 'POST':
+        visibility = int(request.POST.get('visibility'))
+        visibility = visibility if visibility >= 0 and visibility <= 2 else 0
+        BookMark.objects.filter(owner=request.user).update(visibility=visibility)
+        MovieMark.objects.filter(owner=request.user).update(visibility=visibility)
+        GameMark.objects.filter(owner=request.user).update(visibility=visibility)
+        AlbumMark.objects.filter(owner=request.user).update(visibility=visibility)
+        SongMark.objects.filter(owner=request.user).update(visibility=visibility)
+        Activity.objects.filter(owner=request.user).update(visibility=visibility)
+        messages.add_message(request, messages.INFO, _('已重置。'))
+    return redirect(reverse("users:data"))
+
+
+@login_required
+def import_goodreads(request):
+    if request.method == 'POST':
+        raw_url = request.POST.get('url')
+        if GoodreadsImporter.import_from_url(raw_url, request.user):
+            messages.add_message(request, messages.INFO, _('链接已保存,等待后台导入。'))
+        else:
+            messages.add_message(request, messages.ERROR, _('无法识别链接。'))
+    return redirect(reverse("users:data"))
+
+
+@login_required
+def import_douban(request):
+    if request.method == 'POST':
+        importer = DoubanImporter(request.user, request.POST.get('visibility'))
+        if importer.import_from_file(request.FILES['file']):
+            messages.add_message(request, messages.INFO, _('文件上传成功,等待后台导入。'))
+        else:
+            messages.add_message(request, messages.ERROR, _('无法识别文件。'))
+    return redirect(reverse("users:data"))
diff --git a/users/management/commands/backfill_mastodon.py b/users/management/commands/backfill_mastodon.py
new file mode 100644
index 00000000..55f4dea6
--- /dev/null
+++ b/users/management/commands/backfill_mastodon.py
@@ -0,0 +1,21 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from django.contrib.sessions.models import Session
+
+
+class Command(BaseCommand):
+    help = 'Backfill Mastodon data if missing'
+
+    def handle(self, *args, **options):
+        for session in Session.objects.order_by('-expire_date'):
+            uid = session.get_decoded().get('_auth_user_id')
+            token = session.get_decoded().get('oauth_token')
+            if uid and token:
+                user = User.objects.get(pk=uid)
+                if user.mastodon_token:
+                    print(f'skip {user}')
+                    continue
+                user.mastodon_token = token
+                user.refresh_mastodon_data()
+                user.save()
+                print(f"Refreshed {user}")
diff --git a/users/management/commands/disable_user.py b/users/management/commands/disable_user.py
new file mode 100644
index 00000000..ac28b6a5
--- /dev/null
+++ b/users/management/commands/disable_user.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+
+
+class Command(BaseCommand):
+    help = 'disable user'
+
+    def add_arguments(self, parser):
+        parser.add_argument('id', type=int, help='user id')
+
+    def handle(self, *args, **options):
+        h = int(options['id'])
+        u = User.objects.get(id=h)
+        u.username = '(duplicated)'+u.username
+        u.is_active = False
+        u.save()
+        print(f'{u} updated')
diff --git a/users/management/commands/refresh_following.py b/users/management/commands/refresh_following.py
new file mode 100644
index 00000000..b29f4f0c
--- /dev/null
+++ b/users/management/commands/refresh_following.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+from tqdm import tqdm
+
+
+class Command(BaseCommand):
+    help = 'Refresh following data for all users'
+
+    def handle(self, *args, **options):
+        count = 0
+        for user in tqdm(User.objects.all()):
+            user.following = user.get_following_ids()
+            if user.following:
+                count += 1
+                user.save(update_fields=['following'])
+
+        print(f'{count} users updated')
diff --git a/users/management/commands/refresh_mastodon.py b/users/management/commands/refresh_mastodon.py
new file mode 100644
index 00000000..ff79d5fc
--- /dev/null
+++ b/users/management/commands/refresh_mastodon.py
@@ -0,0 +1,25 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+from tqdm import tqdm
+
+
+class Command(BaseCommand):
+    help = 'Refresh Mastodon data for all users if not updated in last 24h'
+
+    def handle(self, *args, **options):
+        count = 0
+        for user in tqdm(User.objects.filter(mastodon_last_refresh__lt=timezone.now() - timedelta(hours=24), is_active=True)):
+            if user.mastodon_token or user.mastodon_refresh_token:
+                tqdm.write(f"Refreshing {user}")
+                if user.refresh_mastodon_data():
+                    tqdm.write(f"Refreshed {user}")
+                    count += 1
+                else:
+                    tqdm.write(f"Refresh failed for {user}")
+                user.save()
+            else:
+                tqdm.write(f'Missing token for {user}')
+
+        print(f'{count} users updated')
diff --git a/users/models.py b/users/models.py
index 7d35433f..2cc92e83 100644
--- a/users/models.py
+++ b/users/models.py
@@ -3,21 +3,40 @@ import django.contrib.postgres.fields as postgres
 from django.db import models
 from django.contrib.auth.models import AbstractUser
 from django.utils import timezone
-from boofilsic.settings import REPORT_MEDIA_PATH_ROOT, DEFAULT_PASSWORD
 from django.core.serializers.json import DjangoJSONEncoder
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 from common.utils import GenerateDateUUIDMediaFilePath
+from django.conf import settings
+from mastodon.api import *
 
 
 def report_image_path(instance, filename):
-    return GenerateDateUUIDMediaFilePath(instance, filename, REPORT_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.REPORT_MEDIA_PATH_ROOT)
 
 
 class User(AbstractUser):
-    mastodon_id = models.IntegerField(blank=False)
+    if settings.MASTODON_ALLOW_ANY_SITE:
+        username = models.CharField(
+            _('username'),
+            max_length=150,
+            unique=False,
+            help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
+        )
+    following = models.JSONField(default=list)
+    mastodon_id = models.CharField(max_length=100, blank=False)
     # mastodon domain name, eg donotban.com
     mastodon_site = models.CharField(max_length=100, blank=False)
-    # store the latest read announcement id, 
+    mastodon_token = models.CharField(max_length=2048, default='')
+    mastodon_refresh_token = models.CharField(max_length=2048, default='')
+    mastodon_locked = models.BooleanField(default=False)
+    mastodon_followers = models.JSONField(default=list)
+    mastodon_following = models.JSONField(default=list)
+    mastodon_mutes = models.JSONField(default=list)
+    mastodon_blocks = models.JSONField(default=list)
+    mastodon_domain_blocks = models.JSONField(default=list)
+    mastodon_account = models.JSONField(default=dict)
+    mastodon_last_refresh = models.DateTimeField(default=timezone.now)
+    # store the latest read announcement id,
     # every time user read the announcement update this field
     read_announcement_index = models.PositiveIntegerField(default=0)
 
@@ -27,13 +46,111 @@ class User(AbstractUser):
                 fields=['username', 'mastodon_site'], name="unique_user_identity")
         ]
 
-    def save(self, *args, **kwargs):
-        """ Automatically populate password field with DEFAULT_PASSWORD before saving."""
-        self.set_password(DEFAULT_PASSWORD)
-        return super().save(*args, **kwargs)
+    # def save(self, *args, **kwargs):
+    #     """ Automatically populate password field with settings.DEFAULT_PASSWORD before saving."""
+    #     self.set_password(settings.DEFAULT_PASSWORD)
+    #     return super().save(*args, **kwargs)
+
+    @property
+    def mastodon_username(self):
+        return self.username + '@' + self.mastodon_site
+
+    @property
+    def display_name(self):
+        return self.mastodon_account['display_name'] if self.mastodon_account and 'display_name' in self.mastodon_account and self.mastodon_account['display_name'] else self.mastodon_username
 
     def __str__(self):
-        return self.username + '@' + self.mastodon_site
+        return self.mastodon_username
+
+    def get_preference(self):
+        pref = Preference.objects.filter(user=self).first()  # self.preference
+        if not pref:
+            pref = Preference.objects.create(user=self)
+        return pref
+
+    def refresh_mastodon_data(self):
+        """ Try refresh account data from mastodon server, return true if refreshed successfully, note it will not save to db """
+        self.mastodon_last_refresh = timezone.now()
+        code, mastodon_account = verify_account(self.mastodon_site, self.mastodon_token)
+        if code == 401 and self.mastodon_refresh_token:
+            self.mastodon_token = refresh_access_token(self.mastodon_site, self.mastodon_refresh_token)
+            if self.mastodon_token:
+                code, mastodon_account = verify_account(self.mastodon_site, self.mastodon_token)
+        updated = False
+        if mastodon_account:
+            self.mastodon_account = mastodon_account
+            self.mastodon_locked = mastodon_account['locked']
+            if self.username != mastodon_account['username']:
+                print(f"username changed from {self} to {mastodon_account['username']}")
+                self.username = mastodon_account['username']
+            # self.mastodon_token = token
+            # user.mastodon_id  = mastodon_account['id']
+            self.mastodon_followers = get_related_acct_list(self.mastodon_site, self.mastodon_token, f'/api/v1/accounts/{self.mastodon_id}/followers')
+            self.mastodon_following = get_related_acct_list(self.mastodon_site, self.mastodon_token, f'/api/v1/accounts/{self.mastodon_id}/following')
+            self.mastodon_mutes = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/mutes')
+            self.mastodon_blocks = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/blocks')
+            self.mastodon_domain_blocks = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/domain_blocks')
+            self.following = self.get_following_ids()
+            updated = True
+        elif code == 401:
+            print(f'401 {self}')
+            self.mastodon_token = ''
+        return updated
+
+    def get_following_ids(self):
+        fl = []
+        for m in self.mastodon_following:
+            target = User.get(m)
+            if target and ((not target.mastodon_locked) or self.mastodon_username in target.mastodon_followers):
+                fl.append(target.id)
+        return fl
+
+    def is_blocking(self, target):
+        return target.mastodon_username in self.mastodon_blocks or target.mastodon_site in self.mastodon_domain_blocks
+
+    def is_blocked_by(self, target):
+        return target.is_blocking(self)
+
+    def is_muting(self, target):
+        return target.mastodon_username in self.mastodon_mutes
+
+    def is_following(self, target):
+        return self.mastodon_username in target.mastodon_followers if target.mastodon_locked else self.mastodon_username in target.mastodon_followers or target.mastodon_username in self.mastodon_following
+
+    def is_followed_by(self, target):
+        return target.is_following(self)
+
+    def get_mark_for_item(self, item):
+        params = {item.__class__.__name__.lower() + '_id': item.id, 'owner': self}
+        mark = item.mark_class.objects.filter(**params).first()
+        return mark
+
+    def get_max_visibility(self, viewer):
+        if not viewer.is_authenticated:
+            return 0
+        elif viewer == self:
+            return 2
+        elif viewer.is_blocked_by(self):
+            return -1
+        elif viewer.is_following(self):
+            return 1
+        else:
+            return 0
+
+    @classmethod
+    def get(self, id):
+        if isinstance(id, str):
+            try:
+                username = id.split('@')[0]
+                site = id.split('@')[1]
+            except IndexError as e:
+                return None
+            query_kwargs = {'username': username, 'mastodon_site': site}
+        elif isinstance(id, int):
+            query_kwargs = {'pk': id}
+        else:
+            return None
+        return User.objects.filter(**query_kwargs).first()
 
 
 class Preference(models.Model):
@@ -43,9 +160,15 @@ class Preference(models.Model):
         blank=True,
         default=list,
     )
+    export_status = models.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
+    import_status = models.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
+    default_visibility = models.PositiveSmallIntegerField(default=0)
+    classic_homepage = models.BooleanField(null=False, default=False)
+    mastodon_publish_public = models.BooleanField(null=False, default=False)
+    mastodon_append_tag = models.CharField(max_length=2048, default='')
 
     def get_serialized_home_layout(self):
-        return str(self.home_layout).replace("\'","\"")
+        return str(self.home_layout).replace("\'", "\"")
 
     def __str__(self):
         return str(self.user)
diff --git a/users/static/js/followers_list.js b/users/static/js/followers_list.js
index 0c355f60..44680865 100644
--- a/users/static/js/followers_list.js
+++ b/users/static/js/followers_list.js
@@ -36,7 +36,8 @@ $(document).ready( function() {
             }
             $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
             $("#userInfoCard .mast-displayname").html(userName);
-            $("#userInfoCard .mast-brief").text($(userData.note).text());
+            $("#userInfoCard .mast-brief").text($("<div>"+userData.note.replace(/\<br/g,'\n<br').replace(/\<p/g,'\n<p')+"</div>").text());
+            $("#userInfoCard .mast-brief").html($("#userInfoCard .mast-brief").html().replace(/\n/g,'<br/>'));
             $(userInfoSpinner).remove();
         }
     );
@@ -45,7 +46,7 @@ $(document).ready( function() {
         id,
         mast_uri,
         token,
-        function(userList, request) {
+        function(userList, nextPage) {
             let subUserList = null;
             if (userList.length == 0) {
                 $(".mast-followers").hide();
@@ -101,12 +102,7 @@ $(document).ready( function() {
             });
 
             mainSpinner.hide();
-            request.getResponseHeader('link').split(',').forEach(link => {
-                if (link.includes('next')) {
-                    let regex = /<(.*?)>/;
-                    nextUrl = link.match(regex)[1];
-                }
-            });            
+            nextUrl = nextPage;
         }
     );
 
@@ -206,7 +202,12 @@ $(document).ready( function() {
                         } else {
                             temp.find(".mast-displayname").text(data.username);
                         }
-                        let url = $("#userPageURL").text().replace('0', data.id);
+                        let url;
+                        if (data.acct.includes('@')) {
+                            url = $("#userPageURL").text().replace('0', data.acct);
+                        } else {
+                            url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
+                        }
                         temp.find("a").attr('href', url);
                         temp.find(".mast-brief").text(data.note.replace(/(<([^>]+)>)/ig, ""));
                         // console.log($(temp).html())
diff --git a/users/static/js/following_list.js b/users/static/js/following_list.js
index 0e6ca0ba..6b3ba278 100644
--- a/users/static/js/following_list.js
+++ b/users/static/js/following_list.js
@@ -36,7 +36,8 @@ $(document).ready( function() {
             }
             $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
             $("#userInfoCard .mast-displayname").html(userName);
-            $("#userInfoCard .mast-brief").text($(userData.note).text());
+            $("#userInfoCard .mast-brief").text($("<div>"+userData.note.replace(/\<br/g,'\n<br').replace(/\<p/g,'\n<p')+"</div>").text());
+            $("#userInfoCard .mast-brief").html($("#userInfoCard .mast-brief").html().replace(/\n/g,'<br/>'));
             $(userInfoSpinner).remove();
         }
     );
@@ -82,7 +83,7 @@ $(document).ready( function() {
         id,
         mast_uri,
         token,
-        function(userList, request) {
+        function(userList, nextPage) {
             // aside
             let subUserList = null;
             if (userList.length == 0) {
@@ -139,12 +140,7 @@ $(document).ready( function() {
             });
 
             mainSpinner.hide();
-            request.getResponseHeader('link').split(',').forEach(link => {
-                if (link.includes('next')) {
-                    let regex = /<(.*?)>/;
-                    nextUrl = link.match(regex)[1];
-                }
-            });
+            nextUrl = nextPage;
         }
     );
 
diff --git a/users/static/lib/js/js.cookie.min.js b/users/static/lib/js/js.cookie.min.js
deleted file mode 100644
index 1549485f..00000000
--- a/users/static/lib/js/js.cookie.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! js-cookie v3.0.0-rc.1 | MIT */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,r=e.Cookies=t();r.noConflict=function(){return e.Cookies=n,r}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)e[r]=n[r]}return e}var t={read:function(e){return e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}};return function n(r,o){function i(t,n,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape),n=r.write(n,t);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n+c}}return Object.create({set:i,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var n=document.cookie?document.cookie.split("; "):[],o={},i=0;i<n.length;i++){var c=n[i].split("="),u=c.slice(1).join("=");'"'===u[0]&&(u=u.slice(1,-1));try{var f=t.read(c[0]);if(o[f]=r.read(u,f),e===f)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){i(t,"",e({},n,{expires:-1}))},withAttributes:function(t){return n(this.converter,e({},this.attributes,t))},withConverter:function(t){return n(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(r)}})}(t,{path:"/"})});
diff --git a/users/tasks.py b/users/tasks.py
new file mode 100644
index 00000000..a7c17f82
--- /dev/null
+++ b/users/tasks.py
@@ -0,0 +1,146 @@
+from django.shortcuts import reverse, redirect, render, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import auth
+from django.contrib.auth import authenticate
+from django.core.paginator import Paginator
+from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Count
+from .models import User, Report, Preference
+from .forms import ReportForm
+from mastodon.api import *
+from mastodon import mastodon_request_included
+from common.config import *
+from common.models import MarkStatusEnum
+from common.utils import PageLinksGenerator
+from management.models import Announcement
+from books.models import *
+from movies.models import *
+from music.models import *
+from games.models import *
+from books.forms import BookMarkStatusTranslator
+from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
+from games.forms import GameMarkStatusTranslator
+from mastodon.models import MastodonApplication
+from django.conf import settings
+from urllib.parse import quote
+from openpyxl import Workbook
+from common.utils import GenerateDateUUIDMediaFilePath
+from datetime import datetime
+import os
+
+
+def refresh_mastodon_data_task(user, token=None):
+    if token:
+        user.mastodon_token = token
+    if user.refresh_mastodon_data():
+        user.save()
+        print(f"{user} mastodon data refreshed")
+    else:
+        print(f"{user} mastodon data refresh failed")
+
+
+def export_marks_task(user):
+    user.preference.export_status['marks_pending'] = True
+    user.preference.save(update_fields=['export_status'])
+    filename = GenerateDateUUIDMediaFilePath(None, 'f.xlsx', settings.MEDIA_ROOT + settings.EXPORT_FILE_PATH_ROOT)
+    if not os.path.exists(os.path.dirname(filename)):
+        os.makedirs(os.path.dirname(filename))
+    heading = ['标题', '简介', '豆瓣评分', '链接', '创建时间', '我的评分', '标签', '评论', 'NeoDB链接', '其它ID']
+    wb = Workbook()  # adding write_only=True will speed up but corrupt the xlsx and won't be importable
+    for status, label in [('collect', '看过'), ('do', '在看'), ('wish', '想看')]:
+        ws = wb.create_sheet(title=label)
+        marks = MovieMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            movie = mark.movie
+            title = movie.title
+            summary = str(movie.year) + ' / ' + ','.join(movie.area) + ' / ' + ','.join(map(lambda x: str(MovieGenreTranslator[x]), movie.genre)) + ' / ' + ','.join(movie.director) + ' / ' + ','.join(movie.actor)
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (movie.rating / 2) if movie.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = movie.source_url
+            url = settings.APP_WEBSITE + movie.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, movie.imdb_code]
+            ws.append(line)
+
+    for status, label in [('collect', '听过'), ('do', '在听'), ('wish', '想听')]:
+        ws = wb.create_sheet(title=label)
+        marks = AlbumMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            album = mark.album
+            title = album.title
+            summary = ','.join(album.artist) + ' / ' + (album.release_date.strftime('%Y') if album.release_date else '')
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (album.rating / 2) if album.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = album.source_url
+            url = settings.APP_WEBSITE + album.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, '']
+            ws.append(line)
+
+    for status, label in [('collect', '读过'), ('do', '在读'), ('wish', '想读')]:
+        ws = wb.create_sheet(title=label)
+        marks = BookMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            book = mark.book
+            title = book.title
+            summary = ','.join(book.author) + ' / ' + str(book.pub_year) + ' / ' + book.pub_house
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (book.rating / 2) if book.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = book.source_url
+            url = settings.APP_WEBSITE + book.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, book.isbn]
+            ws.append(line)
+
+    for status, label in [('collect', '玩过'), ('do', '在玩'), ('wish', '想玩')]:
+        ws = wb.create_sheet(title=label)
+        marks = GameMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            game = mark.game
+            title = game.title
+            summary = ','.join(game.genre) + ' / ' + ','.join(game.platform) + ' / ' + (game.release_date.strftime('%Y-%m-%d') if game.release_date else '')
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (game.rating / 2) if game.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = game.source_url
+            url = settings.APP_WEBSITE + game.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, '']
+            ws.append(line)
+
+    review_heading = ['标题', '评论对象', '链接', '创建时间', '我的评分', '类型', '内容', '评论对象原始链接', '评论对象NeoDB链接']
+    for ReviewModel, label in [(MovieReview, '影评'), (BookReview, '书评'), (AlbumReview, '乐评'), (GameReview, '游戏评论')]:
+        ws = wb.create_sheet(title=label)
+        reviews = ReviewModel.objects.filter(owner=user).order_by("-edited_time")
+        ws.append(review_heading)
+        for review in reviews:
+            title = review.title
+            target = "《" + review.item.title + "》"
+            url = review.url
+            timestamp = review.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = None  # (mark.rating / 2) if mark.rating else None
+            content = review.content
+            target_source_url = review.item.source_url
+            target_url = review.item.url
+            line = [title, target, url, timestamp, my_rating, label, content, target_source_url, target_url]
+            ws.append(line)
+
+    wb.save(filename=filename)
+    user.preference.export_status['marks_pending'] = False
+    user.preference.export_status['marks_file'] = filename
+    user.preference.export_status['marks_date'] = datetime.now().strftime("%Y-%m-%d %H:%M")
+    user.preference.save(update_fields=['export_status'])
diff --git a/users/templates/users/book_list.html b/users/templates/users/book_list.html
deleted file mode 100644
index ff2dfea9..00000000
--- a/users/templates/users/book_list.html
+++ /dev/null
@@ -1,282 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            <a href="{% url 'books:retrieve' mark.book.id %}">
-                                                <img src="{{ mark.book.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                <a href="{% url 'books:retrieve' mark.book.id %}" class="entity-list__entity-link">
-                                                    {{ mark.book.title }}
-                                                </a>
-                                                <a href="{{ mark.book.source_url }}">
-                                                    <span class="source-label source-label__{{ mark.book.source_site }}">{{ mark.book.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            {% comment %}                                                
-                                                <!-- {% if mark.book.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ mark.book.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">
-                                                    {{ mark.book.rating }}
-                                                </span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %} -->
-                                            {% endcomment %}
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                            {% if mark.book.pub_year %}
-                                            {{ mark.book.pub_year }}{% trans '年' %} /
-                                                {% if mark.book.pub_month %}
-                                                    {{ mark.book.pub_month }}{% trans '月' %} /
-                                                {% endif %} 
-                                            {% endif %}
-                                            
-                                            {% if mark.book.author %}
-                                            {% trans '作者' %}
-                                            {% for author in mark.book.author %}                  
-                                            {{ author }}{% if not forloop.last %},{% endif %}
-                                            {% endfor %}/
-                                            {% endif %}
-        
-                                            {% if mark.book.translator %}
-                                            {% trans '译者' %}
-                                            {% for translator in mark.book.translator %}                  
-                                            {{ translator }}{% if not forloop.last %},{% endif %}
-                                            {% endfor %}/
-                                            {% endif %}                                    
-        
-                                            {% if mark.book.orig_title %}
-                                            &nbsp;{% trans '原名' %}
-                                                {{ mark.book.orig_title }}
-                                            {% endif %}
-                                            </span>                    
-                                            <p class="entity-list__entity-brief">
-                                                {{ mark.book.brief }}
-                                            </p>
-                                            <div class="tag-collection">
-                                                {% for tag_dict in mark.book.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path
-                                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/data.html b/users/templates/users/data.html
new file mode 100644
index 00000000..d9f6a19a
--- /dev/null
+++ b/users/templates/users/data.html
@@ -0,0 +1,337 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - 数据管理</title>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            {% if messages %}
+                            <ul class="messages">
+                                {% for message in messages %}
+                                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
+                                {% endfor %}
+                            </ul>
+                            {% endif %}
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导入豆瓣标记和短评' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'sync:douban' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <input type="hidden" name="user" value="{{ request.user.id }}">
+                                            <span>{% trans '导入:' %}</span>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_book" id="syncBook" checked>
+                                                <label for="syncBook">{% trans '书' %}</label>
+                                            </div>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_movie" id="syncMovie" checked>
+                                                <label for="syncMovie">{% trans '电影' %}</label>
+                                            </div>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_music" id="syncMusic" checked>
+                                                <label for="syncMusic">{% trans '音乐' %}</label>
+                                            </div>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_game" id="syncGame" checked>
+                                                <label for="syncGame">{% trans '游戏' %}</label>
+                                            </div>
+                                            <div></div>
+                                            <span>{% trans '覆盖:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="overwrite" id="overwrite">
+                                                <label for="overwrite">{% trans '选中会覆盖现有标记' %}</label>
+                                            </div>
+                                            <div></div>
+                                            <span>{% trans '可见性:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="default_public" id="visibility" checked>
+                                                <label for="visibility">{% trans '选中后导入标记对其他用户可见;标记可见性在导入后也可更改。' %}</label>
+                                            </div>
+                                            <div></div>
+                                            <div class="import-panel__file-input">
+                                                从<a href="https://doufen.org" target="_blank">豆伴(豆坟)</a>备份导出的.xlsx文件,<strong>请勿手动修改该文件</strong>:
+                                                <input type="file" name="file" id="excelFile" required accept=".xlsx"> 
+                                            </div>
+                                            <input type="submit" class="import-panel__button" value="{% trans '导入' %}" id="uploadBtn"
+                                                {% if not latest_task is None and not latest_task.is_finished %}
+                                                disabled
+                                                {% endif %}
+                                            >
+                                        </form>
+                                        <div class="import-panel__progress"
+                                            {% if latest_task.is_finished or latest_task is None %}
+                                            style="display: none;"
+                                            {% endif %}
+                                        >
+                                            <label for="importProgress">{% trans '进度' %}</label>
+                                            <progress id="importProgress" value="{{ latest_task.finished_items }}" max="{{ latest_task.total_items }}"></progress>
+                                            <span class="float-right" id="progressPercent">{{ latest_task.get_progress | floatformat:"0" }}%</span>
+                                            <span class="clearfix"></span>
+                                        </div>
+                                        <div class="import-panel__last-task"
+                                            {% if not latest_task.is_finished %}`
+                                            style="display: none;"
+                                            {% endif %}
+                                        >
+                                            {% trans '上次导入:' %}
+                                            <span class="index">{% trans '总数' %} <span id="lastTaskTotalItems">{{ latest_task.total_items }}</span></span>
+                                            <span class="index">{% trans '同步' %} <span id="lastTaskSuccessItems">{{ latest_task.success_items }}</span></span>
+                                            <span class="index">{% trans '状态' %} <span id="lastTaskStatus">{{ latest_task.get_status_emoji }}</span></span>
+                                            <div class="import-panel__fail-urls"
+                                            {% if not latest_task.failed_urls %}
+                                                style="display: none;"
+                                            {% endif %}
+                                            >
+                                                <span>
+                                                    {% trans '失败条目链接' %}
+                                                </span>
+                                                <a class="float-right" style="cursor: pointer;" id="failedUrlsBtn">
+                                                    ▶
+                                                </a>
+                                                <script>
+                                                    $("#failedUrlsBtn").data("collapse", true);
+                                                    $("#failedUrlsBtn").on('click', ()=>{
+                                                        const btn = $("#failedUrlsBtn");
+                                                        if(btn.data("collapse") == true) {
+                                                            btn.data("collapse", false);
+                                                            btn.text("▼");
+                                                            $("#failedUrls").show();
+                                                        } else {
+                                                            btn.data("collapse", true);
+                                                            btn.text("▶");
+                                                            $("#failedUrls").hide();
+                                                        }
+                                                    });
+                                                </script>
+                                                <span class="clearfix"></span>
+                                                <ul id="failedUrls" style="display: none;">
+                                                    {% for url in latest_task.failed_urls %}
+                                                    <li>{{ url }}</li>
+                                                    {% endfor %}
+                                                </ul>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导入豆瓣评论' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:import_douban' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">
+                                                <p>从<a href="https://doufen.org" target="_blank">豆伴(豆坟)</a>备份导出的.xlsx文件:
+                                                    <input type="file" name="file" id="excel" required accept=".xlsx">
+                                                </p>
+                                                <p>可见性:
+                                                    <label for="id_visibility_0"><input type="radio" name="visibility" value="0" required="" id="id_visibility_0" checked>
+                                                     公开</label>
+                                                    <label for="id_visibility_1"><input type="radio" name="visibility" value="1" required="" id="id_visibility_1">
+                                                     仅关注者</label>
+                                                    <label for="id_visibility_2"><input type="radio" name="visibility" value="2" required="" id="id_visibility_2">
+                                                     仅自己</label>
+                                                </p>
+                                                {% if import_status.douban_pending %}
+                                                <input type="submit" class="import-panel__button" value="{% trans '备份文件已上传,请等待导入完成或刷新页面查看最新进度' %}" disabled />
+                                                {% else %}
+                                                <input type="submit" class="import-panel__button" value="{% trans '导入' %}"/>
+                                                {% endif %}
+
+                                                {% if import_status.douban_pending == 2 %}
+                                                正在等待
+                                                {% elif import_status.douban_pending == 1 %}
+                                                正在导入
+                                                    {% if import_status.douban_total %}
+                                                    共{{ import_status.douban_total }}篇,目前已处理{{ import_status.douban_processed }}篇,其中已存在{{ import_status.douban_skipped }}篇,新增{{ import_status.douban_imported }}篇
+                                                    {% endif %}
+                                                {% elif import_status.douban_file %}
+                                                    上次结果
+                                                    共计{{ import_status.douban_total }}篇,处理{{ import_status.douban_processed }}篇,其中已存在{{ import_status.douban_skipped }}篇,新增{{ import_status.douban_imported }}篇
+                                                {% endif %}
+                                            </div>
+                                            <div>
+                                                请务必在豆伴(豆坟)导出时勾选「书影音游剧」和「评论」;已经存在的评论不会被覆盖。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导入Goodreads帐号或书单' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:import_goodreads' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">输入Goodreads链接
+                                            <input type="url" name="url" value="" placeholder="例如 https://www.goodreads.com/user/show/12345-janedoe">
+                                            <input type="submit" class="import-panel__button" value="{% trans '导入' %}" id="uploadBtn" />
+                                            </div>
+                                            <div>
+                                                Goodreads用户主页链接形如 https://www.goodreads.com/user/show/12345-janedoe 将自动导入到当前用户的想读、在读、已读列表,每本书的评论导入为本站短评;
+                                                <br />
+                                                Goodreads书单链接形如 https://www.goodreads.com/review/list/12345-janedoe?shelf=name 或 https://www.goodreads.com/list/show/155086.Popular_Highlights 将自动导入成为收藏单,每本书的评论导入为收藏单条目备注。
+						<br />
+						欲导入的Goodreads用户需将Who Can View My Profile设置为anyone,导入后可改回原设置。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导出个人数据' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:export_marks' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            {% if export_status.marks_pending %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '正在导出兼容豆伴(doufen)和NiceDB的标记、短评和评论' %}" id="uploadBtn" disabled />
+                                            {% else %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '导出兼容豆伴(doufen)和NiceDB的标记、短评和评论' %}" id="uploadBtn" />
+                                            {% endif %}
+                                            {% if export_status.marks_file %}
+                                            <a href="{% url 'users:export_marks' %}" download>下载 {{ export_status.marks_date }} 的导出</a>
+                                            {% endif %}
+                                        </form>
+                                        <!-- <form action="{% url 'users:export_reviews' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '导出评论' %}" id="uploadBtn"
+                                            >
+                                        </form> -->
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '重置所有标记和短评可见性' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:reset_visibility' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '重置' %}" id="uploadBtn" />
+                                            <div class="import-panel__checkbox">
+                                            <input type="radio" name="visibility" id="visPublic" value="0" checked>
+                                            <label for="visPublic">{% trans '公开' %}</label>
+                                            <input type="radio" name="visibility" id="visFollower" value="1">
+                                            <label for="visFollower">{% trans '仅关注者' %}</label>
+                                            <input type="radio" name="visibility" id="visSelf" value="2">
+                                            <label for="visSelf">{% trans '仅自己' %}</label>
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '更新社交关系数据' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:sync_mastodon' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '同步' %}" id="uploadBtn" /> 上次更新时间 {{ user.mastodon_last_refresh }}
+                                            <div>
+                                            为了正确高效的展示短评和评论,{{ site_name }}会缓存你在联邦网络的关注、屏蔽和静音列表。如果你刚刚更新过帐户的上锁状态、增减过关注、静音或屏蔽,希望立即生效,可以点击这里立刻更新;这类信息也会每天自动同步。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        {% if allow_any_site %}
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '替换社交账号' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:reconnect' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">输入新社交账号所在的实例域名
+                                            <input type="input" name="domain" value="" placeholder="例如mastodon.online">
+                                            <input type="submit" class="import-panel__button" value="{% trans '登录新账号' %}" id="uploadBtn" />
+                                            </div>
+                                            <div>
+                                            替换后可使用新的联邦网络身份来登录{{ site_name }}和控制数据可见性,已有的标记评论收藏单等数据不受影响。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '删除数据和帐号信息' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:clear_data' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">输入完整的 用户名@实例名 以确认删除
+                                            <input type="input" name="verification" value="" placeholder="user@mastodon.social">
+                                            <input type="submit" class="import-panel__button" value="{% trans '永久删除' %}" id="uploadBtn" />
+                                            </div>
+                                            <div>
+                                            删除将无法撤销
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+
+        </div>
+
+        {% include "partial/_footer.html" %}
+    </div>
+
+    <div id="queryProgressURL" data-url="{% url 'sync:progress' %}"></div>
+    <div id="querySyncInfoURL" data-url="{% url 'sync:last' %}"></div>
+</body>
+
+
+</html>
diff --git a/users/templates/users/game_list.html b/users/templates/users/game_list.html
deleted file mode 100644
index f4e97145..00000000
--- a/users/templates/users/game_list.html
+++ /dev/null
@@ -1,272 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load humanize %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            <a href="{% url 'games:retrieve' mark.game.id %}">
-                                                <img src="{{ mark.game.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                <a href="{% url 'games:retrieve' mark.game.id %}" class="entity-list__entity-link">
-                                                    {{ mark.game.title }}
-                                                </a>
-                                                <a href="{{ mark.game.source_url }}">
-                                                    <span class="source-label source-label__{{ mark.game.source_site }}">{{ mark.game.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                                {% if mark.game.other_title %}{% trans '别名' %}
-                                                {% for other_title in mark.game.other_title %}
-                                                {{ other_title }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                                
-                                                {% if mark.game.developer %}{% trans '开发商' %}
-                                                {% for developer in mark.game.developer %}
-                                                {{ developer }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                                
-                                                {% if mark.game.genre %}{% trans '类型' %}
-                                                {% for genre in mark.game.genre %}
-                                                {{ genre }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                                
-                                                {% if mark.game.platform %}{% trans '平台' %}
-                                                {% for platform in mark.game.platform %}
-                                                {{ platform }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                            </span>       
-                                          
-                                            <p class="entity-list__entity-brief">
-                                                {{ mark.game.brief }}
-                                            </p>
-                                            <div class="tag-collection">
-                                                {% for tag_dict in mark.game.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path
-                                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/home.html b/users/templates/users/home.html
index 1d49c7f7..ddad07d3 100644
--- a/users/templates/users/home.html
+++ b/users/templates/users/home.html
@@ -11,18 +11,16 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    
     {% if user == request.user %}
-        <title>{% trans 'NiceDB - 我的主页' %}</title>
+        <title>{{ site_name }} - {% trans '我的个人主页' %}</title>
     {% else %}
-        <title>{% trans 'NiceDB - ' %}{{user.username}}{% trans '的主页' %}</title>
+        <title>{{ site_name }} - {{user.display_name}}</title>
     {% endif %}
-        
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'js/mastodon.js' %}"></script>
     <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-
 </head>
 
 <body>
@@ -44,7 +42,7 @@
                                     {{ wish_book_count }}
                                 </span>
                                 {% if wish_book_more %}
-                                <a href="{% url 'users:book_list' user.id 'wish' %}"
+                                <a href="{% url 'users:book_list' user.mastodon_username 'wish' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                                 <ul class="entity-sort__entity-list">
@@ -72,7 +70,7 @@
                                     {{ do_book_count }}
                                 </span>
                                 {% if do_book_more %}
-                                <a href="{% url 'users:book_list' user.id 'do' %}"
+                                <a href="{% url 'users:book_list' user.mastodon_username 'do' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -101,7 +99,7 @@
                                     {{ collect_book_count }}
                                 </span>
                                 {% if collect_book_more %}
-                                <a href="{% url 'users:book_list' user.id 'collect' %}"
+                                <a href="{% url 'users:book_list' user.mastodon_username 'collect' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -122,6 +120,35 @@
                                 </ul>
                             </div>
 
+                            <div class="entity-sort" id="bookReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的书籍' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ book_reviews_count }}
+                                </span>
+                                {% if book_reviews_more %}
+                                <a href="{% url 'users:book_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for book_review in book_reviews %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'books:retrieve' book_review.book.id %}">
+                                            <img src="{{ book_review.book.cover|thumb:'normal' }}"
+                                                alt="{{book_review.book.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{book_review.book.title}}">{{ book_review.book.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
                             <div class="entity-sort" id="movieWish">
                                 <h5 class="entity-sort__label">
                                     {% trans '想看的电影/剧集' %}
@@ -130,7 +157,7 @@
                                     {{ wish_movie_count }}
                                 </span>
                                 {% if wish_movie_more %}
-                                <a href="{% url 'users:movie_list' user.id 'wish' %}"
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'wish' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -159,7 +186,7 @@
                                     {{ do_movie_count }}
                                 </span>
                                 {% if do_movie_more %}
-                                <a href="{% url 'users:movie_list' user.id 'do' %}"
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'do' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -188,7 +215,7 @@
                                     {{ collect_movie_count }}
                                 </span>
                                 {% if collect_movie_more %}
-                                <a href="{% url 'users:movie_list' user.id 'collect' %}"
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'collect' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -209,6 +236,35 @@
                                 </ul>
                             </div>
 
+                            <div class="entity-sort" id="movieReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的电影/剧集' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ movie_reviews_count }}
+                                </span>
+                                {% if movie_reviews_more %}
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for movie_review in movie_reviews %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'movies:retrieve' movie_review.movie.id %}">
+                                            <img src="{{ movie_review.movie.cover|thumb:'normal' }}"
+                                                alt="{{movie_review.movie.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{movie_review.movie.title}}">{{ movie_review.movie.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
                             <div class="entity-sort" id="musicWish">
                                 <h5 class="entity-sort__label">
                                     {% trans '想听的音乐' %}
@@ -217,7 +273,7 @@
                                     {{ wish_music_count }}
                                 </span>
                                 {% if wish_music_more %}
-                                <a href="{% url 'users:music_list' user.id 'wish' %}"
+                                <a href="{% url 'users:music_list' user.mastodon_username 'wish' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -258,7 +314,7 @@
                                     {{ do_music_count }}
                                 </span>
                                 {% if do_music_more %}
-                                <a href="{% url 'users:music_list' user.id 'do' %}"
+                                <a href="{% url 'users:music_list' user.mastodon_username 'do' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -295,7 +351,7 @@
                                     {{ collect_music_count }}
                                 </span>
                                 {% if collect_music_more %}
-                                <a href="{% url 'users:music_list' user.id 'collect' %}"
+                                <a href="{% url 'users:music_list' user.mastodon_username 'collect' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -324,6 +380,43 @@
                                 </ul>
                             </div>
 
+                            <div class="entity-sort" id="musicReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的音乐' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ music_reviews_count }}
+                                </span>
+                                {% if music_reviews_more %}
+                                <a href="{% url 'users:music_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for music_review in music_reviews %}
+                                    <li class="entity-sort__entity">
+                                        {% if music_review.type == 'album' %}
+                                        <a href="{% url 'music:retrieve_album' music_review.album.id %}">
+                                            <img src="{{ music_review.album.cover|thumb:'normal' }}"
+                                                alt="{{music_review.album.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{music_review.album.title}}">{{ music_review.album.title }}</span>
+                                        </a>
+                                        {% else %}
+                                        <a href="{% url 'music:retrieve_song' music_review.song.id %}">
+                                            <img src="{{ music_review.song.cover|thumb:'normal' }}"
+                                                alt="{{music_review.song.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{music_review.song.title}}">{{ music_review.song.title }}</span>
+                                        </a>
+                                        {% endif %}
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
                             <div class="entity-sort" id="gameWish">
                                 <h5 class="entity-sort__label">
                                     {% trans '想玩的游戏' %}
@@ -332,7 +425,7 @@
                                     {{ wish_game_count }}
                                 </span>
                                 {% if wish_game_more %}
-                                <a href="{% url 'users:game_list' user.id 'wish' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'users:game_list' user.mastodon_username 'wish' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                             
                                 <ul class="entity-sort__entity-list">
@@ -360,7 +453,7 @@
                                     {{ do_game_count }}
                                 </span>
                                 {% if do_game_more %}
-                                <a href="{% url 'users:game_list' user.id 'do' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'users:game_list' user.mastodon_username 'do' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                             
                                 <ul class="entity-sort__entity-list">
@@ -388,7 +481,7 @@
                                     {{ collect_game_count }}
                                 </span>
                                 {% if collect_game_more %}
-                                <a href="{% url 'users:game_list' user.id 'collect' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'users:game_list' user.mastodon_username 'collect' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                             
                                 <ul class="entity-sort__entity-list">
@@ -407,411 +500,174 @@
                                 </ul>
                             </div>
 
-                        </div>
-                            
-                            {% if user == request.user %}
-                                
-                            <div class="entity-sort-control">
-                                <div class="entity-sort-control__button" id="sortEditButton">
-                                    <span class="entity-sort-control__text" id="sortEditText">
-                                        {% trans '编辑布局' %}
-                                    </span>
-                                    <span class="entity-sort-control__text" id="sortSaveText" style="display: none;">
-                                        {% trans '保存' %}
-                                    </span>
-                                    <span class="icon-edit" id="sortEditIcon">
-                                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 383.947 383.947">
-                                            <polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893    " />
-                                            <path
-                                                d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04     C386.027,77.92,386.027,64.373,377.707,56.053z" />
-                                        </svg>
-                                    </span>
-                                    <span class="icon-save" id="sortSaveIcon" style="display: none;">
-                                        <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 384 384" >
-                                            <path
-                                                d="M298.667,0h-256C19.093,0,0,19.093,0,42.667v298.667C0,364.907,19.093,384,42.667,384h298.667    C364.907,384,384,364.907,384,341.333v-256L298.667,0z M192,341.333c-35.307,0-64-28.693-64-64c0-35.307,28.693-64,64-64    s64,28.693,64,64C256,312.64,227.307,341.333,192,341.333z M256,128H42.667V42.667H256V128z" />
-                                        </svg>
-                                    </span>
-                                </div>
-                                <div class="entity-sort-control__button" id="sortExitButton" style="display: none;">
-                                    <span class="entity-sort-control__text">
-                                        {% trans '取消' %}
-                                    </span>
-                                </div>
-                            </div>
-                            <div class="entity-sort-control__button entity-sort-control__button--float-right" id="toggleDisplayButtonTemplate" style="display: none;">
-                                <span class="showText" style="display: none;">
-                                    {% trans '显示' %}
+                            <div class="entity-sort" id="gameReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的游戏' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ game_reviews_count }}
                                 </span>
-                                <span class="hideText" style="display: none;">
-                                    {% trans '隐藏' %}
-                                </span>
-                            </div>
-                            <form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
-                                {% csrf_token %}
-                                <input type="hidden" name="layout">
-                            </form>
-                            <script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.10.0/html5sortable.min.js"
-                                integrity="sha512-tBlVMq89XaEC9iU5LyRjP2Vxs8SmVhEHGbv2Co6SbGa14Wsxy2qZN0jadrN+Xn5AifORaUbvZcG21/ExcNfWDA=="
-                                crossorigin="anonymous"></script>
-                            <script src="{% static 'js/sort_layout.js' %}"></script>
-                            {% endif %}
-                            <script>
-                                const initialLayoutData = JSON.parse("{{ layout|escapejs }}");
-                                // initialize sort element visibility and order
-                                    initialLayoutData.forEach(elem => {
-                                        // False to false, True to true
-                                        if (elem.visibility === "False") {
-                                            elem.visibility = false;
-                                        } else {
-                                            elem.visibility = true;
-                                        }
-                                        // set visiblity
-                                        $('#' + elem.id).data('visibility', elem.visibility);
-                                        if (!elem.visibility) {
-                                            $('#' + elem.id).hide();
-                                        }
-                                        // order
-                                        $('#' + elem.id).appendTo('.main-section-wrapper');
-                                    });
-                            </script>
-
-                    </div>
-
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
+                                {% if game_reviews_more %}
+                                <a href="{% url 'users:game_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
-                            </div>
-                        </div>
+                                <ul class="entity-sort__entity-list">
+                                    {% for game_review in game_reviews %}
+                                    <li class="entity-sort__entity">
 
-                        <!-- contains relations, reports, and upload excel entry -->
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
+                                        <a href="{% url 'games:retrieve' game_review.game.id %}">
+                                            <img src="{{ game_review.game.cover|thumb:'normal' }}"
+                                                alt="{{game_review.game.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{game_review.game.title}}">{{ game_review.game.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
+                            <div class="entity-sort" id="collectionCreated">
+                                <h5 class="entity-sort__label">
+                                    {% trans '创建的收藏单' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ collections_count }}
+                                </span>
+                                {% if collections_more %}
+                                <a href="{% url 'users:collection_list' user.mastodon_username %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+                                {% if user == request.user %}
+                                <a href="{% url 'collection:create' %}"class="entity-sort__more-link">{% trans '新建' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for collection in collections %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            <img src="{{ collection.cover|thumb:'normal' }}"
+                                                alt="{{collection.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{collection.title}}">{{ collection.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
+                            <div class="entity-sort" id="collectionMarked">
+                                <h5 class="entity-sort__label">
+                                    {% trans '关注的收藏单' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ marked_collections_count }}
+                                </span>
+                                {% if marked_collections_more %}
+                                <a href="{% url 'users:marked_collection_list' user.mastodon_username %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for collection in marked_collections %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            <img src="{{ collection.cover|thumb:'normal' }}"
+                                                alt="{{collection.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{collection.title}}">{{ collection.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
+                        </div>
+                            
+                        {% if user == request.user %}
+                            
+                        <div class="entity-sort-control">
+                            <div class="entity-sort-control__button" id="sortEditButton">
+                                <span class="entity-sort-control__text" id="sortEditText">
+                                    {% trans '编辑布局' %}
+                                </span>
+                                <span class="entity-sort-control__text" id="sortSaveText" style="display: none;">
+                                    {% trans '保存' %}
+                                </span>
+                                <span class="icon-edit" id="sortEditIcon">
+                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 383.947 383.947">
+                                        <polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893    " />
+                                        <path
+                                            d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04     C386.027,77.92,386.027,64.373,377.707,56.053z" />
+                                    </svg>
+                                </span>
+                                <span class="icon-save" id="sortSaveIcon" style="display: none;">
+                                    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 384 384" >
+                                        <path
+                                            d="M298.667,0h-256C19.093,0,0,19.093,0,42.667v298.667C0,364.907,19.093,384,42.667,384h298.667    C364.907,384,384,364.907,384,341.333v-256L298.667,0z M192,341.333c-35.307,0-64-28.693-64-64c0-35.307,28.693-64,64-64    s64,28.693,64,64C256,312.64,227.307,341.333,192,341.333z M256,128H42.667V42.667H256V128z" />
                                     </svg>
                                 </span>
                             </div>
-                            <div class="relation-dropdown__body">
-                                <div
-                                    class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-
-                                </div>
-                                
-                                <!-- import douban data -->                                
-                                {% if user == request.user %}                                    
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                                    <div class="import-panel">
-                                        <h5 class="import-panel__label">{% trans '导入豆瓣标记数据' %}</h5>
-                                        <span id="importHelp" class="import-panel__help">?</span>
-                                        <div class="import-panel__body">
-                                            <form action="{% url 'sync:douban' %}" method="POST" enctype="multipart/form-data" >
-
-                                                {% csrf_token %}
-                                                <input type="hidden" name="user" value="{{ request.user.id }}">
-                                                <span>{% trans '导入:' %}</span>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_book" id="syncBook">
-                                                    <label for="syncBook">{% trans '书' %}</label>
-                                                </div>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_movie" id="syncMovie">
-                                                    <label for="syncMovie">{% trans '电影' %}</label>
-                                                </div>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_music" id="syncMusic">
-                                                    <label for="syncMusic">{% trans '音乐' %}</label>
-                                                </div>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_game" id="syncGame">
-                                                    <label for="syncGame">{% trans '游戏' %}</label>
-                                                </div>
-                                                <div></div>
-                                                <span>{% trans '覆盖:' %}</span>
-                                                <div class="import-panel__checkbox import-panel__checkbox--last">
-                                                    <input type="checkbox" name="overwrite" id="overwrite">
-                                                    <label for="overwrite">{% trans '覆盖原有标记' %}</label>
-                                                </div>
-                                                <span id="overwriteHelp" class="import-panel__help">?</span>
-                                                <div></div>
-                                                <span>{% trans '可见性:' %}</span>
-                                                <div class="import-panel__checkbox import-panel__checkbox--last">
-                                                    <input type="checkbox" name="default_public" id="visibility">
-                                                    <label for="visibility">{% trans '公开' %}</label>
-                                                </div>
-                                                <span id="visibilityHelp" class="import-panel__help">?</span>
-                                                <div></div>
-                                                <div class="import-panel__file-input">
-                                                    <input type="file" name="file" id="excelFile" required accept=".xlsx">
-                                                </div>
-                                                <input type="submit" class="import-panel__button" value="{% trans '导入' %}" id="uploadBtn"
-                                                    {% if not latest_task is None and not latest_task.is_finished %}
-                                                    disabled
-                                                    {% endif %}
-                                                >
-                                            </form>
-                                            <div class="import-panel__progress"
-                                                {% if latest_task.is_finished or latest_task is None %}
-                                                style="display: none;"
-                                                {% endif %}
-                                            >
-                                                <label for="importProgress">{% trans '进度' %}</label>
-                                                <progress id="importProgress" value="{{ latest_task.finished_items }}" max="{{ latest_task.total_items }}"></progress>
-                                                <span class="float-right" id="progressPercent">{{ latest_task.get_progress | floatformat:"0" }}%</span>
-                                                <span class="clearfix"></span>
-                                            </div>
-                                            <div class="import-panel__last-task"
-                                                {% if not latest_task.is_finished %}`
-                                                style="display: none;"
-                                                {% endif %}
-                                            >
-                                                {% trans '上次导入:' %}
-                                                <span class="index">{% trans '总数' %} <span id="lastTaskTotalItems">{{ latest_task.total_items }}</span></span>
-                                                <span class="index">{% trans '同步' %} <span id="lastTaskSuccessItems">{{ latest_task.success_items }}</span></span>
-                                                <span class="index">{% trans '状态' %} <span id="lastTaskStatus">{{ latest_task.get_status_emoji }}</span></span>
-                                                <div class="import-panel__fail-urls"
-                                                {% if not latest_task.failed_urls %}
-                                                    style="display: none;"
-                                                {% endif %}
-                                                >
-                                                    <span>
-                                                        {% trans '失败条目链接' %}
-                                                    </span>
-                                                    <a class="float-right" style="cursor: pointer;" id="failedUrlsBtn">
-                                                        ▶
-                                                    </a>
-                                                    <script>
-                                                        $("#failedUrlsBtn").data("collapse", true);
-                                                        $("#failedUrlsBtn").click(()=>{
-                                                            const btn = $("#failedUrlsBtn");
-                                                            if(btn.data("collapse") == true) {
-                                                                btn.data("collapse", false);
-                                                                btn.text("▼");
-                                                                $("#failedUrls").show();
-                                                            } else {
-                                                                btn.data("collapse", true);
-                                                                btn.text("▶");
-                                                                $("#failedUrls").hide();
-                                                            }
-                                                        });
-                                                    </script>
-                                                    <span class="clearfix"></span>
-                                                    <ul id="failedUrls" style="display: none;">
-                                                        {% for url in latest_task.failed_urls %}
-                                                        <li>{{ url }}</li>
-                                                        {% endfor %}
-                                                    </ul>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-                                {% endif %}
-                                    
-                                <div
-                                    class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                                    {% if request.user.is_staff and request.user == user%}
-                                    <div class="report-panel">
-                                        <h5 class="report-panel__label">{% trans '举报信息' %}</h5>
-                                        <a class="report-panel__all-link"
-                                            href="{% url 'users:manage_report' %}">全部举报</a>
-                                        <div class="report-panel__body">
-                                            <ul class="report-panel__report-list">
-                                                {% for report in reports %}
-                                                <li class="report-panel__report">
-                                                    <a href="{% url 'users:home' report.submit_user.id %}"
-                                                        class="report-panel__user-link">{{ report.submit_user }}</a>{% trans '举报了' %}<a
-                                                        href="{% url 'users:home' report.reported_user.id %}"
-                                                        class="report-panel__user-link">{{ report.reported_user }}</a>
-                                                </li>
-                                                {% empty %}
-                                                <div>{% trans '暂无新举报' %}</div>
-                                                {% endfor %}
-    
-                                            </ul>
-                                        </div>
-                                    </div>
-                                    {% endif %}
-                                </div>
+                            <div class="entity-sort-control__button" id="sortExitButton" style="display: none;">
+                                <span class="entity-sort-control__text">
+                                    {% trans '取消' %}
+                                </span>
                             </div>
                         </div>
+                        <div class="entity-sort-control__button entity-sort-control__button--float-right" id="toggleDisplayButtonTemplate" style="display: none;">
+                            <span class="showText" style="display: none;">
+                                {% trans '显示' %}
+                            </span>
+                            <span class="hideText" style="display: none;">
+                                {% trans '隐藏' %}
+                            </span>
+                        </div>
+                        <form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
+                            {% csrf_token %}
+                            <input type="hidden" name="layout">
+                        </form>
+                        <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/html5sortable.min.js" crossorigin="anonymous"></script>
+                        <script src="{% static 'js/sort_layout.js' %}"></script>
+                        {% endif %}
+                        <script>
+                            const initialLayoutData = JSON.parse("{{ layout|escapejs }}");
+                            // initialize sort element visibility and order
+                                initialLayoutData.forEach(elem => {
+                                    // False to false, True to true
+                                    if (elem.visibility === "False") {
+                                        elem.visibility = false;
+                                    } else {
+                                        elem.visibility = true;
+                                    }
+                                    // set visiblity
+                                    $('#' + elem.id).data('visibility', elem.visibility);
+                                    if (!elem.visibility) {
+                                        $('#' + elem.id).hide();
+                                    }
+                                    // order
+                                    $('#' + elem.id).appendTo('.main-section-wrapper');
+                                });
+                        </script>
 
                     </div>
+
+                    {% include "partial/_sidebar.html" %}
                 </div>
             </section>
-
         </div>
-            
         {% include "partial/_footer.html" %}
     </div>
 
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <div id="queryProgressURL" data-url="{% url 'sync:progress' %}"></div>
-    <div id="querySyncInfoURL" data-url="{% url 'sync:last' %}"></div>
-    <!--current user mastodon id-->
-    
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-        
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-
     {% if unread_announcements %}
-    <div id="modals">
-        <style>
-            .bottom-link {
-                margin-top: 30px; text-align: center; margin-bottom: 5px;
-            }
-            .bottom-link a {
-                color: #ccc;
-            }
-        </style>
-        <div class="announcement-modal modal">
-            <div class="announcement-modal__head">
-                <h4 class="announcement-modal__title">{% trans '公告' %}</h4>
-        
-                <span class="announcement-modal__close-button modal-close">
-                    <span class="icon-cross">
-                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                            <polygon
-                                points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
-                            </polygon>
-                        </svg>
-                    </span>
-                </span>
-            </div>
-            <div class="announcement-modal__body">
-                <ul>
-                    {% for ann in unread_announcements %}
-                        <li class="announcement">
-                            <a href="{% url 'management:retrieve' ann.pk %}">
-                                <h5 class="announcement__title">{{ ann.title }}</h5>
-                            </a>
-                            <span class="announcement__datetime">{{ ann.created_time }}</span>
-                            <p class="announcement__content">{{ ann.get_plain_content | truncate:200 }}</p>
-                        </li>
-                        {% if not forloop.last %}
-                            <div class="dividing-line" style="border-top-style: dashed;"></div>
-                        {% endif %}
-                    {% endfor %}
-                </ul>
-                <div class="bottom-link">
-                    <a href="{% url 'management:list' %}">{% trans '查看全部公告' %}</a>
-                </div>
-            </div>
-        </div>
-    </div>
-    <div class="bg-mask"></div>
+    {% include "partial/_announcement.html" %}
     {% endif %}
-    
-    
-    <script>
-        // because the modal and mask elements only exist when there are new announcements
-        $(".announcement-modal").show();
-        $(".bg-mask").show();
-        $(".modal-close").on('click', function () {
-            $(this).parents(".modal").hide();
-            $(".bg-mask").hide();
-        });
-
-    </script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js"
-        integrity="sha512-2rNj2KJ+D8s1ceNasTIex6z4HWyOnEYLVC3FigGOmyQCZc2eBXKgOxQmo3oKLHyfcj53uz4QMsRCWNbLd32Q1g=="
-        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/tippy.js/6.3.1/tippy.umd.min.js"
-        integrity="sha512-Ns7w8bjVjVcBVa+k3XLt0ObfsG2LQfr573HoIYtC4wh8gUKLvCx+rlggxfvsHqup6jvMAEmBtYXmhcKHL+6R5A=="
-        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-    <script>
-        tippy('#importHelp', {
-            content: "{% trans '上传导入由<a href=\"https://github.com/doufen-org/tofu\" target=\"_blank\">豆伴</a>(豆坟)导出的Excel文件,<strong>请勿手动修改该文件</strong>。部分条目由于需要登陆无法自动同步。' %}",
-            interactive: true,
-            allowHTML: true,
-            duration: 0,
-        });
-        tippy('#overwriteHelp', {
-            content: "{% trans '在导入之前如果已经在本站标记了某一个条目,是否使用来自豆瓣的标记覆盖原有的。' %}",
-            interactive: true,
-            allowHTML: true,
-            duration: 0,
-        });
-        tippy('#visibilityHelp', {
-            content: "{% trans '所同步的标记可见性是否为公开;对于已有的标记即便覆盖也不会改变可见性。' %}",
-            interactive: true,
-            allowHTML: true,
-            duration: 0,
-        });
-    </script>
 </body>
-
-
 </html>
\ No newline at end of file
diff --git a/users/templates/users/home_anonymous.html b/users/templates/users/home_anonymous.html
new file mode 100644
index 00000000..946ac96e
--- /dev/null
+++ b/users/templates/users/home_anonymous.html
@@ -0,0 +1,17 @@
+{% load static %}
+{% load i18n %}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="refresh" content="0;URL={{ login_url }}" />
+    <title>{{ site_name }} - {{ username }}@{{ site }}</title>
+    <meta property="og:title" content="{{ site_name }} - {{ username }}@{{ site }}的书影音">
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="{{ request.build_absolute_uri }}">
+    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.jpg' %}">
+</head>
+<body>
+    <a href="https://{{ site }}/@{{ username }}" rel="me" style="display:none;">Mastodon homepage</a>
+</body>
+</html>
\ No newline at end of file
diff --git a/users/templates/users/item_list.html b/users/templates/users/item_list.html
new file mode 100644
index 00000000..321ad0fe
--- /dev/null
+++ b/users/templates/users/item_list.html
@@ -0,0 +1,94 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - {{ user.mastodon_username }} {{ list_title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+        
+            <section id="content" class="container">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            <div class="entity-list">
+
+                                <div class="set">
+                                    <h5 class="entity-list__title">
+                                        {{ user.mastodon_username }} {{ list_title }}
+                                    </h5>
+                                </div>
+                                <ul class="entity-list__entities">
+                                    {% for mark in marks %}
+                                    {% include "partial/list_item.html" with item=mark.item hide_category=True %}
+                                    {% empty %}
+                                    <div>{% trans '无结果' %}</div>
+                                    {% endfor %} 
+                                </ul>
+                            </div>
+                            <div class="pagination">
+                            
+                                {% if marks.pagination.has_prev %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ marks.previous_page_number }}"
+                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                {% endif %}
+                            
+                                {% for page in marks.pagination.page_range %}
+                            
+                                {% if page == marks.pagination.current_page %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                {% else %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}" class="pagination__page-link">{{ page }}</a>
+                                {% endif %}
+                            
+                                {% endfor %}
+                            
+                                {% if marks.pagination.has_next %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ marks.next_page_number }}"
+                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
+                                {% endif %}
+                            
+                            </div>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+</body>
+
+
+</html>
diff --git a/users/templates/users/login.html b/users/templates/users/login.html
index 27a0a0fa..d843543c 100644
--- a/users/templates/users/login.html
+++ b/users/templates/users/login.html
@@ -1,4 +1,3 @@
-
 {% load i18n %}
 {% load static %}
 <!DOCTYPE html>
@@ -6,74 +5,81 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="{% trans 'NiceDB - 登录' %}">
+    <meta property="og:title" content="{{ site_name }} - 联邦宇宙书影音游戏标注平台">
+    <meta name="description" property="og:description" content="{{ site_name }}致力于为联邦宇宙居民提供一个自由、开放、互联的书籍、电影、音乐和游戏收藏评论空间">
     <meta property="og:type" content="website">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.jpg' %}">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
+    <title>{{ site_name }} - {% trans '登录' %}</title>
+    {% include "partial/_common_libs.html" %}
+    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/js.cookie.min.js' %}"></script>
-    <title>{% trans 'NiceDB - 登录' %}</title>
-</head>
-<body>
-    <style>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js"></script>
+    <script> $(document).ready( function() { $('.delayed').remove(); $('#loginButton').prop("disabled", false); } ); </script>
+    <style type="text/css">
+        .delayed {
+          animation: 10s fadeIn;
+          animation-fill-mode: forwards;
+          visibility: hidden;
+        }
+        @keyframes fadeIn {
+          99% {
+            visibility: hidden;
+          }
+          100% {
+            visibility: visible;
+            opacity: 1;
+          }
+        }
+        input, input[type='text']:focus, input[type='search']:focus{
+            border: #84C2FB solid 1px;
+        }
+        input[type='submit'] {
+            background: #84C2FB;
+            border: #84C2FB solid 1px;
+        }
+        input:invalid#domain {
+            border: #F9A879 dashed 1px;
+        }
+        a {
+            color: #84C2FB;
+        }
         select {
             padding-left: 16px;
             padding-right: 16px;
             margin-bottom: 20px;;
         }
     </style>
+</head>
+<body>
     <div id="loginBox" class="box">
-
-            <img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
-        
-        <div id="loginButton">
-            
-            
+        <img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
+        <div>
             {% if user.is_authenticated %}
-            <a href="{% url 'common:home' %}" class="button">{% trans '前往我的主页' %}</a>
+            <a href="{% url 'common:home' %}" class="button">{% trans '前往首页' %}</a>
+            {% else %}
+            <form action="/users/connect">
+            {% if allow_any_site %}
+            <input type="search" name="domain" id="domain"
+                pattern="(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,})"
+                title="实例域名(不含@和@之前的部分),如mastodon.social"
+                placeholder="实例域名(不含@和@之前的部分),如mastodon.social"
+                autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
+            <input type='submit' value="{% trans '授权登录' %}" id="loginButton" disabled />
+            <br><a target="_blank" href="https://about.neodb.social/doc/howto/">{% trans '了解更多' %}</a>
+            <script type="text/javascript">if (Cookies.get('mastodon_domain')) $('#domain').val(Cookies.get('mastodon_domain'));</script>
             {% else %}            
-            <select name="sites" id="sitesSelect" placeholder="test">
+            <select name="domain" placeholder="test">
                 {% for site in sites %}
                 <option value="{{ site.domain_name }}" data-client-id="{{ site.client_id }}">@{{ site.domain_name }}</option>
                 {% endfor %}
             </select>
-            <button name='login'>{% trans '授权登录' %}</button>
+            <input type='submit' value="{% trans '授权登录' %}" id="loginButton" />
             {% endif %}
-                
+            </form>
+            {% endif %}
+            <div class="delayed">网页加载超时,请检查网络(翻墙)设置。</div>
         </div>
-
-    </div>
-    {% if not user.is_authenticated %}
-    
-    
-    <script>
-        {% if selected_site %}
-        $("#sitesSelect").val("{{ selected_site }}");
-        {% else %}
-        $("#sitesSelect").val($("#sitesSelect option:first").val());
-        {% endif %}
-        $("button[name=login]").click(function(e) {
-            e.preventDefault();
-            let selected =  $("#sitesSelect").find(":selected");
-            let client_id = selected.data("client-id");
-            let domain = selected.val();
-
-            Cookies.set('mastodon_domain', domain);
-            {% if debug %}
-            location.href = "https://" + domain + "/oauth/authorize?client_id=" + client_id + 
-                "&scope=read+write&redirect_uri=http://{{ request.get_host }}{% url 'users:OAuth2_login' %}" +
-                "&response_type=code";
-            {% else %}
-            location.href = "https://" + domain + "/oauth/authorize?client_id=" + client_id + 
-                "&scope=read+write&redirect_uri=https://{{ request.get_host }}{% url 'users:OAuth2_login' %}" +
-                "&response_type=code";
-            {% endif %}
-        });
-    </script>
-    {% endif %}
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/users/templates/users/manage_report.html b/users/templates/users/manage_report.html
index 35d3604b..f077a434 100644
--- a/users/templates/users/manage_report.html
+++ b/users/templates/users/manage_report.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 管理举报' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '管理举报' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
@@ -27,9 +27,9 @@
                         
                         {% for report in reports %}
                             <div class="report">
-                                <a href="{% url 'users:home' report.submit_user.id %}">{{ report.submit_user.username }}</a>
+                                <a href="{% url 'users:home' report.submit_user.mastodon_username %}">{{ report.submit_user.username }}</a>
                                 {% trans '举报了' %}
-                                <a href="{% url 'users:home' report.reported_user.id %}">{{ report.reported_user.username }}</a>
+                                <a href="{% url 'users:home' report.reported_user.mastodon_username %}">{{ report.reported_user.username }}</a>
                                 @{{ report.submitted_time }}
                                 
                                 {% if report.image %}
@@ -49,12 +49,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/users/templates/users/movie_list.html b/users/templates/users/movie_list.html
deleted file mode 100644
index fa21d29a..00000000
--- a/users/templates/users/movie_list.html
+++ /dev/null
@@ -1,285 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load humanize %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            <a href="{% url 'movies:retrieve' mark.movie.id %}">
-                                                <img src="{{ mark.movie.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                <a href="{% url 'movies:retrieve' mark.movie.id %}" class="entity-list__entity-link">
-                                                    {% if mark.movie.season %}
-                                                    {{ mark.movie.title }} {% trans '第' %}{{ mark.movie.season|apnumber }}{% trans '季' %} {{ mark.movie.orig_title }} Season
-                                                    {{ mark.movie.season }}
-                                                    {% if mark.movie.year %}({{ mark.movie.year }}){% endif %}
-                                                        
-                                                    {% else %}
-                                                    {{ mark.movie.title }} {{ mark.movie.orig_title }}
-                                                    {% if mark.movie.year %}({{ mark.movie.year }}){% endif %}
-                                                    {% endif %}
-                                                </a>
-                                                <a href="{{ mark.movie.source_url }}">
-                                                    <span class="source-label source-label__{{ mark.movie.source_site }}">{{ mark.movie.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-
-                                            
-                                        {% if mark.movie.director %}{% trans '导演' %}
-                                        {% for director in mark.movie.director %}
-                                        {{ director }}{% if not forloop.last %} {% endif %}
-                                        {% endfor %}/
-                                        {% endif %}
-        
-                                        {% if mark.movie.genre %}{% trans '类型' %}
-                                        {% for genre in mark.movie.get_genre_display %}
-                                        {{ genre }}{% if not forloop.last %} {% endif %}
-                                        {% endfor %}/
-                                        {% endif %}
-
-                                        {% if mark.movie.other_title %}{% trans '又名' %}
-                                        {% for other_title in mark.movie.other_title %}
-                                        {{ other_title }}{% if not forloop.last %} {% endif %}
-                                        {% endfor %}
-                                        {% endif %}
-                                            </span>       
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                            {% if mark.movie.actor %}{% trans '主演' %}
-                                            {% for actor in mark.movie.actor %}
-                                            <span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
-                                            {% if forloop.counter <= 5 %}
-                                                {% if not forloop.counter == 5 %} / {% endif %}
-                                            {% endif %}
-                                            {% endfor %}
-                                            {% endif %}
-                                            </span>                                                
-                                            <p class="entity-list__entity-brief">
-                                                {{ mark.movie.brief }}
-                                            </p>
-                                            <div class="tag-collection">
-                                                {% for tag_dict in mark.movie.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path
-                                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/music_list.html b/users/templates/users/music_list.html
deleted file mode 100644
index 18c0fe74..00000000
--- a/users/templates/users/music_list.html
+++ /dev/null
@@ -1,290 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load humanize %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                    
-                                    {% with mark.music as music %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            {% if music.category_name|lower == 'album' %}
-                                            <a href="{% url 'music:retrieve_album' music.id %}">
-                                                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                            {% elif music.category_name|lower == 'song' %}
-                                            <a href="{% url 'music:retrieve_song' music.id %}">
-                                                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                            {% endif %}
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                {% if music.category_name|lower == 'album' %}
-                                                <a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
-                                                    {{ music.title }}
-                                                </a>
-                                                {% elif music.category_name|lower == 'song' %}
-                                                <a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
-                                                    {{ music.title }}
-                                                </a>
-                                                {% endif %}
-                                                <a href="{{ music.source_url }}">
-                                                    <span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            <span class="entity-list__entity-info ">
-                                                {% if music.artist %}{% trans '艺术家' %}
-                                                {% for artist in music.artist %}
-                                                <span>{{ artist }}</span>
-                                                {% if not forloop.last %} {% endif %}
-                                                {% endfor %}
-                                                {% endif %}
-                                            
-                                                {% if music.genre %}/ {% trans '流派' %}
-                                                {{ music.genre }}
-                                                {% endif %}
-                                            
-                                                {% if music.release_date %}/ {% trans '发行日期' %}
-                                                {{ music.release_date }}
-                                                {% endif %}
-                                            </span>
-                                            {% if music.brief %}
-                                            <p class="entity-list__entity-brief">
-                                                {{ music.brief }}
-                                            </p>
-                                            {% elif music.category_name|lower == 'album' %}
-                                            <p class="entity-list__entity-brief">
-                                                {% trans '曲目:' %}{{ music.track_list }}
-                                            </p>
-                                            {% else %}
-                                            <!-- song -->
-                                            <p class="entity-list__entity-brief">
-                                                {% trans '所属专辑:' %}{{ music.album }}
-                                            </p>
-                                            {% endif %}
-                                            <div class="tag-collection">
-                                                {% for tag_dict in music.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-
-                                    {% endwith %}
-
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/preferences.html b/users/templates/users/preferences.html
new file mode 100644
index 00000000..cca66196
--- /dev/null
+++ b/users/templates/users/preferences.html
@@ -0,0 +1,93 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - 设置</title>
+    {% include "partial/_common_libs.html" %}
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            <form action="{% url 'users:preferences' %}" method="POST">
+                                <div class="tools-section-wrapper">
+                                    <div class="import-panel">
+                                        <h5 class="import-panel__label">{% trans '使用偏好设置' %}</h5>
+                                        <div class="import-panel__body">
+                                            {% csrf_token %}
+                                            <span>{% trans '新标记默认可见性:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <label for="id_visibility_0"><input type="radio" name="default_visibility" value="0" required="" id="id_visibility_0" {%if request.user.preference.default_visibility == 0 %}checked{% endif %}>
+                                                 公开</label>
+                                                <label for="id_visibility_1"><input type="radio" name="default_visibility" value="1" required="" id="id_visibility_1" {%if request.user.preference.default_visibility == 1 %}checked{% endif %}>
+                                                 仅关注者</label>
+                                                <label for="id_visibility_2"><input type="radio" name="default_visibility" value="2" required="" id="id_visibility_2" {%if request.user.preference.default_visibility == 2 %}checked{% endif %}>
+                                                 仅自己</label>
+                                            </div>
+                                            <br>
+                                            <span>{% trans '登录后显示个人主页:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="classic_homepage" id="classic_homepage" {%if request.user.preference.classic_homepage %}checked{% endif %}>
+                                                <label for="classic_homepage">{% trans '默认登录后显示好友动态,如果希望登录后显示原版风格个人主页可选中此处' %}</label>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="tools-section-wrapper" style="margin-top: 2em;">
+                                    <div class="import-panel">
+                                        <h5 class="import-panel__label">{% trans '社交网络分享相关设置' %}</h5>
+                                        <div class="import-panel__body">
+                                            {% csrf_token %}
+                                            <span>{% trans '在联邦网络上以公开方式分享的帖文是否发布到公共时间轴上:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="mastodon_publish_public" id="visibility" {%if request.user.preference.mastodon_publish_public %}checked{% endif %}>
+                                                <label for="visibility">{% trans '选中时为public,未选中时为unlisted' %}</label>
+                                            </div>
+                                            <br><br>
+                                            <span>{% trans '在联邦网络上分享帖文时附加标签:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input name="mastodon_append_tag" id="tag" placeholder="#我的书影音" value="{{ request.user.preference.mastodon_append_tag }}" >
+                                                <label for="tag">{% trans '输入标签文字会被添加到帖文结尾' %}</label>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div style="margin-top: 2em;">
+                                    <input type="submit" class="import-panel__button" value="{% trans '保存' %}">
+                                </div>
+                            </form>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+
+        </div>
+
+        {% include "partial/_footer.html" %}
+    </div>
+
+</body>
+
+
+</html>
\ No newline at end of file
diff --git a/users/templates/users/register.html b/users/templates/users/register.html
index 9b750a66..cb63b5c6 100644
--- a/users/templates/users/register.html
+++ b/users/templates/users/register.html
@@ -6,11 +6,10 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
+    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
-    <title>{% trans 'NiceDB - 注册' %}</title>
+    <title>{{ site_name }} - {% trans '注册' %}</title>
 </head>
 
 <body>
@@ -19,18 +18,18 @@
         <img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
 
         <div id="loginButton">
-            <p>欢迎来到NiceDB书影音!</p>
+            <p>欢迎来到{{ site_name }}!</p>
             <p>
-                NiceDB书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
-                这里仍是一片处女地,丰富的内容需要大家共同创造。
-                请注意虽然您可以随意发表任何言论,但试图添加垃圾数据到公共数据领域(如添加不存在的乱码的书籍)将会受到制裁!
-                BTW欧盟惯例本站使用了Cookie,请理解!
+                {{ site_name }}还在不断完善中,丰富的内容需要大家共同创造。
+                试图添加垃圾数据(如添加信息混乱或缺失的书籍、以推广为主要目的的评论)将会受到严肃处理。
+                {{ site_name }}继承了联邦宇宙的用户关系,比如您在联邦宇宙屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
+                本站为非盈利站点,cookie和其他数据保管使用原则请参阅<a href="/announcement/data-policy/">站内公告</a>。
             </p>
             <p>
-                此外NiceDB书影音现处于“公开阿尔法测试”阶段,您的数据存在丢失的可能。使用过程中遇到的问题或者Bug欢迎向<a href="https://donotban.com/@whitiewhite">作者</a>提出。
+                此外,{{ site_name }}现处于测试阶段,疏漏在所难免,请妥善备份您的数据。
+                使用过程中遇到的问题或者错误欢迎向<a href="{{ support_link }}">维护者</a>提出。感谢理解和支持!
             </p>
-            <form action="{% url 'users:register' %}" method="post">
-                {% csrf_token %}
+            <form action="{% url 'common:home' %}">
                 <input type="submit" class="button" value="{% trans 'Cut the sh*t and get me in!' %}">
             </form>
 
diff --git a/users/templates/users/relation_list.html b/users/templates/users/relation_list.html
index 6984bbd3..ae14e2ea 100644
--- a/users/templates/users/relation_list.html
+++ b/users/templates/users/relation_list.html
@@ -11,11 +11,13 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     {% if is_followers_page %}
-    <title>{% trans 'NiceDB - 被他们关注' %}</title>
+    <title>{{ site_name }} - {% trans '被他们关注' %}</title>
     {% else %}
-    <title>{% trans 'NiceDB - 关注的人' %}</title>
+    <title>{{ site_name }} - {% trans '关注的人' %}</title>
     {% endif %}
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'js/mastodon.js' %}"></script>
 
     {% if is_followers_page %}
@@ -23,8 +25,6 @@
     {% else %}
     <script src="{% static 'js/following_list.js' %}"></script>
     {% endif %}
-
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
 <body>
@@ -64,7 +64,7 @@
                                 <div class="user-profile__header">
                                     <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
                                     <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
+                                    <a href="{% url 'users:home' user.mastodon_username %}">
                                         <h5 class="user-profile__username mast-displayname"></h5>
                                     </a>
                                 </div>
@@ -94,7 +94,7 @@
                                         <h5 class="user-relation__label">
                                             {% trans '关注的人' %}
                                         </h5>
-                                        <a href="{% url 'users:following' user.id %}"
+                                        <a href="{% url 'users:following' user.mastodon_username %}"
                                             class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
                                         <ul class="user-relation__related-user-list mast-following">
                                             <li class="user-relation__related-user">
@@ -110,7 +110,7 @@
                                         <h5 class="user-relation__label">
                                             {% trans '被他们关注' %}
                                         </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
+                                        <a href="{% url 'users:followers' user.mastodon_username %}"
                                             class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
                                         <ul class="user-relation__related-user-list mast-followers">
                                             <li class="user-relation__related-user">
@@ -140,7 +140,7 @@
     {% if user == request.user %}
     <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
     {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
+    <div id="userMastodonID" hidden="true"></div>
     {% endif %}
     <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
     <div id="spinner" hidden>
diff --git a/users/templates/users/report.html b/users/templates/users/report.html
index 5f60738e..ea47b64a 100644
--- a/users/templates/users/report.html
+++ b/users/templates/users/report.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 举报用户' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '举报用户' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
@@ -38,12 +38,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/users/templates/users/tags.html b/users/templates/users/tags.html
new file mode 100644
index 00000000..2d9c738f
--- /dev/null
+++ b/users/templates/users/tags.html
@@ -0,0 +1,110 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - 我的标签</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="entity-reviews">                                
+                                <div class="tag-collection entity-reviews__review-list">
+                                    <h5>{% trans '书籍' %}</h5>
+                                    {% for v in book_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:book_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+				    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+
+                                    <h5>{% trans '电影和剧集' %}</h5>
+                                    {% for v in movie_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:movie_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+                                    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+
+                                    <h5>{% trans '音乐' %}</h5>
+                                    {% for v in music_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:music_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+                                    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+
+                                    <h5>{% trans '游戏' %}</h5>
+                                    {% for v in game_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:game_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+                                    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+
+    <script>
+
+    </script>
+</body>
+
+
+</html>
diff --git a/users/urls.py b/users/urls.py
index 121c1afe..3b0d2b34 100644
--- a/users/urls.py
+++ b/users/urls.py
@@ -5,22 +5,38 @@ app_name = 'users'
 urlpatterns = [
     path('login/', login, name='login'),
     path('register/', register, name='register'),
+    path('connect/', connect, name='connect'),
+    path('reconnect/', reconnect, name='reconnect'),
+    path('data/', data, name='data'),
+    path('data/import_goodreads', import_goodreads, name='import_goodreads'),
+    path('data/import_douban', import_douban, name='import_douban'),
+    path('data/export_reviews', export_reviews, name='export_reviews'),
+    path('data/export_marks', export_marks, name='export_marks'),
+    path('data/sync_mastodon', sync_mastodon, name='sync_mastodon'),
+    path('data/reset_visibility', reset_visibility, name='reset_visibility'),
+    path('data/clear_data', clear_data, name='clear_data'),
+    path('preferences/', preferences, name='preferences'),
     path('logout/', logout, name='logout'),
-    path('delete/', delete, name='delete'),
     path('layout/', set_layout, name='set_layout'),
     path('OAuth2_login/', OAuth2_login, name='OAuth2_login'),
-    path('<int:id>/', home, name='home'),
-    path('<int:id>/followers/', followers, name='followers'),
-    path('<int:id>/following/', following, name='following'),
-    path('<int:id>/book/<str:status>/', book_list, name='book_list'),
-    path('<int:id>/movie/<str:status>/', movie_list, name='movie_list'),
-    path('<int:id>/music/<str:status>/', music_list, name='music_list'),
-    path('<int:id>/game/<str:status>/', game_list, name='game_list'),
+    path('<int:id>/', home_redirect, name='home_redirect'),
+    # path('<int:id>/followers/', followers, name='followers'),
+    # path('<int:id>/following/', following, name='following'),
+    # path('<int:id>/collections/', collection_list, name='collection_list'),
+    # path('<int:id>/book/<str:status>/', book_list, name='book_list'),
+    # path('<int:id>/movie/<str:status>/', movie_list, name='movie_list'),
+    # path('<int:id>/music/<str:status>/', music_list, name='music_list'),
+    # path('<int:id>/game/<str:status>/', game_list, name='game_list'),
     path('<str:id>/', home, name='home'),
     path('<str:id>/followers/', followers, name='followers'),
     path('<str:id>/following/', following, name='following'),
+    path('<str:id>/tags/', tag_list, name='tag_list'),
+    path('<str:id>/collections/', collection_list, name='collection_list'),
+    path('<str:id>/collections/marked/', marked_collection_list, name='marked_collection_list'),
     path('<str:id>/book/<str:status>/', book_list, name='book_list'),
     path('<str:id>/movie/<str:status>/', movie_list, name='movie_list'),
+    path('<str:id>/music/<str:status>/', music_list, name='music_list'),
+    path('<str:id>/game/<str:status>/', game_list, name='game_list'),
     path('report/', report, name='report'),
     path('manage_report/', manage_report, name='manage_report'),
 ]
diff --git a/users/views.py b/users/views.py
index ff77d066..e216f806 100644
--- a/users/views.py
+++ b/users/views.py
@@ -5,11 +5,10 @@ from django.contrib import auth
 from django.contrib.auth import authenticate
 from django.core.paginator import Paginator
 from django.utils.translation import gettext_lazy as _
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.db.models import Count
 from .models import User, Report, Preference
 from .forms import ReportForm
-from mastodon.auth import *
 from mastodon.api import *
 from mastodon import mastodon_request_included
 from common.config import *
@@ -25,156 +24,69 @@ from movies.forms import MovieMarkStatusTranslator
 from music.forms import MusicMarkStatusTranslator
 from games.forms import GameMarkStatusTranslator
 from mastodon.models import MastodonApplication
+from mastodon.api import verify_account
+from django.conf import settings
+from urllib.parse import quote
+import django_rq
+from .account import *
+from .data import *
+from datetime import timedelta
+from django.utils import timezone
+import json
+from django.contrib import messages
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, SongMark, AlbumReview, SongReview
+from collection.models import Collection
+from common.importers.goodreads import GoodreadsImporter
+from common.importers.douban import DoubanImporter
 
 
-# Views
-########################################
-
-# no page rendered
-@mastodon_request_included
-def OAuth2_login(request):
-    """ oauth authentication and logging user into django system """
-    if request.method == 'GET':
-        code = request.GET.get('code')
-        site = request.COOKIES.get('mastodon_domain')
-
-        # Network IO
-        try:
-            token = obtain_token(site, request, code)
-        except ObjectDoesNotExist:
-            return HttpResponseBadRequest("Mastodon site not registered")
-        if token:
-            # oauth is completed when token aquired
-            user = authenticate(request, token=token, site=site)
-            if user:
-                auth_login(request, user, token)
-                if request.session.get('next_url') is not None:
-                    response = redirect(request.session.get('next_url'))
-                    del request.session['next_url']
-                else:
-                    response = redirect(reverse('common:home'))
-                    
-                response.delete_cookie('mastodon_domain')
-                return response
-            else:
-                # will be passed to register page
-                request.session['new_user_token'] = token
-                return redirect(reverse('users:register'))
-        else:
-            return render(
-                request,
-                'common/error.html',
-                {
-                    'msg': _("认证失败😫")
-                }
-            )
-    else:
-        return HttpResponseBadRequest()
+def render_user_not_found(request):
+    msg = _("😖哎呀,这位用户还没有加入本站,快去联邦宇宙呼唤TA来注册吧!")
+    sec_msg = _("")
+    return render(
+        request,
+        'common/error.html',
+        {
+            'msg': msg,
+            'secondary_msg': sec_msg,
+        }
+    )
 
 
-# the 'login' page that user can see
-def login(request):
-    if request.method == 'GET':
-        selected_site = request.GET.get('site', default='')
+def home_redirect(request, id):
+    try:
+        query_kwargs = {'pk': id}
+        user = User.objects.get(**query_kwargs)
+        return redirect(reverse("users:home", args=[user.mastodon_username]))
+    except Exception:
+        return redirect(settings.LOGIN_URL)
 
-        sites = MastodonApplication.objects.all().order_by("domain_name")
 
-        # store redirect url in the cookie
-        if request.GET.get('next'):
-            request.session['next_url'] = request.GET.get('next')
-
-        return render(
-            request,
-            'users/login.html',
-            {
-                'sites': sites,
-                'selected_site': selected_site,
-            }
-        )
-    else:
-        return HttpResponseBadRequest()
+def home_anonymous(request, id):
+    login_url = settings.LOGIN_URL + "?next=" + request.get_full_path()
+    try:
+        username = id.split('@')[0]
+        site = id.split('@')[1]
+        return render(request, 'users/home_anonymous.html', {
+                      'login_url': login_url,
+                      'username': username,
+                      'site': site,
+                      })
+    except Exception:
+        return redirect(login_url)
 
 
 @mastodon_request_included
-@login_required
-def logout(request):
-    if request.method == 'GET':
-        revoke_token(request.user.mastodon_site, request.session['oauth_token'])
-        auth_logout(request)
-        return redirect(reverse("users:login"))
-    else:
-        return HttpResponseBadRequest()
-
-
-@mastodon_request_included
-def register(request):
-    """ register confirm page """
-    if request.method == 'GET':
-        if request.session.get('oauth_token'):
-            return redirect(reverse('common:home'))
-        elif request.session.get('new_user_token'):
-            return render(
-                request,
-                'users/register.html'
-            )
-        else:
-            return HttpResponseBadRequest()
-    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'],
-            mastodon_site=request.COOKIES['mastodon_domain'],
-        )
-        new_user.save()
-        del request.session['new_user_token']
-        auth_login(request, new_user, token)
-        response = redirect(reverse('common:home'))
-        response.delete_cookie('mastodon_domain')
-        return response
-    else:
-        return HttpResponseBadRequest()
-
-
-def delete(request):
-    raise NotImplementedError
-
-
-@mastodon_request_included
-@login_required
 def home(request, id):
+    if not request.user.is_authenticated:
+        return home_anonymous(request, id)
     if request.method == 'GET':
-        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,
-                }
-            )
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
 
         # access one's own home page
         if user == request.user:
@@ -194,23 +106,19 @@ def home(request, id):
             album_marks = request.user.user_albummarks.all()
             song_marks = request.user.user_songmarks.all()
             game_marks = request.user.user_gamemarks.all()
-
-            latest_task = user.user_synctasks.order_by("-id").first()
+            book_reviews = request.user.user_bookreviews.all()
+            movie_reviews = request.user.user_moviereviews.all()
+            album_reviews = request.user.user_albumreviews.all()
+            song_reviews = request.user.user_songreviews.all()
+            game_reviews = request.user.user_gamereviews.all()
 
         # visit other's home page
         else:
-            latest_task = None
             # no these value on other's home page
             reports = None
             unread_announcements = None
 
-            # cross site info for visiting other's home page
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
-            
-            # make queries
-            relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
-            if relation['blocked_by']:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -219,20 +127,28 @@ def home(request, id):
                         'msg': msg,
                     }
                 )
-            book_marks = BookMark.get_available_by_user(user, relation['following'])
-            movie_marks = MovieMark.get_available_by_user(user, relation['following'])
-            song_marks = SongMark.get_available_by_user(user, relation['following'])
-            album_marks = AlbumMark.get_available_by_user(user, relation['following'])
-            game_marks = GameMark.get_available_by_user(user, relation['following'])
+            is_following = request.user.is_following(user)
+            book_marks = BookMark.get_available_by_user(user, is_following)
+            movie_marks = MovieMark.get_available_by_user(user, is_following)
+            song_marks = SongMark.get_available_by_user(user, is_following)
+            album_marks = AlbumMark.get_available_by_user(user, is_following)
+            game_marks = GameMark.get_available_by_user(user, is_following)
+            book_reviews = BookReview.get_available_by_user(user, is_following)
+            movie_reviews = MovieReview.get_available_by_user(user, is_following)
+            song_reviews = SongReview.get_available_by_user(user, is_following)
+            album_reviews = AlbumReview.get_available_by_user(user, is_following)
+            game_reviews = GameReview.get_available_by_user(user, is_following)
 
+        collections = Collection.objects.filter(owner=user)
+        marked_collections = Collection.objects.filter(pk__in=CollectionMark.objects.filter(owner=user).values_list('collection', flat=True))
 
         # book marks
-        filtered_book_marks = filter_marks(book_marks, BOOKS_PER_SET, 'book')          
+        filtered_book_marks = filter_marks(book_marks, BOOKS_PER_SET, 'book')
         book_marks_count = count_marks(book_marks, "book")
 
         # movie marks
         filtered_movie_marks = filter_marks(movie_marks, MOVIES_PER_SET, 'movie')
-        movie_marks_count= count_marks(movie_marks, "movie")
+        movie_marks_count = count_marks(movie_marks, "movie")
 
         # game marks
         filtered_game_marks = filter_marks(game_marks, GAMES_PER_SET, 'game')
@@ -241,7 +157,6 @@ def home(request, id):
         # music marks
         filtered_music_marks = filter_marks([song_marks, album_marks], MUSIC_PER_SET, 'music')
         music_marks_count = count_marks([song_marks, album_marks], "music")
-     
 
         for mark in filtered_music_marks["do_music_marks"] +\
             filtered_music_marks["wish_music_marks"] +\
@@ -251,12 +166,12 @@ def home(request, id):
                 mark.type = "album"
             else:
                 mark.type = "song"
-        
-        try:
-            layout = user.preference.get_serialized_home_layout()
-        except ObjectDoesNotExist:
-            Preference.objects.create(user=user)
-            layout = user.preference.get_serialized_home_layout()
+
+        music_reviews = list(album_reviews.order_by("-edited_time")) + list(song_reviews.order_by("-edited_time"))
+        for review in music_reviews:
+            review.type = 'album' if review.__class__ == AlbumReview else 'song'
+
+        layout = user.get_preference().get_serialized_home_layout()
 
         return render(
             request,
@@ -271,10 +186,36 @@ def home(request, id):
                 **movie_marks_count,
                 **music_marks_count,
                 **game_marks_count,
+
+                'book_tags': BookTag.all_by_user(user)[:10] if user == request.user else [],
+                'movie_tags': MovieTag.all_by_user(user)[:10] if user == request.user else [],
+                'music_tags': AlbumTag.all_by_user(user)[:10] if user == request.user else [],
+                'game_tags': GameTag.all_by_user(user)[:10] if user == request.user else [],
+
+                'book_reviews': book_reviews.order_by("-edited_time")[:BOOKS_PER_SET],
+                'movie_reviews': movie_reviews.order_by("-edited_time")[:MOVIES_PER_SET],
+                'music_reviews': music_reviews[:MUSIC_PER_SET],
+                'game_reviews': game_reviews[:GAMES_PER_SET],
+                'book_reviews_more': book_reviews.count() > BOOKS_PER_SET,
+                'movie_reviews_more': movie_reviews.count() > MOVIES_PER_SET,
+                'music_reviews_more': len(music_reviews) > MUSIC_PER_SET,
+                'game_reviews_more': game_reviews.count() > GAMES_PER_SET,
+                'book_reviews_count': book_reviews.count(),
+                'movie_reviews_count': movie_reviews.count(),
+                'music_reviews_count': len(music_reviews),
+                'game_reviews_count': game_reviews.count(),
+
+                'collections': collections.order_by("-edited_time")[:BOOKS_PER_SET],
+                'collections_count': collections.count(),
+                'collections_more': collections.count() > BOOKS_PER_SET,
+
+                'marked_collections': marked_collections.order_by("-edited_time")[:BOOKS_PER_SET],
+                'marked_collections_count': marked_collections.count(),
+                'marked_collections_more': marked_collections.count() > BOOKS_PER_SET,
+
                 'layout': layout,
                 'reports': reports,
                 'unread_announcements': unread_announcements,
-                'latest_task': latest_task,
             }
         )
     else:
@@ -283,7 +224,7 @@ def home(request, id):
 
 def filter_marks(querysets, maximum, type_name):
     """
-    Filter marks by amount limits and order them edited time, store results in a dict, 
+    Filter marks by amount limits and order them edited time, store results in a dict,
     which could be directly used in template.
     @param querysets: one queryset or multiple querysets as a list
     """
@@ -295,9 +236,9 @@ def filter_marks(querysets, maximum, type_name):
         marks = []
         count = 0
         for queryset in querysets:
-            marks += list(queryset.filter(status=MarkStatusEnum[status.upper()]).order_by("-edited_time")[:maximum])
+            marks += list(queryset.filter(status=MarkStatusEnum[status.upper()]).order_by("-created_time")[:maximum])
             count += queryset.filter(status=MarkStatusEnum[status.upper()]).count()
-            
+
         # marks
         marks = sorted(marks, key=lambda e: e.edited_time, reverse=True)[:maximum]
         result[f"{status}_{type_name}_marks"] = marks
@@ -309,6 +250,7 @@ def filter_marks(querysets, maximum, type_name):
 
     return result
 
+
 def count_marks(querysets, type_name):
     """
     Count all available marks, then assembly a dict to be used in template
@@ -327,44 +269,11 @@ def count_marks(querysets, type_name):
 
 @mastodon_request_included
 @login_required
-def followers(request, id):  
+def followers(request, id):
     if request.method == 'GET':
-        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,
-                }
-            )        
-        # mastodon request
-        if not user == request.user:
-            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,
-                    }
-                )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
+        user = User.get(id)
+        if user is None or user != request.user:
+            return render_user_not_found(request)
         return render(
             request,
             'users/relation_list.html',
@@ -381,42 +290,9 @@ def followers(request, id):
 @login_required
 def following(request, id):
     if request.method == 'GET':
-        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,
-                }
-            )        
-        # mastodon request
-        if not user == request.user:
-            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,
-                    }
-                )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
+        user = User.get(id)
+        if user is None or user != request.user:
+            return render_user_not_found(request)
         return render(
             request,
             'users/relation_list.html',
@@ -433,35 +309,15 @@ def following(request, id):
 @login_required
 def book_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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']:
+
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
+        if user != request.user:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -470,13 +326,22 @@ def book_list(request, id, status):
                         'msg': msg,
                     }
                 )
-            queryset = BookMark.get_available_by_user(user, relation['following']).filter(
-                status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = BookReview.get_available_by_user(user, is_following).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = BookTag.find_by_user(tag, user, request.user).order_by("-mark__created_time")
+            else:
+                queryset = BookMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         else:
-            queryset = BookMark.objects.filter(
-                owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            if status == 'reviewed':
+                queryset = BookReview.objects.filter(owner=user).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = BookTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time")
+            else:
+                queryset = BookMark.objects.filter(
+                    owner=user, status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         paginator = Paginator(queryset, ITEMS_PER_PAGE)
         page_number = request.GET.get('page', default=1)
         marks = paginator.get_page(page_number)
@@ -484,14 +349,20 @@ 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(_("的书"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的书"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的书"))
+        else:
+            list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书"))
         return render(
             request,
-            'users/book_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
@@ -502,35 +373,15 @@ def book_list(request, id, status):
 @login_required
 def movie_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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']:
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
+        if user != request.user:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -539,14 +390,21 @@ def movie_list(request, id, status):
                         'msg': msg,
                     }
                 )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
-        
-            queryset = MovieMark.get_available_by_user(user, relation['following']).filter(
-                status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = MovieReview.get_available_by_user(user, is_following).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = MovieTag.find_by_user(tag, user, request.user).order_by("-mark__created_time")
+            else:
+                queryset = MovieMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         else:
-            queryset = MovieMark.objects.filter(
-                owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            if status == 'reviewed':
+                queryset = MovieReview.objects.filter(owner=user).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = MovieTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time")
+            else:
+                queryset = MovieMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         paginator = Paginator(queryset, ITEMS_PER_PAGE)
         page_number = request.GET.get('page', default=1)
         marks = paginator.get_page(page_number)
@@ -554,53 +412,40 @@ 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(_("的电影和剧集"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的电影和剧集"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的电影和剧集"))
+        else:
+            list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集"))
+
         return render(
             request,
-            'users/movie_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
         return HttpResponseBadRequest()
-            
+
 
 @mastodon_request_included
 @login_required
 def game_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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']:
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
+        if user != request.user:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -609,14 +454,22 @@ def game_list(request, id, status):
                         'msg': msg,
                     }
                 )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
-        
-            queryset = GameMark.get_available_by_user(user, relation['following']).filter(
-                status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = GameReview.get_available_by_user(user, is_following).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = GameTag.find_by_user(tag, user, request.user).order_by("-mark__created_time")
+            else:
+                queryset = GameMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         else:
-            queryset = GameMark.objects.filter(
-                owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            if status == 'reviewed':
+                queryset = GameReview.objects.filter(owner=user).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = GameTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time")
+            else:
+                queryset = GameMark.objects.filter(
+                    owner=user, status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         paginator = Paginator(queryset, ITEMS_PER_PAGE)
         page_number = request.GET.get('page', default=1)
         marks = paginator.get_page(page_number)
@@ -624,53 +477,39 @@ def game_list(request, id, status):
             mark.game.tag_list = mark.game.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(GameMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的游戏"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的游戏"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的游戏"))
+        else:
+            list_title = str(GameMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的游戏"))
         return render(
             request,
-            'users/game_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
         return HttpResponseBadRequest()
-            
+
 
 @mastodon_request_included
 @login_required
 def music_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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,
-                }
-            )        
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
         if not user == request.user:
-            # mastodon request
-            relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
-            if relation['blocked_by']:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -679,39 +518,55 @@ def music_list(request, id, status):
                         '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'])
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = list(AlbumReview.get_available_by_user(user, is_following).order_by("-edited_time")) + \
+                    list(SongReview.get_available_by_user(user, is_following).order_by("-edited_time"))
+            elif status == 'tagged':
+                queryset = list(AlbumTag.find_by_user(tag, user, request.user).order_by("-mark__created_time"))
+            else:
+                queryset = list(AlbumMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()])) \
+                        + list(SongMark.get_available_by_user(user, is_following).filter(
+                        status=MarkStatusEnum[status.upper()]))
         else:
-            queryset = list(AlbumMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) \
-                + list(SongMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]))
+            if status == 'reviewed':
+                queryset = list(AlbumReview.objects.filter(owner=user).order_by("-edited_time")) + \
+                    list(SongReview.objects.filter(owner=user).order_by("-edited_time"))
+            elif status == 'tagged':
+                queryset = list(AlbumTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time"))
+            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:
+            if mark.__class__ in [AlbumMark, AlbumReview, AlbumTag]:
                 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:
+            elif mark.__class__ == SongMark or mark.__class__ == SongReview:
                 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(MusicMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的音乐"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的音乐"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的音乐"))
+        else:
+            list_title = str(MusicMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的音乐"))
         return render(
             request,
-            'users/music_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
@@ -724,7 +579,7 @@ def set_layout(request):
         layout = json.loads(request.POST.get('layout'))
         request.user.preference.home_layout = layout
         request.user.preference.save()
-        return redirect(reverse("common:home"))
+        return redirect(reverse("users:home", args=[request.user.mastodon_username]))
     else:
         return HttpResponseBadRequest()
 
@@ -751,14 +606,14 @@ def report(request):
             form.instance.is_read = False
             form.instance.submit_user = request.user
             form.save()
-            return redirect(reverse("users:home", args=[form.instance.reported_user.id]))
+            return redirect(reverse("users:home", args=[form.instance.reported_user.mastodon_username]))
         else:
             return render(
                 request,
                 'users/report.html',
                 {
                     'form': form,
-                }                
+                }
             )
     else:
         return HttpResponseBadRequest()
@@ -782,15 +637,37 @@ def manage_report(request):
         return HttpResponseBadRequest()
 
 
-# Utils
-########################################
-def auth_login(request, user, token):
-    """ Decorates django ``login()``. Attach token to session."""
-    request.session['oauth_token'] = token
-    auth.login(request, user)
+@login_required
+def collection_list(request, id):
+    from collection.views import list
+    user = User.get(id)
+    if user is None:
+        return render_user_not_found(request)
+    return list(request, user.id)
 
 
-def auth_logout(request):
-    """ Decorates django ``logout()``. Release token in session."""
-    del request.session['oauth_token']
-    auth.logout(request)    
+@login_required
+def marked_collection_list(request, id):
+    from collection.views import list
+    user = User.get(id)
+    if user is None:
+        return render_user_not_found(request)
+    return list(request, user.id, True)
+
+
+@login_required
+def tag_list(request, id):
+    user = User.get(id)
+    if user is None:
+        return render_user_not_found(request)
+    if user != request.user:
+        raise PermissionDenied()  # tag list is for user's own view only, for now
+    return render(
+        request,
+        'users/tags.html', {
+            'book_tags': BookTag.all_by_user(user),
+            'movie_tags': MovieTag.all_by_user(user),
+            'music_tags': AlbumTag.all_by_user(user),
+            'game_tags': GameTag.all_by_user(user),
+        }
+    )

From 5da627a8df8cc057ef7a615470077723084ae32b Mon Sep 17 00:00:00 2001
From: Henri Dickson <90480431+alphatownsman@users.noreply.github.com>
Date: Wed, 9 Nov 2022 13:56:50 -0500
Subject: [PATCH 3/6] add all NeoDB features to NiceDB (#115)

* fix scraping failure with wepb image (merge upstream/fix-webp-scrape)

* add filetype to requirements

* add proxycrawl.com as fallback for douban scraper

* load 3p js/css from cdn

* add fix-cover task

* fix book/album cover tasks

* scrapestack

* bandcamp scrape and preview ;
manage.py scrape <url> ;
make ^C work when DEBUG

* use scrapestack when fix cover

* add user agent to improve compatibility

* search BandCamp for music albums

* add missing MovieGenre

* fix search 500 when song has no parent album

* adjust timeout

* individual scrapers

* fix tmdb parser

* export marks via rq; pref to send public toot; move import to data page

* fix spotify import

* fix edge cases

* export: fix dupe tags

* use rq to manage doufen import

* add django command to manage rq jobs

* fix export edge case

* tune rq admin

* fix detail page 502 step 1: async pull mastodon follow/block/mute list

* fix detail page 502 step 2: calculate relationship by local cached data

* manual sync mastodon follow info

* domain_blocks parsing fix

* marks by who i follows

* adjust label

* use username in urls

* add page to list a user\'s review

* review widget on user home page

* fix preview 500

* fix typo

* minor fix

* fix google books parsing

* allow mark/review visible to oneself

* fix auto sync masto for new user

* fix search 500

* add command to restart a sync task

* reset visibility

* delete user data

* fix tag search result pagination

* not upgrade to django 4 yet

* basic doc

* wip: collection

* wip

* wip

* collection use htmx

* show in-collection section for entities

* fix typo

* add su for easier debug

* fix some 500s

* fix login using alternative domain

* hide data from disabled user

* add item to list from detail page

* my tags

* collection: inline comment edit

* show number of ratings

* fix collection delete

* more detail in collection view

* use item template in search result

* fix 500

* write index to meilisearch

* fix search

* reindex in batch

* fix 500

* show search result from meilisearch

* more search commands

* index less fields

* index new items only

* search highlights

* fix 500

* auto set search category

* classic search if no meili server

* fix index stats error

* support typesense backend

* workaround typesense bug

* make external search async

* fix 500, typo

* fix cover scripts

* fix minor issue in douban parser

* supports m.douban.com and customized bandcamp domain

* move account

* reword with gender-friendly and instance-neutral language

* Friendica does not have vapid_key in api response

* enable anonymous search

* tweak book result template

* API v0

API v0

* fix meilisearch reindex

* fix search by url error

* login via twitter.com

* login via pixelfed

* minor fix

* no refresh on inactive users

* support refresh access token

* get rid of /users/number-id/

* refresh twitter handler automatically

* paste image when review

* support PixelFed (very long token)

* fix django-markdownx version

* ignore single quote for meilisearch for now

* update logo

* show book review/mark from same isbn

* show movie review/mark from same imdb

* fix login with older mastodon servers

* import Goodreads book list and profile

* add timestamp to Goodreads import

* support new google books api

* import goodreads list

* minor goodreads fix

* click corner action icon to add to wishlist

* clean up duplicated code

* fix anonymous search

* fix 500

* minor fix search 500

* show rating only if votes > 5

* Entity.refresh_rating()

* preference to append text when sharing; clean up duplicated code

* fix missing data for user tagged view

* fix page link for tag view

* fix 500 when language field longer than 10

* fix 500 when sharing mark for song

* fix error when reimport goodread profile

* fix minor typo

* fix a rare 500

* error log dump less

* fix tags in marks export

* fix missing param in pagination

* import douban review

* clarify text

* fix missing sheet in review import

* review: show in progress

* scrape douban: ignore unknown genre

* minor fix

* improve review import by guess entity urls

* clear guide text for review import

* improve review import form text

* workaround some 500

* fix mark import error

* fix img in review import

* load external results earlier

* ignore search server errors

* simplify user register flow to avoid inconsistent state

* Add a learn more link on login page

* Update login.html

* show mark created timestamp as mark time

* no 500 for api error

* redirect for expired tokens

* ensure preference object created.

* mark collections

* tag list

* fix tag display

* fix sorting etc

* fix 500

* fix potential export 500; save shared links

* fix share to twittwe

* fix review url

* fix 500

* fix 500

* add timeline, etc

* missing status change in timeline

* missing id in timeline

* timeline view by default

* workaround bug in markdownx...

* fix typo

* option to create new collection when add from detail page

* add missing announcement and tags in timeline home

* add missing announcement

* add missing announcement

* opensearch

* show fediverse shared link

* public review no longer requires login

* fix markdownx bug

* fix 500

* use cloudflare cdn

* validate jquery load and domain input

* fix 500

* tips for goodreads import

* collaborative collection

* show timeline and profile link on nav bar

* minor tweak

* share collection

* fix Goodreads search

* show wish mark in timeline

* resync failed urls with local proxy

* resync failed urls with local proxy: check proxy first

* scraper minor fix

* resync failed urls

* fix fields limit

* fix douban parsing error

* resync

* scraper minor fix

* scraper minor fix

* scraper minor fix

* local proxy

* local proxy

* sync default config from neodb

* configurable site name

* fix 500

* fix 500 for anonymous user

* add sentry

* add git version in log

* add git version in log

* no longer rely on cdnjs.cloudflare.com

* move jq/cash to _common_libs template partial

* fix rare js error

* fix 500

* avoid double submission error

* import tag in lower case

* catch some js network errors

* catch some js network errors

* support more goodread urls

* fix unaired tv in tmdb

* support more google book urls

* fix related series

* more goodreads urls

* robust googlebooks search

* robust search

* Update settings.py

* Update scraper.py

* Update requirements.txt

* make nicedb work

* doc update

* simplify permission check

* update doc

* update doc for bug report link

* skip spotify tracks

* fix 500

* improve search api

* blind fix import compatibility

* show years for movie in timeline

* show years for movie in timeline; thinner font

* export reviews

* revert user home to use jquery https://github.com/fabiospampinato/cash/issues/246

* IGDB

* use IGDB for Steam

* use TMDB for IMDb

* steam: igdb then fallback to steam

* keep change history

* keep change history: add django settings

* Steam: keep localized title/brief while merging IGDB

* basic Docker support

* rescrape

* Create codeql-analysis.yml

* Create SECURITY.md

* Create pysa.yml

Co-authored-by: doubaniux <goodsir@vivaldi.net>
Co-authored-by: Your Name <you@example.com>
Co-authored-by: Their Name <they@example.com>
Co-authored-by: Mt. Front <mfcndw@gmail.com>
---
 .github/workflows/codeql-analysis.yml         |   74 +
 .github/workflows/pysa.yml                    |   50 +
 .gitignore                                    |    5 +-
 Dockerfile                                    |   23 +
 README.md                                     |   11 +-
 SECURITY.md                                   |    5 +
 boofilsic/context_processors.py               |    5 +
 boofilsic/settings.py                         |  123 +-
 boofilsic/urls.py                             |    6 +
 books/admin.py                                |    3 +-
 books/apps.py                                 |    5 +
 books/forms.py                                |   24 +-
 books/management/commands/fix-book-cover.py   |  200 +++
 books/models.py                               |  162 ++-
 books/templates/books/create_update.html      |   28 +-
 .../templates/books/create_update_review.html |   12 +-
 books/templates/books/delete.html             |   12 +-
 books/templates/books/delete_review.html      |   14 +-
 books/templates/books/detail.html             |  132 +-
 books/templates/books/mark_list.html          |   43 +-
 books/templates/books/review_detail.html      |   22 +-
 books/templates/books/review_list.html        |   18 +-
 books/templates/books/scrape.html             |    4 +-
 books/urls.py                                 |    6 +-
 books/views.py                                |  137 +-
 collection/__init__.py                        |    0
 collection/admin.py                           |    3 +
 collection/apps.py                            |    6 +
 collection/forms.py                           |   45 +
 collection/models.py                          |  126 ++
 collection/templates/add_to_list.html         |   45 +
 collection/templates/create_update.html       |   71 +
 collection/templates/delete.html              |  117 ++
 collection/templates/detail.html              |  147 ++
 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                           |  442 ++++++
 common/forms.py                               |   50 +-
 common/importers/douban.py                    |  270 ++++
 common/importers/goodreads.py                 |  202 +++
 common/index.py                               |   12 +
 common/management/commands/delete_job.py      |   19 +
 common/management/commands/index_stats.py     |   40 +
 common/management/commands/init_index.py      |   18 +
 common/management/commands/list_jobs.py       |   24 +
 common/management/commands/reindex.py         |   40 +
 common/management/commands/restart_sync.py    |   28 +
 common/management/commands/scrape.py          |   25 +
 common/models.py                              |  220 ++-
 common/scraper.py                             | 1259 ++---------------
 common/scrapers/bandcamp.py                   |   71 +
 common/scrapers/bangumi.py                    |  199 +++
 common/scrapers/douban.py                     |  714 ++++++++++
 common/scrapers/goodreads.py                  |  157 ++
 common/scrapers/google.py                     |  102 ++
 common/scrapers/igdb.py                       |   88 ++
 common/scrapers/imdb.py                       |  116 ++
 common/scrapers/spotify.py                    |  287 ++++
 common/scrapers/steam.py                      |   92 ++
 common/scrapers/tmdb.py                       |  150 ++
 common/search/meilisearch.py                  |  183 +++
 common/search/typesense.py                    |  215 +++
 common/searcher.py                            |  209 +++
 common/static/css/boofilsic.css               |  552 +++-----
 common/static/css/boofilsic.min.css           |    2 +-
 common/static/img/fediverse.svg               |    5 +
 common/static/img/logo.svg                    |  141 +-
 common/static/img/logo_square.jpg             |  Bin 42490 -> 32105 bytes
 common/static/img/logo_square.svg             |  194 ++-
 common/static/js/create_update_review.js      |    4 +-
 common/static/js/detail.js                    |   16 +-
 common/static/js/home.js                      |  189 ++-
 common/static/js/mastodon.js                  |   72 +-
 common/static/js/rating-star-readonly.js      |    8 +-
 common/static/js/scrape.js                    |    2 +-
 common/static/js/sort_layout.js               |    6 +-
 common/static/lib/css/milligram.css           |  605 --------
 common/static/lib/css/multiple-select.min.css |   10 -
 common/static/lib/css/neo.css                 |  166 +++
 common/static/lib/js/hyperscript-0.9.5.min.js |    2 +
 common/static/lib/js/multiple-select.min.js   |   10 -
 common/static/opensearch.xml                  |    8 +
 common/static/sass/_AsideSection.sass         |    2 +-
 common/static/sass/_Label.sass                |   38 +-
 common/static/sass/_Modal.sass                |    4 +-
 common/static/sass/_Vendor.sass               |    5 +-
 common/templates/common/error.html            |    5 +-
 .../common/external_search_result.html        |   48 +
 common/templates/common/search_result.html    |  418 +-----
 common/templates/partial/_announcement.html   |   61 +
 common/templates/partial/_common_libs.html    |   23 +
 common/templates/partial/_footer.html         |    7 +-
 common/templates/partial/_navbar.html         |   39 +-
 common/templates/partial/_sidebar.html        |  186 +++
 common/templates/partial/list_item.html       |    9 +
 common/templates/partial/list_item_book.html  |  159 +++
 common/templates/partial/list_item_game.html  |  139 ++
 common/templates/partial/list_item_movie.html |  164 +++
 common/templates/partial/list_item_music.html |  171 +++
 common/templates/partial/mark_list.html       |   37 +
 common/templatetags/highlight.py              |   14 +-
 common/templatetags/neo.py                    |   48 +
 common/templatetags/oauth_token.py            |    2 +-
 common/templatetags/thumb.py                  |    5 +-
 common/urls.py                                |    2 +
 common/utils.py                               |    2 +
 common/views.py                               |  124 +-
 doc/GUIDE.md                                  |  106 ++
 docker-compose.yml                            |   32 +
 docker/entrypoint.sh                          |   13 +
 docker/start.sh                               |   36 +
 games/admin.py                                |    3 +-
 games/apps.py                                 |    5 +
 games/forms.py                                |   25 +-
 games/models.py                               |   64 +-
 games/templates/games/create_update.html      |   29 +-
 .../templates/games/create_update_review.html |   12 +-
 games/templates/games/delete.html             |   12 +-
 games/templates/games/delete_review.html      |   14 +-
 games/templates/games/detail.html             |  101 +-
 games/templates/games/mark_list.html          |   42 +-
 games/templates/games/review_detail.html      |   22 +-
 games/templates/games/review_list.html        |   14 +-
 games/templates/games/scrape.html             |    4 +-
 games/urls.py                                 |    7 +-
 games/views.py                                |  137 +-
 management/models.py                          |    2 +-
 .../templates/management/create_update.html   |    4 +-
 management/templates/management/delete.html   |    4 +-
 management/templates/management/detail.html   |    4 +-
 management/templates/management/list.html     |    4 +-
 mastodon/admin.py                             |    2 +-
 mastodon/api.py                               |  373 ++++-
 mastodon/auth.py                              |   82 +-
 mastodon/decorators.py                        |    2 +-
 mastodon/management/commands/wrong_sites.py   |   21 +
 mastodon/models.py                            |   10 +-
 mastodon/utils.py                             |    9 +-
 movies/admin.py                               |    3 +-
 movies/apps.py                                |    5 +
 movies/forms.py                               |   22 +-
 .../management/commands/fix-movie-poster.py   |  203 +++
 movies/models.py                              |  108 +-
 movies/templates/movies/create_update.html    |   31 +-
 .../movies/create_update_review.html          |   16 +-
 movies/templates/movies/delete.html           |   12 +-
 movies/templates/movies/delete_review.html    |   14 +-
 movies/templates/movies/detail.html           |  112 +-
 movies/templates/movies/mark_list.html        |   44 +-
 movies/templates/movies/review_detail.html    |   24 +-
 movies/templates/movies/review_list.html      |   19 +-
 movies/templates/movies/scrape.html           |    4 +-
 movies/urls.py                                |    6 +-
 movies/views.py                               |  137 +-
 music/admin.py                                |    5 +-
 music/apps.py                                 |    6 +
 music/forms.py                                |   33 +-
 music/management/commands/fix-album-cover.py  |  199 +++
 music/models.py                               |  117 +-
 music/templates/music/album_detail.html       |   89 +-
 music/templates/music/album_mark_list.html    |   45 +-
 .../templates/music/album_review_detail.html  |   24 +-
 music/templates/music/album_review_list.html  |   16 +-
 .../templates/music/create_update_album.html  |   29 +-
 .../music/create_update_album_review.html     |   14 +-
 music/templates/music/create_update_song.html |   10 +-
 .../music/create_update_song_review.html      |   14 +-
 music/templates/music/delete_album.html       |   12 +-
 .../templates/music/delete_album_review.html  |   14 +-
 music/templates/music/delete_song.html        |   12 +-
 music/templates/music/delete_song_review.html |   14 +-
 music/templates/music/scrape_album.html       |    4 +-
 music/templates/music/scrape_song.html        |    4 +-
 music/templates/music/song_detail.html        |   89 +-
 music/templates/music/song_mark_list.html     |   52 +-
 music/templates/music/song_review_detail.html |   24 +-
 music/templates/music/song_review_list.html   |   16 +-
 music/urls.py                                 |   13 +-
 music/views.py                                |  250 ++--
 requirements.txt                              |   26 +
 sync/apps.py                                  |    5 +-
 sync/jobs.py                                  |  158 +--
 sync/management/commands/resync.py            |   91 ++
 sync/models.py                                |    8 +-
 sync/views.py                                 |    9 +-
 timeline/__init__.py                          |    0
 timeline/admin.py                             |    3 +
 timeline/apps.py                              |   15 +
 .../management/commands/regen_activity.py     |   20 +
 timeline/models.py                            |   63 +
 timeline/templates/timeline.html              |   83 ++
 timeline/templates/timeline_data.html         |  124 ++
 timeline/tests.py                             |    3 +
 timeline/urls.py                              |    9 +
 timeline/views.py                             |   71 +
 users/account.py                              |  255 ++++
 users/data.py                                 |  142 ++
 .../management/commands/backfill_mastodon.py  |   21 +
 users/management/commands/disable_user.py     |   19 +
 .../management/commands/refresh_following.py  |   19 +
 users/management/commands/refresh_mastodon.py |   25 +
 users/models.py                               |  145 +-
 users/static/js/followers_list.js             |   19 +-
 users/static/js/following_list.js             |   12 +-
 users/static/lib/js/js.cookie.min.js          |    2 -
 users/tasks.py                                |  146 ++
 users/templates/users/book_list.html          |  282 ----
 users/templates/users/data.html               |  337 +++++
 users/templates/users/game_list.html          |  272 ----
 users/templates/users/home.html               |  668 ++++-----
 users/templates/users/home_anonymous.html     |   17 +
 users/templates/users/item_list.html          |   94 ++
 users/templates/users/login.html              |  108 +-
 users/templates/users/manage_report.html      |   14 +-
 users/templates/users/movie_list.html         |  285 ----
 users/templates/users/music_list.html         |  290 ----
 users/templates/users/preferences.html        |   93 ++
 users/templates/users/register.html           |   21 +-
 users/templates/users/relation_list.html      |   18 +-
 users/templates/users/report.html             |   10 +-
 users/templates/users/tags.html               |  110 ++
 users/urls.py                                 |   32 +-
 users/views.py                                |  693 ++++-----
 228 files changed, 12218 insertions(+), 6514 deletions(-)
 create mode 100644 .github/workflows/codeql-analysis.yml
 create mode 100644 .github/workflows/pysa.yml
 create mode 100644 Dockerfile
 create mode 100644 SECURITY.md
 create mode 100644 boofilsic/context_processors.py
 create mode 100644 books/management/commands/fix-book-cover.py
 create mode 100644 collection/__init__.py
 create mode 100644 collection/admin.py
 create mode 100644 collection/apps.py
 create mode 100644 collection/forms.py
 create mode 100644 collection/models.py
 create mode 100644 collection/templates/add_to_list.html
 create mode 100644 collection/templates/create_update.html
 create mode 100644 collection/templates/delete.html
 create mode 100644 collection/templates/detail.html
 create mode 100644 collection/templates/edit_item_comment.html
 create mode 100644 collection/templates/entity_list.html
 create mode 100644 collection/templates/list.html
 create mode 100644 collection/templates/share_collection.html
 create mode 100644 collection/templates/show_item_comment.html
 create mode 100644 collection/tests.py
 create mode 100644 collection/urls.py
 create mode 100644 collection/views.py
 create mode 100644 common/importers/douban.py
 create mode 100644 common/importers/goodreads.py
 create mode 100644 common/index.py
 create mode 100644 common/management/commands/delete_job.py
 create mode 100644 common/management/commands/index_stats.py
 create mode 100644 common/management/commands/init_index.py
 create mode 100644 common/management/commands/list_jobs.py
 create mode 100644 common/management/commands/reindex.py
 create mode 100644 common/management/commands/restart_sync.py
 create mode 100644 common/management/commands/scrape.py
 create mode 100644 common/scrapers/bandcamp.py
 create mode 100644 common/scrapers/bangumi.py
 create mode 100644 common/scrapers/douban.py
 create mode 100644 common/scrapers/goodreads.py
 create mode 100644 common/scrapers/google.py
 create mode 100644 common/scrapers/igdb.py
 create mode 100644 common/scrapers/imdb.py
 create mode 100644 common/scrapers/spotify.py
 create mode 100644 common/scrapers/steam.py
 create mode 100644 common/scrapers/tmdb.py
 create mode 100644 common/search/meilisearch.py
 create mode 100644 common/search/typesense.py
 create mode 100644 common/searcher.py
 create mode 100644 common/static/img/fediverse.svg
 delete mode 100644 common/static/lib/css/milligram.css
 delete mode 100644 common/static/lib/css/multiple-select.min.css
 create mode 100644 common/static/lib/css/neo.css
 create mode 100644 common/static/lib/js/hyperscript-0.9.5.min.js
 delete mode 100644 common/static/lib/js/multiple-select.min.js
 create mode 100644 common/static/opensearch.xml
 create mode 100644 common/templates/common/external_search_result.html
 create mode 100644 common/templates/partial/_announcement.html
 create mode 100644 common/templates/partial/_common_libs.html
 create mode 100644 common/templates/partial/_sidebar.html
 create mode 100644 common/templates/partial/list_item.html
 create mode 100644 common/templates/partial/list_item_book.html
 create mode 100644 common/templates/partial/list_item_game.html
 create mode 100644 common/templates/partial/list_item_movie.html
 create mode 100644 common/templates/partial/list_item_music.html
 create mode 100644 common/templates/partial/mark_list.html
 create mode 100644 common/templatetags/neo.py
 create mode 100644 doc/GUIDE.md
 create mode 100644 docker-compose.yml
 create mode 100755 docker/entrypoint.sh
 create mode 100755 docker/start.sh
 create mode 100644 mastodon/management/commands/wrong_sites.py
 create mode 100644 movies/management/commands/fix-movie-poster.py
 create mode 100644 music/management/commands/fix-album-cover.py
 create mode 100644 requirements.txt
 create mode 100644 sync/management/commands/resync.py
 create mode 100644 timeline/__init__.py
 create mode 100644 timeline/admin.py
 create mode 100644 timeline/apps.py
 create mode 100644 timeline/management/commands/regen_activity.py
 create mode 100644 timeline/models.py
 create mode 100644 timeline/templates/timeline.html
 create mode 100644 timeline/templates/timeline_data.html
 create mode 100644 timeline/tests.py
 create mode 100644 timeline/urls.py
 create mode 100644 timeline/views.py
 create mode 100644 users/account.py
 create mode 100644 users/data.py
 create mode 100644 users/management/commands/backfill_mastodon.py
 create mode 100644 users/management/commands/disable_user.py
 create mode 100644 users/management/commands/refresh_following.py
 create mode 100644 users/management/commands/refresh_mastodon.py
 delete mode 100644 users/static/lib/js/js.cookie.min.js
 create mode 100644 users/tasks.py
 delete mode 100644 users/templates/users/book_list.html
 create mode 100644 users/templates/users/data.html
 delete mode 100644 users/templates/users/game_list.html
 create mode 100644 users/templates/users/home_anonymous.html
 create mode 100644 users/templates/users/item_list.html
 delete mode 100644 users/templates/users/movie_list.html
 delete mode 100644 users/templates/users/music_list.html
 create mode 100644 users/templates/users/preferences.html
 create mode 100644 users/templates/users/tags.html

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..514df728
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,74 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "neo" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "neo" ]
+  schedule:
+    - cron: '35 0 * * 0'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'javascript', 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        
+        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+        
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+    #   If the Autobuild fails above, remove it and uncomment the following three lines. 
+    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+    # - run: |
+    #   echo "Run, Build Application using script"
+    #   ./location_of_script_within_repo/buildscript.sh
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
+      with:
+        category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/pysa.yml b/.github/workflows/pysa.yml
new file mode 100644
index 00000000..e4e20af3
--- /dev/null
+++ b/.github/workflows/pysa.yml
@@ -0,0 +1,50 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+# This workflow integrates Python Static Analyzer (Pysa) with
+# GitHub's Code Scanning feature.
+#
+# Python Static Analyzer (Pysa) is a security-focused static
+# analysis tool that tracks flows of data from where they
+# originate to where they terminate in a dangerous location.
+#
+# See https://pyre-check.org/docs/pysa-basics/
+
+name: Pysa
+
+on:
+  workflow_dispatch:
+  push:
+    branches: [ "neo" ]
+  pull_request:
+    branches: [ "neo" ]
+  schedule:
+    - cron: '45 12 * * 4'
+
+permissions:
+    contents: read
+
+jobs:
+  pysa:
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+
+      - name: Run Pysa
+        uses: facebook/pysa-action@f46a63777e59268613bd6e2ff4e29f144ca9e88b
+        with:
+          # To customize these inputs:
+          # See https://github.com/facebook/pysa-action#inputs
+          repo-directory: './'
+          requirements-path: 'requirements.txt'
+          infer-types: true
+          include-default-sapp-filters: true
diff --git a/.gitignore b/.gitignore
index 086f9d04..d1edae82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,7 @@ migrations/
 
 # debug log file
 /log
-log
\ No newline at end of file
+log
+
+# conf folder for neodb
+/neodb
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..cc9bf6e8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+# syntax=docker/dockerfile:1
+FROM python:3.8-slim
+ENV PYTHONDONTWRITEBYTECODE=1
+ENV PYTHONUNBUFFERED=1
+RUN apt-get update \  
+  && apt-get install -y --no-install-recommends build-essential libpq-dev git \  
+  && rm -rf /var/lib/apt/lists/*
+COPY requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt \  
+    && rm -rf /tmp/requirements.txt \  
+    && useradd -U app_user \  
+    && install -d -m 0755 -o app_user -g app_user /app/static
+
+ENV DJANGO_SETTINGS_MODULE=neodb.dev
+WORKDIR /app
+USER app_user:app_user
+COPY --chown=app_user:app_user . .
+RUN chmod +x docker/*.sh
+
+# Section 6- Docker Run Checks and Configurations 
+ENTRYPOINT [ "docker/entrypoint.sh" ]
+
+CMD [ "docker/start.sh", "server" ]
\ No newline at end of file
diff --git a/README.md b/README.md
index 2cfde8ea..d9f46dbc 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,13 @@ An application allows you to mark any books, movies and more things you love.
 
 Depends on Mastodon.
 
+## Install
+Please see [doc/GUIDE.md](doc/GUIDE.md)
+
+## Bug Report
+ - to file a bug for NiceDB, please create an issue [here](https://github.com/doubaniux/boofilsic/issues/new)
+ - to file a bug or request new features for NeoDB, please contact NeoDB on [Fediverse](https://mastodon.social/@neodb) or [Twitter](https://twitter.com/NeoDBsocial)
+
 ## Contribution
 The project is based on Django. If you are familiar with this technique and willing to read through the terrible code😝, your contribution would be the most welcome!
 
@@ -11,8 +18,6 @@ Currently looking for someone to help with:
 - Explaining the structure of code
 - Refactoring (this is something big)
 
-This project is still in its early stage, so you are not encouraged to deploy it on your own. If you do want to give it a try, please check the [fork of *alphatownsman*](https://github.com/alphatownsman/boofilsic), which is more friendly.
-
 ## Sponsor
-If you like this project, please consider sponsoring us on [Patreon](https://patreon.com/tertius).
+If you like this project, please consider sponsoring NiceDB on [Patreon](https://patreon.com/tertius).
 
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..cafb3b8b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,5 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Please DM [us on Fediverse](https://mastodon.social/@neodb) or send email to `dev`@`neodb.social` to report a vulnerability. Please do not post publicly or create pr/issues directly. Thank you.
diff --git a/boofilsic/context_processors.py b/boofilsic/context_processors.py
new file mode 100644
index 00000000..6fd333b3
--- /dev/null
+++ b/boofilsic/context_processors.py
@@ -0,0 +1,5 @@
+from django.conf import settings
+
+
+def site_info(request):
+    return settings.SITE_INFO
diff --git a/boofilsic/settings.py b/boofilsic/settings.py
index 86d4abf2..6393f4a0 100644
--- a/boofilsic/settings.py
+++ b/boofilsic/settings.py
@@ -12,10 +12,13 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
 
 import os
 import psycopg2.extensions
+from git import Repo
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
+# https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
 
 # Quick-start development settings - unsuitable for production
 # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
@@ -38,6 +41,8 @@ INTERNAL_IPS = [
 
 INSTALLED_APPS = [
     'django.contrib.admin',
+    'hijack',
+    'hijack.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
@@ -45,6 +50,9 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'django.contrib.humanize',
     'django.contrib.postgres',
+    'django_sass',
+    'django_rq',
+    'simple_history',
     'markdownx',
     'management.apps.ManagementConfig',
     'mastodon.apps.MastodonConfig',
@@ -54,7 +62,12 @@ INSTALLED_APPS = [
     'movies.apps.MoviesConfig',
     'music.apps.MusicConfig',
     'games.apps.GamesConfig',
+    'sync.apps.SyncConfig',
+    'collection.apps.CollectionConfig',
+    'timeline.apps.TimelineConfig',
     'easy_thumbnails',
+    'user_messages',
+    'django_slack',
 ]
 
 MIDDLEWARE = [
@@ -65,6 +78,8 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'hijack.middleware.HijackUserMiddleware',
+    'simple_history.middleware.HistoryRequestMiddleware',
 ]
 
 ROOT_URLCONF = 'boofilsic.urls'
@@ -79,7 +94,9 @@ TEMPLATES = [
                 'django.template.context_processors.debug',
                 'django.template.context_processors.request',
                 'django.contrib.auth.context_processors.auth',
-                'django.contrib.messages.context_processors.messages',
+                # 'django.contrib.messages.context_processors.messages',
+                "user_messages.context_processors.messages",
+                'boofilsic.context_processors.site_info',
             ],
         },
     },
@@ -95,10 +112,10 @@ if DEBUG:
     DATABASES = {
         'default': {
             'ENGINE': 'django.db.backends.postgresql',
-            'NAME': 'test',
-            'USER': 'donotban',
-            'PASSWORD': 'donotbansilvousplait',
-            'HOST': '172.18.116.29',
+            'NAME': os.environ.get('DB_NAME', 'test'),
+            'USER': os.environ.get('DB_USER', 'donotban'),
+            'PASSWORD': os.environ.get('DB_PASSWORD', 'donotbansilvousplait'),
+            'HOST': os.environ.get('DB_HOST', '172.18.116.29'),
             'OPTIONS': {
                 'client_encoding': 'UTF8',
                 # 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
@@ -184,13 +201,29 @@ STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesSto
 
 AUTH_USER_MODEL = 'users.User'
 
+SILENCED_SYSTEM_CHECKS = [
+    "auth.W004",  # User.username is non-unique
+    "admin.E404"  # Required by django-user-messages
+]
+
 MEDIA_URL = '/media/'
 MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
 
+PROJECT_ROOT = os.path.abspath(os.path.dirname(__name__))
+SITE_INFO = {
+    'site_name': 'NiceDB',
+    'support_link': 'https://github.com/doubaniux/boofilsic/issues',
+    'version_hash': None,
+    'settings_module': os.getenv('DJANGO_SETTINGS_MODULE'),
+    'sentry_dsn': None,
+}
+
 # Mastodon configs
-CLIENT_NAME = 'NiceDB'
-APP_WEBSITE = 'https://nicedb.org'
-REDIRECT_URIS = "https://nicedb.org/users/OAuth2_login/\nhttps://www.nicedb.org/users/OAuth2_login/"
+CLIENT_NAME = os.environ.get('APP_NAME', 'NiceDB')
+SITE_INFO['site_name'] = os.environ.get('APP_NAME', 'NiceDB')
+APP_WEBSITE = os.environ.get('APP_URL', 'https://nicedb.org')
+REDIRECT_URIS = APP_WEBSITE + "/users/OAuth2_login/"
+
 
 # Path to save report related images, ends with slash
 REPORT_MEDIA_PATH_ROOT = 'report/'
@@ -205,10 +238,23 @@ ALBUM_MEDIA_PATH_ROOT = 'album/'
 DEFAULT_ALBUM_IMAGE = os.path.join(ALBUM_MEDIA_PATH_ROOT, 'default.svg')
 GAME_MEDIA_PATH_ROOT = 'game/'
 DEFAULT_GAME_IMAGE = os.path.join(GAME_MEDIA_PATH_ROOT, 'default.svg')
+COLLECTION_MEDIA_PATH_ROOT = 'collection/'
+DEFAULT_COLLECTION_IMAGE = os.path.join(COLLECTION_MEDIA_PATH_ROOT, 'default.svg')
+SYNC_FILE_PATH_ROOT = 'sync/'
+EXPORT_FILE_PATH_ROOT = 'export/'
+
+# Allow user to login via any Mastodon/Pleroma sites
+MASTODON_ALLOW_ANY_SITE = False
 
 # Timeout of requests to Mastodon, in seconds
 MASTODON_TIMEOUT = 30
 
+MASTODON_CLIENT_SCOPE = 'read write follow'
+#use the following if it's a new site
+#MASTODON_CLIENT_SCOPE = 'read:accounts read:follows read:search read:blocks read:mutes write:statuses write:media'
+
+MASTODON_LEGACY_CLIENT_SCOPE = 'read write follow'
+
 # Tags for toots posted from this site
 MASTODON_TAGS = '#NiceDB #NiceDB%(category)s #NiceDB%(category)s%(type)s'
 
@@ -217,7 +263,7 @@ STAR_SOLID = ':star_solid:'
 STAR_HALF = ':star_half:'
 STAR_EMPTY = ':star_empty:'
 
-# Default password for each user. since assword is not used any way,
+# Default password for each user. since password is not used any way,
 # any string that is not empty is ok
 DEFAULT_PASSWORD = 'ab7nsm8didusbaqPgq'
 
@@ -231,8 +277,12 @@ ADMIN_URL = 'tertqX7256n7ej8nbv5cwvsegdse6w7ne5rHd'
 LUMINATI_USERNAME = 'lum-customer-hl_nw4tbv78-zone-static'
 LUMINATI_PASSWORD = 'nsb7te9bw0ney'
 
+SCRAPING_TIMEOUT = 90
+
 # ScraperAPI api key
 SCRAPERAPI_KEY = 'wnb3794v675b8w475h0e8hr7tyge'
+PROXYCRAWL_KEY = None
+SCRAPESTACK_KEY = None
 
 # Spotify credentials
 SPOTIFY_CREDENTIAL = "NzYzNkYTE6MGQ0ODY0NTY2Y2b3n645sdfgAyY2I1ljYjg3Nzc0MjIwODQ0ZWE="
@@ -240,6 +290,17 @@ SPOTIFY_CREDENTIAL = "NzYzNkYTE6MGQ0ODY0NTY2Y2b3n645sdfgAyY2I1ljYjg3Nzc0MjIwODQ0
 # IMDb API service https://imdb-api.com/
 IMDB_API_KEY = "k23fwewff23"
 
+# The Movie Database (TMDB) API Keys
+TMDB_API3_KEY = "deadbeef"
+TMDB_API4_KEY = "deadbeef.deadbeef.deadbeef"
+
+# Google Books API Key
+GOOGLE_API_KEY = 'deadbeef-deadbeef-deadbeef'
+
+# IGDB
+IGDB_CLIENT_ID = 'deadbeef'
+IGDB_ACCESS_TOKEN = 'deadbeef'
+
 # Thumbnail setting
 # It is possible to optimize the image size even more: https://easy-thumbnails.readthedocs.io/en/latest/ref/optimize/
 THUMBNAIL_ALIASES = {
@@ -257,3 +318,47 @@ if DEBUG:
 
 # https://django-debug-toolbar.readthedocs.io/en/latest/
 # maybe benchmarking before deployment
+
+REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1')
+
+RQ_QUEUES = {
+    'mastodon': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'DEFAULT_TIMEOUT': -1,
+    },
+    'export': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'DEFAULT_TIMEOUT': -1,
+    },
+    'doufen': {
+        'HOST': REDIS_HOST,
+        'PORT': 6379,
+        'DB': 0,
+        'DEFAULT_TIMEOUT': -1,
+    }
+}
+
+RQ_SHOW_ADMIN_LINK = True
+
+SEARCH_INDEX_NEW_ONLY = False
+
+SEARCH_BACKEND = None
+
+# SEARCH_BACKEND = 'MEILISEARCH'
+# MEILISEARCH_SERVER = 'http://127.0.0.1:7700'
+# MEILISEARCH_KEY = 'deadbeef'
+
+# SEARCH_BACKEND = 'TYPESENSE'
+# TYPESENSE_CONNECTION = {
+#     'api_key': 'deadbeef',
+#     'nodes': [{
+#         'host': 'localhost',
+#         'port': '8108',
+#         'protocol': 'http'
+#     }],
+#     'connection_timeout_seconds': 2
+# }
diff --git a/boofilsic/urls.py b/boofilsic/urls.py
index dd52087a..38d74a5a 100644
--- a/boofilsic/urls.py
+++ b/boofilsic/urls.py
@@ -27,10 +27,16 @@ urlpatterns = [
     path('movies/', include('movies.urls')),
     path('music/', include('music.urls')),
     path('games/', include('games.urls')),
+    path('collections/', include('collection.urls')),
+    path('timeline/', include('timeline.urls')),
     path('sync/', include('sync.urls')),
     path('announcement/', include('management.urls')),
+    path('hijack/', include('hijack.urls')),
     path('', include('common.urls')),
+]
 
+urlpatterns += [
+    path(settings.ADMIN_URL + '-rq/', include('django_rq.urls'))
 ]
 
 if settings.DEBUG:
diff --git a/books/admin.py b/books/admin.py
index 942dccb4..75df663b 100644
--- a/books/admin.py
+++ b/books/admin.py
@@ -1,7 +1,8 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Book)
+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
index f716137a..b03e2d23 100644
--- a/books/apps.py
+++ b/books/apps.py
@@ -3,3 +3,8 @@ 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
index da7ecee6..27abda07 100644
--- a/books/forms.py
+++ b/books/forms.py
@@ -1,17 +1,12 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
-from .models import Book, BookMark, BookReview
+from .models import Book, BookMark, BookReview, BookMarkStatusTranslation
 from common.models import MarkStatusEnum
 from common.forms import *
 
 
 def BookMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在读"),
-        MarkStatusEnum.WISH.value: _("想读"),
-        MarkStatusEnum.COLLECT.value: _("读过")
-    }
-    return trans_dict[status]        
+    return BookMarkStatusTranslation[status]
 
 
 class BookForm(forms.ModelForm):
@@ -96,11 +91,8 @@ class BookMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
-        ]
-        labels = {
-            'rating': _("评分"),
-        }        
+            'visibility',
+        ]       
         widgets = {
             'book': forms.TextInput(attrs={"hidden": ""}),
         }      
@@ -115,14 +107,8 @@ class BookReviewForm(ReviewForm):
             'book',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'book': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'book': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/books/management/commands/fix-book-cover.py b/books/management/commands/fix-book-cover.py
new file mode 100644
index 00000000..ae9227b5
--- /dev/null
+++ b/books/management/commands/fix-book-cover.py
@@ -0,0 +1,200 @@
+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('不存在[^<]+</title>', 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 = '<html />'
+        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
index 4f898709..8b23e9a6 100644
--- a/books/models.py
+++ b/books/models.py
@@ -1,98 +1,184 @@
-import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
 from common.utils import GenerateDateUUIDMediaFilePath
-from boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE
-from django.utils import timezone
+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, BOOK_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.BOOK_MEDIA_PATH_ROOT)
 
 
 class Book(Entity):
     # widely recognized name, usually in Chinese
-    title = models.CharField(_("title"), max_length=200)
-    subtitle = models.CharField(_("subtitle"), blank=True, default='', max_length=200)
+    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=200)
+    orig_title = models.CharField(
+        _("original title"), blank=True, default='', max_length=500)
 
     author = postgres.ArrayField(
-        models.CharField(_("author"), blank=True, default='', max_length=100),
+        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=100),
+        models.CharField(_("translator"), blank=True,
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
     )
-    language = models.CharField(_("language"), blank=True, default='', max_length=10)
-    pub_house = models.CharField(_("publishing house"), blank=True, default='', max_length=200)
+    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=50)
+    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)
+    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=DEFAULT_BOOK_IMAGE, 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:
-        # more info: https://docs.djangoproject.com/en/2.2/ref/models/options/
-        # set managed=False if the model represents an existing table or
-        # a database view that has been created by some other means.
-        # check the link above for further info
-        # managed = True
-        # db_table = 'book'
         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'),
+            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 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)
+    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")
+            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)
+    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")
-        ]    
+            models.UniqueConstraint(
+                fields=['owner', 'book'], name="unique_book_review")
+        ]
+
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + 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)
+    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")
+            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
index fdd7ec61..de4b8ca7 100644
--- a/books/templates/books/create_update.html
+++ b/books/templates/books/create_update.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -22,8 +22,24 @@
         
             <section id="content" class="container">
                 <div class="grid">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'books:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'books:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                        {% comment %} <a href="{% url 'books:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -38,12 +54,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/create_update_review.html b/books/templates/books/create_update_review.html
index 9acae132..04cc0c3b 100644
--- a/books/templates/books/create_update_review.html
+++ b/books/templates/books/create_update_review.html
@@ -12,8 +12,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -80,7 +80,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -100,12 +100,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/delete.html b/books/templates/books/delete.html
index 5adcb246..82d2fd7a 100644
--- a/books/templates/books/delete.html
+++ b/books/templates/books/delete.html
@@ -11,8 +11,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除图书' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除图书' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if book.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' book.last_editor.id %}">
+                                    <a href="{% url 'users:home' book.last_editor.mastodon_username %}">
                                         <span>{{ book.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/delete_review.html b/books/templates/books/delete_review.html
index 8c0a7d18..7a0fad5c 100644
--- a/books/templates/books/delete_review.html
+++ b/books/templates/books/delete_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
                                             viewBox="0 0 20 20">
@@ -47,7 +47,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -90,12 +90,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/books/templates/books/detail.html b/books/templates/books/detail.html
index fd8ee1e7..2452bcf1 100644
--- a/books/templates/books/detail.html
+++ b/books/templates/books/detail.html
@@ -1,9 +1,12 @@
 {% load static %}
 {% load i18n %}
+{% load l10n %}
+{% load humanize %}
 {% load admin_url %}
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load strip_scheme %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -11,11 +14,11 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB书 - {{ book.title }}">
+    <meta property="og:title" content="{{ site_name }}书 - {{ book.title }}">
     <meta property="og:type" content="book">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ book.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
+    <meta property="og:site_name" content="{{ site_name }}">
     <meta property="og:description" content="{{ book.brief }}">
     {% if book.author %}
     <meta property="og:book:author" content="{% for author in book.author %}{{ author }}{% if not forloop.last %},{% endif %}{% endfor %}">
@@ -23,13 +26,13 @@
     {% if book.isbn %}
     <meta property="og:book:isbn" content="{{ book.isbn }}">
     {% endif %}
+
+    <title>{{ site_name }} - {% trans '书籍详情' %} | {{ book.title }}</title>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
     
-    <title>{% trans 'NiceDB - 书籍详情' %} | {{ book.title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
 </head>
 
 <body>
@@ -57,11 +60,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if book.rating %}
+                                            {% if book.rating and book.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ book.rating }} </span>
+                                            <small>({{ book.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if book.isbn %}{% trans 'ISBN:' %}{{ book.isbn }}{% endif %}</div>
@@ -96,7 +100,7 @@
                                         
                                     
                                         {% if book.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.id %}">{{ book.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.mastodon_username %}">{{ book.last_editor | default:"" }}</a></div>
                                         {% endif %}
                                             
                                         <div>
@@ -148,46 +152,27 @@
                                 
                             <div class="entity-marks">
                                 <h5 class="entity-marks__title">{% trans '这本书的标记' %}</h5>
-                                {% if mark_list_more %}
-                                <a href="{% url 'books:retrieve_mark_list' book.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
-                                {% endif %}
+                                <a href="{% url 'books:retrieve_mark_list' book.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
+                                <a href="{% url 'books:retrieve_mark_list' book.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=book %}
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这本书的评论' %}</h5>
                                 {% if review_list_more %}
-                                <a href="{% url 'books:retrieve_review_list' book.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'books:retrieve_review_list' book.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
+                                    {% if others_review.book != book %}
+                                    <span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'books:retrieve' others_review.book.id %}">{{ others_review.book.get_source_site_display }}</a></span>
+                                    {% endif %}
                                     <span class="entity-reviews__review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
                                     <span>{{ others_review.get_plain_content | truncate:100 }}</span>
                                 </li>
@@ -202,7 +187,6 @@
 
                     <div class="grid__aside" id="aside">
                         <div class="aside-section-wrapper">
-                            
                             {% if mark %}
                             <div class="mark-panel">
 
@@ -212,7 +196,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -224,7 +208,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -245,9 +229,8 @@
                                     <button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在读' %}</button>
                                     <button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '读过' %}</button>
                                 </div>
-                            </div>                            
+                            </div>
                             {% endif %}
-                                
                         </div>
                         
                         <div class="aside-section-wrapper">
@@ -255,7 +238,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -284,7 +267,53 @@
 
                             {% endif %}
                         </div>
-                                
+
+                        {% if book.get_related_books.count > 0 %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关书目' %}</div>
+                                <div >
+                                    {% for b in book.get_related_books %}
+                                    <p>
+                                        <a href="{% url 'books:retrieve' b.id %}">{{ b.title }}</a>
+                                        <small>({{ b.pub_house }} {{ b.pub_year }})</small>
+                                        <span class="source-label source-label__{{ b.source_site }}">{{ b.get_source_site_display }}</span>
+                                    </p>
+                                    {% endfor %}
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if book.isbn %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '借阅或购买' %}</div>
+                                <div class="action-panel__button-group">
+                                    <a class="action-panel__button" target="_blank" href="https://www.worldcat.org/isbn/{{ book.isbn }}">{% trans 'WorldCat' %}</a>
+                                    <a class="action-panel__button" target="_blank" href="https://openlibrary.org/search?isbn={{ book.isbn }}">{% trans 'Open Library' %}</a>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'book' book.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    {% endif %}
                     </div>
                 </div>
             </section>
@@ -296,7 +325,6 @@
     <div id="modals">
         <div class="mark-modal modal">
             <div class="mark-modal__head">
-                
                 {% if not mark %}
                 <style>
                     .mark-modal__title::after {
@@ -313,12 +341,12 @@
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                             <polygon
                             points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
-                        </polygon>
-                    </svg>
+                            </polygon>
+                        </svg>
+                    </span>
                 </span>
-            </span>
-        </div>
-        <div class="mark-modal__body">
+            </div>
+            <div class="mark-modal__body">
                 <form action="{% url 'books:create_update_mark' %}" method="post">
                     {{ mark_form.media }}
                     {% csrf_token %}
@@ -344,8 +372,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:
+                            {{ mark_form.visibility }}</span>
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/books/templates/books/mark_list.html b/books/templates/books/mark_list.html
index 46c5acf1..fe96c4ad 100644
--- a/books/templates/books/mark_list.html
+++ b/books/templates/books/mark_list.html
@@ -12,8 +12,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ book.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ book.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -33,38 +33,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'books:retrieve' book.id %}">{{ book.title }}</a>{% trans ' 的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-        
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-   
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                    </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-                                        
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=book %}
                             </div>
                             <div class="pagination">
                             
@@ -132,12 +101,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
   
diff --git a/books/templates/books/review_detail.html b/books/templates/books/review_detail.html
index 76a53aaf..b4046532 100644
--- a/books/templates/books/review_detail.html
+++ b/books/templates/books/review_detail.html
@@ -11,17 +11,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB书评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}书评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ book.cover|thumb:'normal' }}">
+    <title>{{ site_name }}{% trans '书评' %} - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -37,7 +38,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -46,7 +47,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -71,6 +72,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -112,16 +114,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/books/templates/books/review_list.html b/books/templates/books/review_list.html
index d7fad68b..a7682b0b 100644
--- a/books/templates/books/review_list.html
+++ b/books/templates/books/review_list.html
@@ -12,8 +12,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ book.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ book.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -39,12 +39,14 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
-                                        
+                                            {% if review.book != book %}
+                                            <span class="entity-reviews__review-time source-label"><a href="{% url 'books:retrieve' review.book.id %}" class="entity-reviews__review-time">{{ review.book.get_source_site_display }}</a></span>
+                                            {% endif %}
         
                                         <span href="{% url 'books:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'books:retrieve_review' review.id %}">{{ review.title }}</a></span>
                                             
@@ -119,12 +121,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/books/templates/books/scrape.html b/books/templates/books/scrape.html
index 6ad5a8a1..4f0adcea 100644
--- a/books/templates/books/scrape.html
+++ b/books/templates/books/scrape.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/books/urls.py b/books/urls.py
index 7ca4d24e..518ea096 100644
--- a/books/urls.py
+++ b/books/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -8,8 +8,10 @@ urlpatterns = [
     path('<int:id>/', retrieve, name='retrieve'),
     path('update/<int:id>/', update, name='update'),
     path('delete/<int:id>/', delete, name='delete'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('mark/', create_update_mark, name='create_update_mark'),
-    path('<int:book_id>/mark/list/', retrieve_mark_list, name='retrieve_mark_list'),
+    path('wish/<int:id>/', wish, name='wish'),
+    re_path('(?P<book_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
     path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
     path('<int:book_id>/review/create/', create_review, name='create_review'),
     path('review/update/<int:id>/', update_review, name='update_review'),
diff --git a/books/views.py b/books/views.py
index a06c31cf..10d31b77 100644
--- a/books/views.py
+++ b/books/views.py
@@ -2,22 +2,24 @@ 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
+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.api import check_visibility, post_toot, TootVisibilityEnum
-from mastodon.utils import rating_to_emoji
+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
+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 boofilsic.settings import MASTODON_TAGS
+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__)
@@ -88,6 +90,18 @@ def create(request):
         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':
@@ -98,6 +112,7 @@ def update(request, id):
             'books/create_update.html',
             {
                 'form': form,
+                'is_update': True,
                 'title': _('修改书籍'),
                 'submit_url': reverse("books:update", args=[book.id]),
                 # provided for frontend js
@@ -126,6 +141,7 @@ def update(request, id):
                 'books/create_update.html',
                 {
                     'form': form,
+                    'is_update': True,
                     'title': _('修改书籍'),
                     'submit_url': reverse("books:update", args=[book.id]),
                     # provided for frontend js
@@ -166,6 +182,7 @@ def retrieve(request, id):
         else:
             mark_form = BookMarkForm(initial={
                 'book': book,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -184,10 +201,8 @@ def retrieve(request, id):
             mark_list_more = None
             review_list_more = None
         else:
-            mark_list = BookMark.get_available(
-                book, request.user, request.session['oauth_token'])
-            review_list = BookReview.get_available(
-                book, request.user, request.session['oauth_token'])
+            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:
@@ -195,6 +210,7 @@ def retrieve(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(book=book)))
 
         # def strip_html_tags(text):
         #     import re
@@ -219,6 +235,7 @@ def retrieve(request, id):
                 'review_list_more': review_list_more,
                 'book_tag_list': book_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -263,12 +280,19 @@ def create_update_mark(request):
         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:
@@ -276,13 +300,13 @@ def create_update_mark(request):
             form = BookMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -304,27 +328,10 @@ def create_update_mark(request):
                 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("books:retrieve",
-                                                                args=[book.id])
-                words = BookMarkStatusTranslator(form.cleaned_data['status']) +\
-                    f"《{book.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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("books:retrieve", args=[form.instance.book.id]))
     else:
@@ -333,11 +340,30 @@ def create_update_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_mark_list(request, book_id):
+def wish(request, id):
+    if request.method == 'POST':
+        book = get_object_or_404(Book, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            '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(
-            book, request.user, request.session['oauth_token'])
+        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)
@@ -398,23 +424,8 @@ def create_review(request, book_id):
             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("books:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.book.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -450,22 +461,8 @@ def update_review(request, id):
             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("books:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.book.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("books:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -500,11 +497,10 @@ def delete_review(request, id):
 
 
 @mastodon_request_included
-@login_required
 def retrieve_review(request, id):
     if request.method == 'GET':
         review = get_object_or_404(BookReview, pk=id)
-        if not check_visibility(review, request.session['oauth_token'], request.user):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -539,8 +535,7 @@ def retrieve_review(request, id):
 def retrieve_review_list(request, book_id):
     if request.method == 'GET':
         book = get_object_or_404(Book, pk=book_id)
-        queryset = BookReview.get_available(
-            book, request.user, request.session['oauth_token'])
+        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)
diff --git a/collection/__init__.py b/collection/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/collection/admin.py b/collection/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/collection/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/collection/apps.py b/collection/apps.py
new file mode 100644
index 00000000..7edc77d1
--- /dev/null
+++ b/collection/apps.py
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 00000000..ed84f5a1
--- /dev/null
+++ b/collection/forms.py
@@ -0,0 +1,45 @@
+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
new file mode 100644
index 00000000..d079ec60
--- /dev/null
+++ b/collection/models.py
@@ -0,0 +1,126 @@
+from django.db import models
+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 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 settings.APP_WEBSITE + 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
new file mode 100644
index 00000000..8ce1cae8
--- /dev/null
+++ b/collection/templates/add_to_list.html
@@ -0,0 +1,45 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
+    <div class="modal-underlay" _="on click trigger closeModal"></div>
+    <div class="modal-content">
+        <div class="add-to-list-modal__head">
+            <span class="add-to-list-modal__title">{% trans '添加到收藏单' %}</span>
+            <span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
+                <span class="icon-cross">
+                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                        <polygon
+                        points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
+                        </polygon>
+                    </svg>
+                </span>
+            </span>
+        </div>
+        <div class="add-to-list-modal__body">
+            <form action="/collections/add_to_list/{{ type }}/{{ id }}/" method="post">
+                {% csrf_token %}
+                <select name="collection_id">
+                    {% for collection in collections %}
+                    <option value="{{ collection.id }}">{{ collection.title }}{% if collection.visibility > 0 %}🔒{% endif %}</option>
+                    {% endfor %}
+                    <option value="0">新建收藏单</option>
+                </select>
+                <div>
+                    <textarea type="text" name="comment" placeholder="条目备注"></textarea>
+                </div>
+                <div class="add-to-list-modal__confirm-button">
+                    <input type="submit" class="button float-right" value="{% trans '提交' %}">
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
diff --git a/collection/templates/create_update.html b/collection/templates/create_update.html
new file mode 100644
index 00000000..43e82dcb
--- /dev/null
+++ b/collection/templates/create_update.html
@@ -0,0 +1,71 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <style type="text/css">
+        #id_collaborative li, #id_visibility li {display: inline-block !important;}
+    </style>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        {% include "partial/_navbar.html" %}
+        <div id="content-wrapper">
+            <section id="content" class="container">
+                <div class="grid">
+                    <div class="single-section-wrapper" id="main">
+                        <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
+                            {% csrf_token %}
+                            {{ form }}
+                            <input class="button" type="submit" value="{% trans '提交' %}">
+                        </form>
+                        {{ form.media }}
+                    </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+
+    <script>
+        // mark required
+        $("#content *[required]").each(function () {
+            $(this).prev().prepend("*");
+        });
+
+        // when source site is this site, hide url input box and populate it with fake url
+        // the backend would update this field
+        if ($("select[name='source_site']").val() == "{{ this_site_enum_value }}") {
+            $("input[name='source_url']").hide();
+            $("label[for='id_source_url']").hide();
+            $("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
+        }
+        $("select[name='source_site']").change(function () {
+            let value = $(this).val();
+            if (value == "{{ this_site_enum_value }}") {
+                $("input[name='source_url']").hide();
+                $("label[for='id_source_url']").hide();
+                $("input[name='source_url']").val("https://www.temp.com/" + Date.now() + Math.random());
+            } else {
+                $("input[name='source_url']").show();
+                $("label[for='id_source_url']").show();
+                $("input[name='source_url']").val("");
+            }
+        });
+
+    </script>
+</body>
+
+
+</html>
\ No newline at end of file
diff --git a/collection/templates/delete.html b/collection/templates/delete.html
new file mode 100644
index 00000000..ee6d440d
--- /dev/null
+++ b/collection/templates/delete.html
@@ -0,0 +1,117 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
+    <meta property="og:description" content="{{ collection.description }}">
+    <meta property="og:type" content="article">
+    <meta property="og:article:author" content="{{ collection.owner.username }}">
+    <meta property="og:url" content="{{ request.build_absolute_uri }}">
+    <meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
+    <title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+        
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="review-head">
+                                <h5 class="review-head__title">
+                                    确认删除收藏单「{{ collection.title }}」吗?
+                                </h5>
+                                {% if collection.visibility > 0 %}
+                                <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                            <path
+                                                d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                                            </svg></span>
+                                {% endif %}
+                                <div class="review-head__body">
+                                    <div class="review-head__info">
+                                      
+                                            <a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
+                                            
+                                                
+                                            <span class="review-head__time">{{ collection.edited_time }}</span>
+                                            
+                                    </div>
+                                    <div class="review-head__actions">
+                                    </div>
+                                </div>
+                                <div id="rawContent">
+                                    {{ form.description }}
+                                </div>
+                                {{ form.media }}
+                                <div class="dividing-line"></div>
+                                <div class="clearfix">
+                                    <form action="{% url 'collection:delete' collection.id %}" method="post" class="float-right">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '确认' %}">
+                                    </form>
+                                    <button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
+                                </div>
+                                <!-- <div class="dividing-line"></div> -->
+                                <!-- <div class="entity-card__img-wrapper" style="text-align: center;">
+                                    <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                </div> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="grid__aside" id="aside">
+                        <div class="aside-section-wrapper">
+                            <div class="entity-card">
+                                <div class="entity-card__img-wrapper">
+                                    <a href="{% url 'collection:retrieve' collection.id %}">
+                                        <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                    </a>
+                                </div>
+                                <div class="entity-card__info-wrapper">
+                                    <h5 class="entity-card__title">
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            {{ collection.title }}
+                                        </a>
+                                    </h5>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+    <script>
+        $(".markdownx textarea").hide();
+    </script>
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+</body>
+
+
+</html>
diff --git a/collection/templates/detail.html b/collection/templates/detail.html
new file mode 100644
index 00000000..d78513b9
--- /dev/null
+++ b/collection/templates/detail.html
@@ -0,0 +1,147 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
+    <meta property="og:description" content="{{ collection.description }}">
+    <meta property="og:type" content="article">
+    <meta property="og:article:author" content="{{ collection.owner.username }}">
+    <meta property="og:url" content="{{ request.build_absolute_uri }}">
+    <meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
+
+    <title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+        
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="review-head">
+                                <h5 class="review-head__title">
+                                    {{ collection.title }}
+                                </h5>
+                                {% if collection.visibility > 0 %}
+                                <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                            <path
+                                                d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                                            </svg></span>
+                                {% endif %}
+                                <div class="review-head__body">
+                                    <div class="review-head__info">
+                                      
+                                            <a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
+                                            
+                                                
+                                            <span class="review-head__time">{{ collection.edited_time }}</span>
+                                            
+                                    </div>
+                                    <div class="review-head__actions">
+                                            {% if request.user == collection.owner %}
+                                            <a class="review-head__action-link" href="{% url 'collection:update' collection.id %}">{% trans '编辑' %}</a>
+                                            <a class="review-head__action-link" href="{% url 'collection:delete' collection.id %}">{% trans '删除' %}</a>
+                                            {% elif editable %}
+                                            <span class="review-head__time">可协作整理</span>
+                                            {% endif %}
+                                    </div>
+                                </div>
+                                <!-- <div class="dividing-line"></div> -->
+                                <!-- <div class="entity-card__img-wrapper" style="text-align: center;">
+                                    <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                </div> -->
+                                <div id="rawContent">
+                                    {{ form.description }}
+                                </div>
+                                {{ form.media }}
+                            </div>
+                            <div class="entity-list" hx-get="{% url 'collection:retrieve_entity_list' collection.id %}" hx-trigger="load">
+                            </div>
+                        </div>
+                    </div>
+                    <div class="grid__aside" id="aside">
+                        <div class="aside-section-wrapper">
+                            <div class="entity-card">
+                                <div class="entity-card__img-wrapper">
+                                    <a href="{% url 'collection:retrieve' collection.id %}">
+                                        <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
+                                    </a>
+                                </div>
+                                <div class="entity-card__info-wrapper">
+                                    <h5 class="entity-card__title">
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            {{ collection.title }}
+                                        </a>
+                                    </h5>
+                                </div>
+                            </div>
+                        </div>
+
+                        {% if request.user != collection.owner %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__button-group action-panel__button-group--center">
+                                    {% if following %}
+                                    <form action="{% url 'collection:unfollow' collection.id %}" method="post">
+                                        {% csrf_token %}
+                                        <button class="action-panel__button">{% trans '取消关注' %}</button>
+                                    </form>
+                                    {% else %}
+                                    <form action="{% url 'collection:follow' collection.id %}" method="post">
+                                        {% csrf_token %}
+                                        <button class="action-panel__button">{% trans '关注' %}</button>
+                                    </form>
+                                    {% endif %}                                         
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__button-group action-panel__button-group--center">
+                                    <form>
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:share' collection.id %}" hx-target="body" hx-swap="beforeend">{% trans '分享到联邦网络' %}</button>
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+    <script>
+        $(".markdownx textarea").hide();
+    </script>
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+</body>
+
+
+</html>
diff --git a/collection/templates/edit_item_comment.html b/collection/templates/edit_item_comment.html
new file mode 100644
index 00000000..674985e3
--- /dev/null
+++ b/collection/templates/edit_item_comment.html
@@ -0,0 +1,5 @@
+<form hx-post="{% url 'collection:update_item_comment' collection.id collectionitem.id %}">
+		<input name="comment" value="{{ collectionitem.comment }}">
+		<input type="submit" style="width:unset;" value="修改">
+		<button style="width:unset;" hx-get="{% url 'collection:show_item_comment' collection.id collectionitem.id %}">取消</button>
+</form>
\ No newline at end of file
diff --git a/collection/templates/entity_list.html b/collection/templates/entity_list.html
new file mode 100644
index 00000000..4cdc9ee2
--- /dev/null
+++ b/collection/templates/entity_list.html
@@ -0,0 +1,21 @@
+{% load thumb %}
+{% load i18n %}
+{% load l10n %}
+<ul class="entity-list__entities">
+    {% for collectionitem in collection.collectionitem_list %}
+    {% if collectionitem.item is not None %}
+    {% include "partial/list_item.html" with item=collectionitem.item %}
+    {% endif %}
+    {% empty %}
+    {% endfor %}
+    {% if editable %}
+    <li>
+        <form hx-target=".entity-list" hx-post="{% url 'collection:append_item' form.instance.id %}" method="POST">
+            {% csrf_token %}
+            <input type="url" name="url" placeholder="https://neodb.social/movies/1/" style="min-width:24rem" required>
+            <input type="text" name="comment" placeholder="{% trans '备注' %}" style="min-width:24rem">
+            <input class="button" type="submit" value="{% trans '添加' %}" >
+        </form>
+    </li>
+    {% endif %}
+</ul>
diff --git a/collection/templates/list.html b/collection/templates/list.html
new file mode 100644
index 00000000..0c9c75c3
--- /dev/null
+++ b/collection/templates/list.html
@@ -0,0 +1,99 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="entity-reviews">
+                                <h5 class="entity-reviews__title entity-reviews__title--stand-alone">
+                                    {{ title }}
+                                </h5>
+                                <ul class="entity-reviews__review-list">
+
+                                    {% for collection in collections %}
+
+                                    <li class="entity-reviews__review entity-reviews__review--wider">
+                                        <img src="{{ collection.cover|thumb:'normal' }}" style="width:40px; float:right"class="entity-card__img">
+                                        <span class="entity-reviews__review-title"><a href="{% url 'collection:retrieve' collection.id %}">{{ collection.title }}</a></span>
+                                        <span class="entity-reviews__review-time">{{ collection.edited_time }}</span>
+                                        {% if collection.visibility > 0 %}
+                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
+                                        {% endif %}
+                                    </li>
+                                    {% empty %}
+                                    <div>{% trans '无结果' %}</div>
+                                    {% endfor %}
+
+                                </ul>
+                            </div>
+                            <div class="pagination">
+
+                                {% if collections.pagination.has_prev %}
+                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                <a href="?page={{ collections.previous_page_number }}"
+                                class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                {% endif %}
+
+                                {% for page in collections.pagination.page_range %}
+
+                                {% if page == collections.pagination.current_page %}
+                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                {% else %}
+                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
+                                {% endif %}
+
+                                {% endfor %}
+
+                                {% if collections.pagination.has_next %}
+                                <a href="?page={{ collections.next_page_number }}"
+                                class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                <a href="?page={{ collections.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
+                                {% endif %}
+
+                            </div>
+                        </div>
+                    </div>
+
+
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+
+    <script>
+
+    </script>
+</body>
+
+
+</html>
diff --git a/collection/templates/share_collection.html b/collection/templates/share_collection.html
new file mode 100644
index 00000000..dc8e3beb
--- /dev/null
+++ b/collection/templates/share_collection.html
@@ -0,0 +1,56 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+<div id="modal" _="on closeModal add .closing then wait for animationend then remove me">
+    <div class="modal-underlay" _="on click trigger closeModal"></div>
+    <div class="modal-content">
+        <div class="add-to-list-modal__head">
+            <span class="add-to-list-modal__title">{% trans '分享收藏单' %}</span>
+            <span class="add-to-list-modal__close-button modal-close" _="on click trigger closeModal">
+                <span class="icon-cross">
+                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                        <polygon
+                        points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
+                        </polygon>
+                    </svg>
+                </span>
+            </span>
+        </div>
+        <div class="add-to-list-modal__body">
+            <form action="/collections/share/{{ id }}/" method="post">
+                {% csrf_token %}
+                <div>
+                <label for="id_visibility_0">分享可见性(不同于收藏单本身的权限):</label>
+                <ul id="id_visibility">
+                    <li><label for="id_visibility_0"><input type="radio" name="visibility" value="0" required="" id="id_visibility_0" {% if visibility == 0 %}checked{% endif %}>
+                 公开</label>
+
+                </li>
+                    <li><label for="id_visibility_1"><input type="radio" name="visibility" value="1" required="" id="id_visibility_1" {% if visibility == 1 %}checked{% endif %}>
+                 仅关注者</label>
+
+                </li>
+                    <li><label for="id_visibility_2"><input type="radio" name="visibility" value="2" required="" id="id_visibility_2" {% if visibility == 2 %}checked{% endif %}>
+                 仅自己</label>
+
+                </li>
+                </ul>
+                </div>
+                <div>
+                    <textarea type="text" name="comment" placeholder="分享附言"></textarea>
+                </div>
+                <div class="add-to-list-modal__confirm-button">
+                    <input type="submit" class="button float-right" value="{% trans '提交' %}">
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
diff --git a/collection/templates/show_item_comment.html b/collection/templates/show_item_comment.html
new file mode 100644
index 00000000..f9add2bd
--- /dev/null
+++ b/collection/templates/show_item_comment.html
@@ -0,0 +1,4 @@
+{{ collectionitem.comment }} 
+{% if editable %}
+<a class="action-icon" hx-get="{% url 'collection:update_item_comment' collection.id collectionitem.id %}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19,20H5a1,1,0,0,0,0,2H19a1,1,0,0,0,0-2Z"/><path d="M5,18h.09l4.17-.38a2,2,0,0,0,1.21-.57l9-9a1.92,1.92,0,0,0-.07-2.71h0L16.66,2.6A2,2,0,0,0,14,2.53l-9,9a2,2,0,0,0-.57,1.21L4,16.91a1,1,0,0,0,.29.8A1,1,0,0,0,5,18ZM15.27,4,18,6.73,16,8.68,13.32,6Zm-8.9,8.91L12,7.32l2.7,2.7-5.6,5.6-3,.28Z"/></g></svg></a>
+{% endif %}
\ No newline at end of file
diff --git a/collection/tests.py b/collection/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/collection/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/collection/urls.py b/collection/urls.py
new file mode 100644
index 00000000..f3c62663
--- /dev/null
+++ b/collection/urls.py
@@ -0,0 +1,27 @@
+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('<int:id>/', retrieve, name='retrieve'),
+    path('<int:id>/entity_list', retrieve_entity_list, name='retrieve_entity_list'),
+    path('update/<int:id>/', update, name='update'),
+    path('delete/<int:id>/', delete, name='delete'),
+    path('follow/<int:id>/', follow, name='follow'),
+    path('unfollow/<int:id>/', unfollow, name='unfollow'),
+    path('<int:id>/append_item/', append_item, name='append_item'),
+    path('<int:id>/delete_item/<int:item_id>', delete_item, name='delete_item'),
+    path('<int:id>/move_up_item/<int:item_id>', move_up_item, name='move_up_item'),
+    path('<int:id>/move_down_item/<int:item_id>', move_down_item, name='move_down_item'),
+    path('<int:id>/update_item_comment/<int:item_id>', update_item_comment, name='update_item_comment'),
+    path('<int:id>/show_item_comment/<int:item_id>', show_item_comment, name='show_item_comment'),
+    path('with/<str:type>/<int:id>/', list_with, name='list_with'),
+    path('add_to_list/<str:type>/<int:id>/', add_to_list, name='add_to_list'),
+    path('share/<int:id>/', share, name='share'),
+    path('follow2/<int:id>/', wish, name='wish'),
+
+    # TODO: tag
+]
diff --git a/collection/views.py b/collection/views.py
new file mode 100644
index 00000000..659cb586
--- /dev/null
+++ b/collection/views.py
@@ -0,0 +1,442 @@
+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 post_toot, TootVisibilityEnum, 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)
+
+        if request.user.is_authenticated:
+            following = True if CollectionMark.objects.filter(owner=request.user, collection=collection).first() is not None else False
+            followers = []
+        else:
+            following = False
+            followers = []
+
+        return render(
+            request,
+            'detail.html',
+            {
+                'collection': collection,
+                'form': form,
+                'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
+                'followers': followers,
+                'following': following,
+            }
+        )
+    else:
+        logger.warning('non-GET method at /collections/<id>')
+        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)
+
+    followers = []
+    if request.user.is_authenticated:
+        followers = []
+
+    return render(
+        request,
+        'entity_list.html',
+        {
+            'collection': collection,
+            'form': form,
+            'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
+            'followers': followers,
+
+        }
+    )
+
+
+@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/common/forms.py b/common/forms.py
index 5372ce12..849b31c4 100644
--- a/common/forms.py
+++ b/common/forms.py
@@ -1,4 +1,5 @@
 from django import forms
+from markdownx.fields import MarkdownxFormField
 import django.contrib.postgres.forms as postgres
 from django.utils import formats
 from django.core.exceptions import ValidationError
@@ -45,7 +46,7 @@ class HstoreInput(forms.Widget):
         js = ('js/key_value_input.js',)
 
 
-class JSONField(postgres.JSONField):
+class JSONField(forms.fields.JSONField):
     widget = KeyValueInput
     def to_python(self, value):
         if not value:
@@ -88,7 +89,7 @@ class RatingValidator:
                 _('%(value)s is not an integer'),
                 params={'value': value},
             )
-        if not str(value) in [str(i) for i in range(1, 11)]:
+        if not str(value) in [str(i) for i in range(0, 11)]:
             raise ValidationError(
                 _('%(value)s is not an integer in range 1-10'),
                 params={'value': value},
@@ -154,9 +155,9 @@ class MultiSelect(forms.SelectMultiple):
 
     class Media:
         css = {
-            'all': ('lib/css/multiple-select.min.css',)
+            'all': ('https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.css',)
         }
-        js = ('lib/js/multiple-select.min.js',)
+        js = ('https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.js',)
 
 
 class HstoreField(forms.CharField):
@@ -223,22 +224,25 @@ class DurationField(forms.TimeField):
 #############################
 # Form
 #############################
+VISIBILITY_CHOICES = [
+    (0, _("公开")),
+    (1, _("仅关注者")),
+    (2, _("仅自己")),
+]
 
-class MarkForm(forms.ModelForm):
-    IS_PRIVATE_CHOICES = [
-        (True, _("仅关注者")),
-        (False, _("公开")),
-    ]
-    
+
+class MarkForm(forms.ModelForm):    
     id = forms.IntegerField(required=False, widget=forms.HiddenInput())
     share_to_mastodon = forms.BooleanField(
-        label=_("分享到长毛象"), initial=True, required=False)
+        label=_("分享到联邦网络"), initial=True, required=False)
     rating = forms.IntegerField(
-        validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
-    is_private = RadioBooleanField(
+        label=_("评分"), validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
+    visibility = forms.TypedChoiceField(
         label=_("可见性"),
-        initial=True,
-        choices=IS_PRIVATE_CHOICES
+        initial=0,
+        coerce=int,
+        choices=VISIBILITY_CHOICES,
+        widget=forms.RadioSelect
     )
     tags = TagField(
         required=False,
@@ -259,15 +263,15 @@ class MarkForm(forms.ModelForm):
 
 
 class ReviewForm(forms.ModelForm):
-    IS_PRIVATE_CHOICES = [
-        (True, _("仅关注者")),
-        (False, _("公开")),
-    ]
+    title = forms.CharField(label=_("标题"))
+    content = MarkdownxFormField(label=_("正文 (Markdown)"))
     share_to_mastodon = forms.BooleanField(
-        label=_("分享到长毛象"), initial=True, required=False)
+        label=_("分享到联邦网络"), initial=True, required=False)
     id = forms.IntegerField(required=False, widget=forms.HiddenInput())
-    is_private = RadioBooleanField(
+    visibility = forms.TypedChoiceField(
         label=_("可见性"),
-        initial=True,
-        choices=IS_PRIVATE_CHOICES
+        initial=0,
+        coerce=int,
+        choices=VISIBILITY_CHOICES,
+        widget=forms.RadioSelect
     )
diff --git a/common/importers/douban.py b/common/importers/douban.py
new file mode 100644
index 00000000..321cea8f
--- /dev/null
+++ b/common/importers/douban.py
@@ -0,0 +1,270 @@
+import openpyxl
+import requests
+import re
+from lxml import html
+from markdownify import markdownify as md
+from datetime import datetime
+from common.scraper import get_scraper_by_url
+import logging
+import pytz
+from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
+from user_messages import api as msg
+import django_rq
+from common.utils import GenerateDateUUIDMediaFilePath
+import os
+from books.models import BookReview, Book, BookMark, BookTag
+from movies.models import MovieReview, Movie, MovieMark, MovieTag
+from music.models import AlbumReview, Album, AlbumMark, AlbumTag
+from games.models import GameReview, Game, GameMark, GameTag
+from common.scraper import DoubanAlbumScraper, DoubanBookScraper, DoubanGameScraper, DoubanMovieScraper
+from PIL import Image
+from io import BytesIO
+import filetype
+from common.models import MarkStatusEnum
+
+
+logger = logging.getLogger(__name__)
+tz_sh = pytz.timezone('Asia/Shanghai')
+
+
+def fetch_remote_image(url):
+    try:
+        print(f'fetching remote image {url}')
+        raw_img = None
+        ext = None
+        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}'
+        else:
+            dl_url = url
+        img_response = requests.get(dl_url, timeout=settings.SCRAPING_TIMEOUT)
+        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
+        f = GenerateDateUUIDMediaFilePath(None, "x." + ext, settings.MARKDOWNX_MEDIA_PATH)
+        file = settings.MEDIA_ROOT + f
+        local_url = settings.MEDIA_URL + f
+        os.makedirs(os.path.dirname(file), exist_ok=True)
+        img.save(file)
+        # print(f'remote image saved as {local_url}')
+        return local_url
+    except Exception:
+        print(f'unable to fetch remote image {url}')
+        return url
+
+
+class DoubanImporter:
+    total = 0
+    processed = 0
+    skipped = 0
+    imported = 0
+    failed = []
+    user = None
+    visibility = 0
+    file = None
+
+    def __init__(self, user, visibility):
+        self.user = user
+        self.visibility = visibility
+
+    def update_user_import_status(self, status):
+        self.user.preference.import_status['douban_pending'] = status
+        self.user.preference.import_status['douban_file'] = self.file
+        self.user.preference.import_status['douban_visibility'] = self.visibility
+        self.user.preference.import_status['douban_total'] = self.total
+        self.user.preference.import_status['douban_processed'] = self.processed
+        self.user.preference.import_status['douban_skipped'] = self.skipped
+        self.user.preference.import_status['douban_imported'] = self.imported
+        self.user.preference.import_status['douban_failed'] = self.failed
+        self.user.preference.save(update_fields=['import_status'])
+
+    def import_from_file(self, uploaded_file):
+        try:
+            wb = openpyxl.open(uploaded_file, read_only=True, data_only=True, keep_links=False)
+            wb.close()
+            file = settings.MEDIA_ROOT + GenerateDateUUIDMediaFilePath(None, "x.xlsx", settings.SYNC_FILE_PATH_ROOT)
+            os.makedirs(os.path.dirname(file), exist_ok=True)
+            with open(file, 'wb') as destination:
+                for chunk in uploaded_file.chunks():
+                    destination.write(chunk)
+            self.file = file
+            self.update_user_import_status(2)
+            jid = f'Douban_{self.user.id}_{os.path.basename(self.file)}'
+            django_rq.get_queue('doufen').enqueue(self.import_from_file_task, job_id=jid)
+        except Exception:
+            return False
+        # self.import_from_file_task(file, user, visibility)
+        return True
+
+    mark_sheet_config = {
+        '想读': [MarkStatusEnum.WISH, DoubanBookScraper, Book, BookMark, BookTag],
+        '在读': [MarkStatusEnum.DO, DoubanBookScraper, Book, BookMark, BookTag],
+        '读过': [MarkStatusEnum.COLLECT, DoubanBookScraper, Book, BookMark, BookTag],
+        '想看': [MarkStatusEnum.WISH, DoubanMovieScraper, Movie, MovieMark, MovieTag],
+        '在看': [MarkStatusEnum.DO, DoubanMovieScraper, Movie, MovieMark, MovieTag],
+        '想看': [MarkStatusEnum.COLLECT, DoubanMovieScraper, Movie, MovieMark, MovieTag],
+        '想听': [MarkStatusEnum.WISH, DoubanAlbumScraper, Album, AlbumMark, AlbumTag],
+        '在听': [MarkStatusEnum.DO, DoubanAlbumScraper, Album, AlbumMark, AlbumTag],
+        '听过': [MarkStatusEnum.COLLECT, DoubanAlbumScraper, Album, AlbumMark, AlbumTag],
+        '想玩': [MarkStatusEnum.WISH, DoubanGameScraper, Game, GameMark, GameTag],
+        '在玩': [MarkStatusEnum.DO, DoubanGameScraper, Game, GameMark, GameTag],
+        '玩过': [MarkStatusEnum.COLLECT, DoubanGameScraper, Game, GameMark, GameTag],
+    }
+    review_sheet_config = {
+        '书评': [DoubanBookScraper, Book, BookReview],
+        '影评': [DoubanMovieScraper, Movie, MovieReview],
+        '乐评': [DoubanAlbumScraper, Album, AlbumReview],
+        '游戏评论&攻略': [DoubanGameScraper, Game, GameReview],
+    }
+    mark_data = {}
+    review_data = {}
+    entity_lookup = {}
+
+    def load_sheets(self):
+        f = open(self.file, 'rb')
+        wb = openpyxl.load_workbook(f, read_only=True, data_only=True, keep_links=False)
+        for data, config in [(self.mark_data, self.mark_sheet_config), (self.review_data, self.review_sheet_config)]:
+            for name in config:
+                data[name] = []
+                if name in wb:
+                    print(f'{self.user} parsing {name}')
+                    for row in wb[name].iter_rows(min_row=2, values_only=True):
+                        cells = [cell for cell in row]
+                        if len(cells) > 6:
+                            data[name].append(cells)
+        for sheet in self.mark_data.values():
+            for cells in sheet:
+                # entity_lookup["title|rating"] = [(url, time), ...]
+                k = f'{cells[0]}|{cells[5]}'
+                v = (cells[3], cells[4])
+                if k in self.entity_lookup:
+                    self.entity_lookup[k].append(v)
+                else:
+                    self.entity_lookup[k] = [v]
+        self.total = sum(map(lambda a: len(a), self.review_data.values()))
+
+    def guess_entity_url(self, title, rating, timestamp):
+        k = f'{title}|{rating}'
+        if k not in self.entity_lookup:
+            return None
+        v = self.entity_lookup[k]
+        if len(v) > 1:
+            v.sort(key=lambda c: abs(timestamp - (datetime.strptime(c[1], "%Y-%m-%d %H:%M:%S") if type(c[1])==str else c[1]).replace(tzinfo=tz_sh)))
+        return v[0][0]
+        # for sheet in self.mark_data.values():
+        #     for cells in sheet:
+        #         if cells[0] == title and cells[5] == rating:
+        #             return cells[3]
+
+    def import_from_file_task(self):
+        print(f'{self.user} import start')
+        msg.info(self.user, f'开始导入豆瓣评论')
+        self.update_user_import_status(1)
+        self.load_sheets()
+        print(f'{self.user} sheet loaded, {self.total} lines total')
+        self.update_user_import_status(1)
+        for name, param in self.review_sheet_config.items():
+            self.import_review_sheet(self.review_data[name], param[0], param[1], param[2])
+        self.update_user_import_status(0)
+        msg.success(self.user, f'豆瓣评论导入完成,共处理{self.total}篇,已存在{self.skipped}篇,新增{self.imported}篇。')
+        if len(self.failed):
+            msg.error(self.user, f'豆瓣评论导入时未能处理以下网址:\n{" , ".join(self.failed)}')
+
+    def import_review_sheet(self, worksheet, scraper, entity_class, review_class):
+        prefix = f'{self.user} |'
+        if worksheet is None:  # or worksheet.max_row < 2:
+            print(f'{prefix} {review_class.__name__} empty sheet')
+            return
+        for cells in worksheet:
+            if len(cells) < 6:
+                continue
+            title = cells[0]
+            entity_title = re.sub('^《', '', re.sub('》$', '', cells[1]))
+            review_url = cells[2]
+            time = cells[3]
+            rating = cells[4]
+            content = cells[6]
+            self.processed += 1
+            if time:
+                if type(time) == str:
+                    time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S")
+                time = time.replace(tzinfo=tz_sh)
+            else:
+                time = None
+            if not content:
+                content = ""
+            if not title:
+                title = ""
+            r = self.import_review(entity_title, rating, title, review_url, content, time, scraper, entity_class, review_class)
+            if r == 1:
+                self.imported += 1
+            elif r == 2:
+                self.skipped += 1
+            else:
+                self.failed.append(review_url)
+            self.update_user_import_status(1)
+
+    def import_review(self, entity_title, rating, title, review_url, content, time, scraper, entity_class, review_class):
+        # return 1: done / 2: skipped / None: failed
+        prefix = f'{self.user} |'
+        url = self.guess_entity_url(entity_title, rating, time)
+        if url is None:
+            print(f'{prefix} fetching {review_url}')
+            try:
+                if settings.SCRAPESTACK_KEY is not None:
+                    _review_url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={review_url}'
+                else:
+                    _review_url = review_url
+                r = requests.get(_review_url, timeout=settings.SCRAPING_TIMEOUT)
+                if r.status_code != 200:
+                    print(f'{prefix} fetching error {review_url} {r.status_code}')
+                    return
+                h = html.fromstring(r.content.decode('utf-8'))
+                for u in h.xpath("//header[@class='main-hd']/a/@href"):
+                    if '.douban.com/subject/' in u:
+                        url = u
+                if not url:
+                    print(f'{prefix} fetching error {review_url} unable to locate entity url')
+                    return
+            except Exception:
+                print(f'{prefix} fetching exception {review_url}')
+                return
+        try:
+            entity = entity_class.objects.get(source_url=url)
+            print(f'{prefix} matched {url}')
+        except ObjectDoesNotExist:
+            try:
+                print(f'{prefix} scraping {url}')
+                scraper.scrape(url)
+                form = scraper.save(request_user=self.user)
+                entity = form.instance
+            except Exception as e:
+                print(f"{prefix} scrape failed: {url} {e}")
+                logger.error(f"{prefix} scrape failed: {url}", exc_info=e)
+                return
+        params = {
+            'owner': self.user,
+            entity_class.__name__.lower(): entity
+        }
+        if review_class.objects.filter(**params).exists():
+            return 2
+        content = re.sub(r'<span style="font-weight: bold;">([^<]+)</span>', r'<b>\1</b>', content)
+        content = re.sub(r'(<img [^>]+>)', r'\1<br>', content)
+        content = re.sub(r'<div class="image-caption">([^<]+)</div>', r'<br><i>\1</i><br>', content)
+        content = md(content)
+        content = re.sub(r'(?<=!\[\]\()([^)]+)(?=\))', lambda x: fetch_remote_image(x[1]), content)
+        params = {
+            'owner': self.user,
+            'created_time': time,
+            'edited_time': time,
+            'title': title,
+            'content': content,
+            'visibility': self.visibility,
+            entity_class.__name__.lower(): entity,
+        }
+        review_class.objects.create(**params)
+        return 1
diff --git a/common/importers/goodreads.py b/common/importers/goodreads.py
new file mode 100644
index 00000000..4e2fec58
--- /dev/null
+++ b/common/importers/goodreads.py
@@ -0,0 +1,202 @@
+import re
+import requests
+from lxml import html
+from datetime import datetime
+# from common.scrapers.goodreads import GoodreadsScraper
+from common.scraper import get_scraper_by_url
+from books.models import Book, BookMark
+from collection.models import Collection
+from common.models import MarkStatusEnum
+from django.conf import settings
+from user_messages import api as msg
+import django_rq
+from django.utils.timezone import make_aware
+
+
+re_list = r'^https://www.goodreads.com/list/show/\d+'
+re_shelf = r'^https://www.goodreads.com/review/list/\d+[^?]*\?shelf=[^&]+'
+re_profile = r'^https://www.goodreads.com/user/show/(\d+)'
+gr_rating = {
+    'did not like it': 2,
+    'it was ok': 4,
+    'liked it': 6,
+    'really liked it': 8,
+    'it was amazing': 10
+}
+
+
+class GoodreadsImporter:
+    @classmethod
+    def import_from_url(self, raw_url, user):
+        match_list = re.match(re_list, raw_url)
+        match_shelf = re.match(re_shelf, raw_url)
+        match_profile = re.match(re_profile, raw_url)
+        if match_profile or match_shelf or match_list:
+            django_rq.get_queue('doufen').enqueue(self.import_from_url_task, raw_url, user)
+            return True
+        else:
+            return False
+
+    @classmethod
+    def import_from_url_task(cls, url, user):
+        match_list = re.match(re_list, url)
+        match_shelf = re.match(re_shelf, url)
+        match_profile = re.match(re_profile, url)
+        total = 0
+        if match_list or match_shelf:
+            shelf = cls.parse_shelf(match_shelf[0], user) if match_shelf else cls.parse_list(match_list[0], user)
+            if shelf['title'] and shelf['books']:
+                collection = Collection.objects.create(title=shelf['title'],
+                                                       description=shelf['description'] + '\n\nImported from [Goodreads](' + url + ')',
+                                                       owner=user)
+                for book in shelf['books']:
+                    collection.append_item(book['book'], book['review'])
+                    total += 1
+                collection.save()
+            msg.success(user, f'成功从Goodreads导入包含{total}本书的收藏单{shelf["title"]}。')
+        elif match_profile:
+            uid = match_profile[1]
+            shelves = {
+                MarkStatusEnum.WISH: f'https://www.goodreads.com/review/list/{uid}?shelf=to-read',
+                MarkStatusEnum.DO: f'https://www.goodreads.com/review/list/{uid}?shelf=currently-reading',
+                MarkStatusEnum.COLLECT: f'https://www.goodreads.com/review/list/{uid}?shelf=read',
+            }
+            for status in shelves:
+                shelf_url = shelves.get(status)
+                shelf = cls.parse_shelf(shelf_url, user)
+                for book in shelf['books']:
+                    params = {
+                        'owner': user,
+                        'rating': book['rating'],
+                        'text': book['review'],
+                        'status': status,
+                        'visibility': 0,
+                        'book': book['book'],
+                    }
+                    if book['last_updated']:
+                        params['created_time'] = book['last_updated']
+                        params['edited_time'] = book['last_updated']
+                    try:
+                        mark = BookMark.objects.create(**params)
+                        mark.book.update_rating(None, mark.rating)
+                    except Exception:
+                        print(f'Skip mark for {book["book"]}')
+                        pass
+                    total += 1
+            msg.success(user, f'成功从Goodreads用户主页导入{total}个标记。')
+
+    @classmethod
+    def parse_shelf(cls, url, user):  # return {'title': 'abc', books: [{'book': obj, 'rating': 10, 'review': 'txt'}, ...]}
+        title = None
+        books = []
+        url_shelf = url + '&view=table'
+        while url_shelf:
+            print(f'Shelf loading {url_shelf}')
+            r = requests.get(url_shelf, timeout=settings.SCRAPING_TIMEOUT)
+            if r.status_code != 200:
+                print(f'Shelf loading error {url_shelf}')
+                break
+            url_shelf = None
+            content = html.fromstring(r.content.decode('utf-8'))
+            title_elem = content.xpath("//span[@class='h1Shelf']/text()")
+            if not title_elem:
+                print(f'Shelf parsing error {url_shelf}')
+                break
+            title = title_elem[0].strip()
+            print("Shelf title: " + title)
+            for cell in content.xpath("//tbody[@id='booksBody']/tr"):
+                url_book = 'https://www.goodreads.com' + \
+                    cell.xpath(
+                        ".//td[@class='field title']//a/@href")[0].strip()
+                # has_review = cell.xpath(
+                #     ".//td[@class='field actions']//a/text()")[0].strip() == 'view (with text)'
+                rating_elem = cell.xpath(
+                    ".//td[@class='field rating']//span/@title")
+                rating = gr_rating.get(
+                    rating_elem[0].strip()) if rating_elem else None
+                url_review = 'https://www.goodreads.com' + \
+                    cell.xpath(
+                        ".//td[@class='field actions']//a/@href")[0].strip()
+                review = ''
+                last_updated = None
+                try:
+                    r2 = requests.get(
+                        url_review, timeout=settings.SCRAPING_TIMEOUT)
+                    if r2.status_code == 200:
+                        c2 = html.fromstring(r2.content.decode('utf-8'))
+                        review_elem = c2.xpath(
+                            "//div[@itemprop='reviewBody']/text()")
+                        review = '\n'.join(
+                            p.strip() for p in review_elem) if review_elem else ''
+                        date_elem = c2.xpath(
+                            "//div[@class='readingTimeline__text']/text()")
+                        for d in date_elem:
+                            date_matched = re.search(r'(\w+)\s+(\d+),\s+(\d+)', d)
+                            if date_matched:
+                                last_updated = make_aware(datetime.strptime(date_matched[1] + ' ' + date_matched[2] + ' ' + date_matched[3], '%B %d %Y'))
+                    else:
+                        print(f"Error loading review{url_review}, ignored")
+                    scraper = get_scraper_by_url(url_book)
+                    url_book = scraper.get_effective_url(url_book)
+                    book = Book.objects.filter(source_url=url_book).first()
+                    if not book:
+                        print("add new book " + url_book)
+                        scraper.scrape(url_book)
+                        form = scraper.save(request_user=user)
+                        book = form.instance
+                    books.append({
+                        'url': url_book,
+                        'book': book,
+                        'rating': rating,
+                        'review': review,
+                        'last_updated': last_updated
+                    })
+                except Exception:
+                    print("Error adding " + url_book)
+                    pass  # likely just download error
+            next_elem = content.xpath("//a[@class='next_page']/@href")
+            url_shelf = ('https://www.goodreads.com' + next_elem[0].strip()) if next_elem else None
+        return {'title': title, 'description': '', 'books': books}
+
+    @classmethod
+    def parse_list(cls, url, user):  # return {'title': 'abc', books: [{'book': obj, 'rating': 10, 'review': 'txt'}, ...]}
+        title = None
+        books = []
+        url_shelf = url
+        while url_shelf:
+            print(f'List loading {url_shelf}')
+            r = requests.get(url_shelf, timeout=settings.SCRAPING_TIMEOUT)
+            if r.status_code != 200:
+                print(f'List loading error {url_shelf}')
+                break
+            url_shelf = None
+            content = html.fromstring(r.content.decode('utf-8'))
+            title_elem = content.xpath('//h1[@class="gr-h1 gr-h1--serif"]/text()')
+            if not title_elem:
+                print(f'List parsing error {url_shelf}')
+                break
+            title = title_elem[0].strip()
+            description = content.xpath('//div[@class="mediumText"]/text()')[0].strip()
+            print("List title: " + title)
+            for link in content.xpath('//a[@class="bookTitle"]/@href'):
+                url_book = 'https://www.goodreads.com' + link
+                try:
+                    scraper = get_scraper_by_url(url_book)
+                    url_book = scraper.get_effective_url(url_book)
+                    book = Book.objects.filter(source_url=url_book).first()
+                    if not book:
+                        print("add new book " + url_book)
+                        scraper.scrape(url_book)
+                        form = scraper.save(request_user=user)
+                        book = form.instance
+                    books.append({
+                        'url': url_book,
+                        'book': book,
+                        'review': '',
+                    })
+                except Exception:
+                    print("Error adding " + url_book)
+                    pass  # likely just download error
+            next_elem = content.xpath("//a[@class='next_page']/@href")
+            url_shelf = ('https://www.goodreads.com' + next_elem[0].strip()) if next_elem else None
+        return {'title': title, 'description': description, 'books': books}
diff --git a/common/index.py b/common/index.py
new file mode 100644
index 00000000..42227b60
--- /dev/null
+++ b/common/index.py
@@ -0,0 +1,12 @@
+from django.conf import settings
+
+
+if settings.SEARCH_BACKEND == 'MEILISEARCH':
+    from .search.meilisearch import Indexer
+elif settings.SEARCH_BACKEND == 'TYPESENSE':
+    from .search.typesense import Indexer
+else:
+    class Indexer:
+        @classmethod
+        def update_model_indexable(self, cls):
+            pass
diff --git a/common/management/commands/delete_job.py b/common/management/commands/delete_job.py
new file mode 100644
index 00000000..1c1d6c86
--- /dev/null
+++ b/common/management/commands/delete_job.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+import pprint
+from redis import Redis
+from rq.job import Job
+from rq import Queue
+
+
+class Command(BaseCommand):
+    help = 'Delete a job'
+
+    def add_arguments(self, parser):
+        parser.add_argument('job_id', type=str, help='Job ID')
+
+    def handle(self, *args, **options):
+        redis = Redis()
+        job_id = str(options['job_id'])
+        job = Job.fetch(job_id, connection=redis)
+        job.delete()
+        self.stdout.write(self.style.SUCCESS(f'Deleted {job}'))
diff --git a/common/management/commands/index_stats.py b/common/management/commands/index_stats.py
new file mode 100644
index 00000000..28a9f07e
--- /dev/null
+++ b/common/management/commands/index_stats.py
@@ -0,0 +1,40 @@
+from django.core.management.base import BaseCommand
+from common.index import Indexer
+from django.conf import settings
+from movies.models import Movie
+from books.models import Book
+from games.models import Game
+from music.models import Album, Song
+from django.core.paginator import Paginator
+from tqdm import tqdm
+from time import sleep
+from datetime import timedelta
+from django.utils import timezone
+
+
+class Command(BaseCommand):
+    help = 'Check search index'
+
+    def handle(self, *args, **options):
+        print(f'Connecting to search server')
+        stats = Indexer.get_stats()
+        print(stats)
+        st = Indexer.instance().get_all_update_status() 
+        cnt = {"enqueued": [0, 0], "processing": [0, 0], "processed": [0, 0], "failed": [0, 0]}
+        lastEnq = {"enqueuedAt": ""}
+        lastProc = {"enqueuedAt": ""}
+        for s in st:
+            n = s["type"].get("number")
+            cnt[s["status"]][0] += 1
+            cnt[s["status"]][1] += n if n else 0
+            if s["status"] == "processing":
+                print(s)
+            elif s["status"] == "enqueued":
+                if s["enqueuedAt"] > lastEnq["enqueuedAt"]:
+                    lastEnq = s
+            elif s["status"] == "processed":
+                if s["enqueuedAt"] > lastProc["enqueuedAt"]:
+                    lastProc = s
+        print(lastEnq)
+        print(lastProc)
+        print(cnt)
diff --git a/common/management/commands/init_index.py b/common/management/commands/init_index.py
new file mode 100644
index 00000000..797d32e4
--- /dev/null
+++ b/common/management/commands/init_index.py
@@ -0,0 +1,18 @@
+from django.core.management.base import BaseCommand
+from common.index import Indexer
+from django.conf import settings
+
+
+class Command(BaseCommand):
+    help = 'Initialize the search index'
+
+    def handle(self, *args, **options):
+        print(f'Connecting to search server')
+        Indexer.init()
+        self.stdout.write(self.style.SUCCESS('Index created.'))
+        # try:
+        #     Indexer.init()
+        #     self.stdout.write(self.style.SUCCESS('Index created.'))
+        # except Exception:
+        #     Indexer.update_settings()
+        #     self.stdout.write(self.style.SUCCESS('Index settings updated.'))
diff --git a/common/management/commands/list_jobs.py b/common/management/commands/list_jobs.py
new file mode 100644
index 00000000..e843f527
--- /dev/null
+++ b/common/management/commands/list_jobs.py
@@ -0,0 +1,24 @@
+from django.core.management.base import BaseCommand
+import pprint
+from redis import Redis
+from rq.job import Job
+from rq import Queue
+
+
+class Command(BaseCommand):
+    help = 'Show jobs in queue'
+
+    def add_arguments(self, parser):
+        parser.add_argument('queue', type=str, help='Queue')
+
+    def handle(self, *args, **options):
+        redis = Redis()
+        queue = Queue(str(options['queue']), connection=redis)
+        for registry in [queue.started_job_registry, queue.deferred_job_registry, queue.finished_job_registry, queue.failed_job_registry, queue.scheduled_job_registry]:
+            self.stdout.write(self.style.SUCCESS(f'Registry {registry}'))
+            for job_id in registry.get_job_ids():
+                try:
+                    job = Job.fetch(job_id, connection=redis)
+                    pprint.pp(job)
+                except Exception as e:
+                    print(f'Error fetching {job_id}')
diff --git a/common/management/commands/reindex.py b/common/management/commands/reindex.py
new file mode 100644
index 00000000..5dcc766f
--- /dev/null
+++ b/common/management/commands/reindex.py
@@ -0,0 +1,40 @@
+from django.core.management.base import BaseCommand
+from common.index import Indexer
+from django.conf import settings
+from movies.models import Movie
+from books.models import Book
+from games.models import Game
+from music.models import Album, Song
+from django.core.paginator import Paginator
+from tqdm import tqdm
+from time import sleep
+from datetime import timedelta
+from django.utils import timezone
+
+
+BATCH_SIZE = 1000
+
+
+class Command(BaseCommand):
+    help = 'Regenerate the search index'
+
+    # def add_arguments(self, parser):
+    #     parser.add_argument('hours', type=int, help='Re-index items modified in last N hours, 0 to reindex all')
+
+    def handle(self, *args, **options):
+        # h = int(options['hours'])
+        print(f'Connecting to search server')
+        if Indexer.busy():
+            print('Please wait for previous updates')
+        # Indexer.update_settings()
+        # self.stdout.write(self.style.SUCCESS('Index settings updated.'))
+        for c in [Book, Song, Album, Game, Movie]:
+            print(f'Re-indexing {c}')
+            qs = c.objects.all()  # if h == 0 else c.objects.filter(edited_time__gt=timezone.now() - timedelta(hours=h))
+            pg = Paginator(qs.order_by('id'), BATCH_SIZE)
+            for p in tqdm(pg.page_range):
+                items = list(map(lambda o: Indexer.obj_to_dict(o), pg.get_page(p).object_list))
+                if items:
+                    Indexer.replace_batch(items)
+                    while Indexer.busy():
+                        sleep(0.5)
diff --git a/common/management/commands/restart_sync.py b/common/management/commands/restart_sync.py
new file mode 100644
index 00000000..93903c57
--- /dev/null
+++ b/common/management/commands/restart_sync.py
@@ -0,0 +1,28 @@
+from django.core.management.base import BaseCommand
+from redis import Redis
+from rq.job import Job
+from sync.models import SyncTask
+from sync.jobs import import_doufen_task
+from django.utils import timezone
+import django_rq
+
+
+class Command(BaseCommand):
+    help = 'Restart a sync task'
+
+    def add_arguments(self, parser):
+        parser.add_argument('synctask_id', type=int, help='Sync Task ID')
+
+    def handle(self, *args, **options):
+        task = SyncTask.objects.get(id=options['synctask_id'])
+        task.finished_items = 0
+        task.failed_urls = []
+        task.success_items = 0
+        task.total_items = 0
+        task.is_finished = False
+        task.is_failed = False
+        task.break_point = ''
+        task.started_time = timezone.now()
+        task.save()
+        django_rq.get_queue('doufen').enqueue(import_doufen_task, task, job_id=f'SyncTask_{task.id}')
+        self.stdout.write(self.style.SUCCESS(f'Queued {task}'))
diff --git a/common/management/commands/scrape.py b/common/management/commands/scrape.py
new file mode 100644
index 00000000..d48898e6
--- /dev/null
+++ b/common/management/commands/scrape.py
@@ -0,0 +1,25 @@
+from django.core.management.base import BaseCommand
+from common.scraper import get_scraper_by_url, get_normalized_url
+import pprint
+
+
+class Command(BaseCommand):
+    help = 'Scrape an item from URL (but not save it)'
+
+    def add_arguments(self, parser):
+        parser.add_argument('url', type=str, help='URL to scrape')
+
+    def handle(self, *args, **options):
+        url = str(options['url'])
+        url = get_normalized_url(url)
+        scraper = get_scraper_by_url(url)
+
+        if scraper is None:
+            self.stdout.write(self.style.ERROR(f'Unable to match a scraper for {url}'))
+            return
+
+        effective_url = scraper.get_effective_url(url)
+        self.stdout.write(f'Fetching {effective_url} via {scraper.__name__}')
+        data, img = scraper.scrape(effective_url)
+        self.stdout.write(self.style.SUCCESS(f'Done.'))
+        pprint.pp(data)
diff --git a/common/models.py b/common/models.py
index c6bf6a70..5d1b55b5 100644
--- a/common/models.py
+++ b/common/models.py
@@ -1,29 +1,34 @@
 import re
 from decimal import *
 from markdown import markdown
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 from django.db import models, IntegrityError
 from django.core.serializers.json import DjangoJSONEncoder
-from django.db.models import Q
+from django.db.models import Q, Count, Sum
 from markdownx.models import MarkdownxField
 from users.models import User
-from mastodon.api import get_relationships, get_cross_site_id
-from boofilsic.settings import CLIENT_NAME
 from django.utils import timezone
+from django.conf import settings
 
 
 RE_HTML_TAG = re.compile(r"<[^>]*>")
+MAX_TOP_TAGS = 5
 
 
 # abstract base classes
 ###################################
 class SourceSiteEnum(models.TextChoices):
-    IN_SITE = "in-site", CLIENT_NAME
-    DOUBAN = "douban",  _("豆瓣")
+    IN_SITE = "in-site", settings.CLIENT_NAME
+    DOUBAN = "douban", _("豆瓣")
     SPOTIFY = "spotify", _("Spotify")
     IMDB = "imdb", _("IMDb")
     STEAM = "steam", _("STEAM")
     BANGUMI = 'bangumi', _("bangumi")
+    GOODREADS = "goodreads", _("goodreads")
+    TMDB = "tmdb", _("The Movie Database")
+    GOOGLEBOOKS = "googlebooks", _("Google Books")
+    BANDCAMP = "bandcamp", _("BandCamp")
+    IGDB = "igdb", _("IGDB")
 
 
 class Entity(models.Model):
@@ -52,10 +57,25 @@ class Entity(models.Model):
                 rating__lte=10), name='%(class)s_rating_upperbound'),
         ]
 
-
     def get_absolute_url(self):
         raise NotImplementedError("Subclass should implement this method")
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + self.get_absolute_url()
+
+    def get_json(self):
+        return {
+            'title': self.title,
+            'brief': self.brief,
+            'rating': self.rating,
+            'url': self.url,
+            'cover_url': settings.APP_WEBSITE + self.cover.url,
+            'top_tags': self.tags[:5],
+            'category_name': self.verbose_category_name,
+            'other_info': self.other_info,
+        }
+
     def save(self, *args, **kwargs):
         """ update rating and strip source url scheme & querystring before save to db """
         if self.rating_number and self.rating_total_score:
@@ -108,6 +128,15 @@ class Entity(models.Model):
         self.calculate_rating(old_rating, new_rating)
         self.save()
 
+    def refresh_rating(self):  # TODO: replace update_rating()
+        a = self.marks.filter(rating__gt=0).aggregate(Sum('rating'), Count('rating'))
+        if self.rating_total_score != a['rating__sum'] or self.rating_number != a['rating__count']:
+            self.rating_total_score = a['rating__sum']
+            self.rating_number = a['rating__count']
+            self.rating = a['rating__sum'] / a['rating__count'] if a['rating__count'] > 0 else None
+            self.save()
+        return self.rating
+
     def get_tags_manager(self):
         """
         Since relation between tag and entity is foreign key, and related name has to be unique,
@@ -115,9 +144,13 @@ class Entity(models.Model):
         """
         raise NotImplementedError("Subclass should implement this method.")
 
+    @property
+    def top_tags(self):
+        return self.get_tags_manager().values('content').annotate(tag_frequency=Count('content')).order_by('-tag_frequency')[:MAX_TOP_TAGS]
+
     def get_marks_manager(self):
         """
-        Normally this won't be used. 
+        Normally this won't be used.
         There is no ocassion where visitor can simply view all the marks.
         """
         raise NotImplementedError("Subclass should implement this method.")
@@ -129,6 +162,19 @@ class Entity(models.Model):
         """
         raise NotImplementedError("Subclass should implement this method.")
 
+    @property
+    def all_tag_list(self):
+        return self.get_tags_manager().values('content').annotate(frequency=Count('content')).order_by('-frequency')
+
+    @property
+    def tags(self):
+        return list(map(lambda t: t['content'], self.all_tag_list))
+
+    @property
+    def marks(self):
+        params = {self.__class__.__name__.lower() + '_id': self.id}
+        return self.mark_class.objects.filter(**params)
+
     @classmethod
     def get_category_mapping_dict(cls):
         category_mapping_dict = {}
@@ -144,75 +190,64 @@ class Entity(models.Model):
     def verbose_category_name(self):
         raise NotImplementedError("Subclass should implement this.")
 
+    @property
+    def mark_class(self):
+        raise NotImplementedError("Subclass should implement this.")
+
+    @property
+    def tag_class(self):
+        raise NotImplementedError("Subclass should implement this.")
+
 
 class UserOwnedEntity(models.Model):
-    is_private = models.BooleanField()
-    owner = models.ForeignKey(
-        User, on_delete=models.CASCADE, related_name='user_%(class)ss')
+    is_private = models.BooleanField(default=False, null=True)  # first set allow null, then migration, finally (in a few days) remove for good
+    visibility = models.PositiveSmallIntegerField(default=0)  # 0: Public / 1: Follower only / 2: Self only
+    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_%(class)ss')
     created_time = models.DateTimeField(default=timezone.now)
     edited_time = models.DateTimeField(default=timezone.now)
 
     class Meta:
         abstract = True
 
+    def is_visible_to(self, viewer):
+        if not viewer.is_authenticated:
+            return self.visibility == 0
+        owner = self.owner
+        if owner == viewer:
+            return True
+        if not owner.is_active:
+            return False
+        if self.visibility == 2:
+            return False
+        if viewer.is_blocking(owner) or owner.is_blocking(viewer) or viewer.is_muting(owner):
+            return False
+        if self.visibility == 1:
+            return viewer.is_following(owner)
+        else:
+            return True
+
+    def is_editable_by(self, viewer):
+        return True if viewer.is_staff or viewer.is_superuser or viewer == self.owner else False
+
     @classmethod
-    def get_available(cls, entity, request_user, token):
-        # TODO add amount limit for once query
-        """ 
-        Returns all avaliable user-owned entities related to given entity. 
-        This method handles mute/block relationships and private/public visibilities.
-        """
-        # the foreign key field that points to entity
-        # has to be named as the lower case name of that entity
+    def get_available(cls, entity, request_user, following_only=False):
+        # e.g. SongMark.get_available(song, request.user)
         query_kwargs = {entity.__class__.__name__.lower(): entity}
-        user_owned_entities = cls.objects.filter(
-            **query_kwargs).order_by("-edited_time")
-
-        # every user should only be abled to have one user owned entity for each entity
-        # this is guaranteed by models
-        id_list = []
-
-        # none_index tracks those failed cross site id query
-        none_index = []
-
-        for (i, entity) in enumerate(user_owned_entities):
-            if entity.owner.mastodon_site == request_user.mastodon_site:
-                id_list.append(entity.owner.mastodon_id)
-            else:
-                # TODO there could be many requests therefore make the pulling asynchronized
-                cross_site_id = get_cross_site_id(
-                    entity.owner, request_user.mastodon_site, token)
-                if not cross_site_id is None:
-                    id_list.append(cross_site_id)
-                else:
-                    none_index.append(i)
-                    # populate those query-failed None postions
-                    # to ensure the consistency of the orders of 
-                    # the three(id_list, user_owned_entities, relationships)
-                    id_list.append(request_user.mastodon_id)
-
-        # Mastodon request
-        relationships = get_relationships(
-            request_user.mastodon_site, id_list, token)
-        mute_block_blocked_index = []
-        following_index = []
-        for i, r in enumerate(relationships):
-            # the order of relationships is corresponding to the id_list,
-            # and the order of id_list is the same as user_owned_entiies
-            if r['blocking'] or r['blocked_by'] or r['muting']:
-                mute_block_blocked_index.append(i)
-            if r['following']:
-                following_index.append(i)
-        available_entities = [
-            e for i, e in enumerate(user_owned_entities)
-                if ((e.is_private == True and i in following_index) or e.is_private == False or e.owner == request_user)
-                    and not i in mute_block_blocked_index and not i in none_index
-        ]
-        return available_entities
+        all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time")  # get all marks for song
+        visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
+        return visible_entities
 
     @classmethod
-    def get_available_by_user(cls, owner, is_following):
-        """ 
+    def get_available_for_identicals(cls, entity, request_user, following_only=False):
+        # e.g. SongMark.get_available(song, request.user)
+        query_kwargs = {entity.__class__.__name__.lower() + '__in': entity.get_identicals()}
+        all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time")  # get all marks for song
+        visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
+        return visible_entities
+
+    @classmethod
+    def get_available_by_user(cls, owner, is_following):  # FIXME
+        """
         Returns all avaliable owner's entities.
         Mute/Block relation is not handled in this method.
 
@@ -220,10 +255,17 @@ class UserOwnedEntity(models.Model):
         :param is_following: if the current user is following the owner
         """
         user_owned_entities = cls.objects.filter(owner=owner)
-        if not is_following:
-            user_owned_entities = user_owned_entities.exclude(is_private=True)
+        if is_following:
+            user_owned_entities = user_owned_entities.exclude(visibility=2)
+        else:
+            user_owned_entities = user_owned_entities.filter(visibility=0)
         return user_owned_entities
 
+    @property
+    def item(self):
+        attr = re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', self.__class__.__name__)[0].lower()
+        return getattr(self, attr)
+
 
 # commonly used entity classes
 ###################################
@@ -236,10 +278,20 @@ class MarkStatusEnum(models.TextChoices):
 class Mark(UserOwnedEntity):
     status = models.CharField(choices=MarkStatusEnum.choices, max_length=20)
     rating = models.PositiveSmallIntegerField(blank=True, null=True)
-    text = models.CharField(max_length=500, blank=True, default='')
+    text = models.CharField(max_length=5000, blank=True, default='')
+    shared_link = models.CharField(max_length=5000, blank=True, default='')
 
     def __str__(self):
-        return f"({self.id}) {self.owner} {self.status.upper()}"
+        return f"Mark({self.id} {self.owner} {self.status.upper()})"
+
+    @property
+    def translated_status(self):
+        raise NotImplementedError("Subclass should implement this.")
+
+    @property
+    def tags(self):
+        tags = self.item.tag_class.objects.filter(mark_id=self.id)
+        return tags
 
     class Meta:
         abstract = True
@@ -249,7 +301,7 @@ class Mark(UserOwnedEntity):
             models.CheckConstraint(check=models.Q(
                 rating__lte=10), name='mark_rating_upperbound'),
         ]
-    
+
     # TODO update entity rating when save
     # TODO update tags
 
@@ -257,6 +309,7 @@ class Mark(UserOwnedEntity):
 class Review(UserOwnedEntity):
     title = models.CharField(max_length=120)
     content = MarkdownxField()
+    shared_link = models.CharField(max_length=5000, blank=True, default='')
 
     def __str__(self):
         return self.title
@@ -271,6 +324,10 @@ class Review(UserOwnedEntity):
     class Meta:
         abstract = True
 
+    @property
+    def translated_status(self):
+        return '评论了'
+
 
 class Tag(models.Model):
     content = models.CharField(max_length=50)
@@ -278,5 +335,28 @@ class Tag(models.Model):
     def __str__(self):
         return self.content
 
+    @property
+    def edited_time(self):
+        return self.mark.edited_time
+
+    @property
+    def created_time(self):
+        return self.mark.created_time
+
+    @property
+    def text(self):
+        return self.mark.text
+
+    @classmethod
+    def find_by_user(cls, tag, owner, viewer):
+        qs = cls.objects.filter(content=tag, mark__owner=owner)
+        if owner != viewer:
+            qs = qs.filter(mark__visibility__lte=owner.get_max_visibility(viewer))
+        return qs
+
+    @classmethod
+    def all_by_user(cls, owner):
+        return cls.objects.filter(mark__owner=owner).values('content').annotate(total=Count('content')).order_by('-total')
+
     class Meta:
         abstract = True
diff --git a/common/scraper.py b/common/scraper.py
index 33bef7e4..defe6f38 100644
--- a/common/scraper.py
+++ b/common/scraper.py
@@ -7,23 +7,17 @@ import dateparser
 import datetime
 import time
 import filetype
+import dns.resolver
+import urllib.parse
 from lxml import html
 from threading import Thread
-from boofilsic.settings import LUMINATI_USERNAME, LUMINATI_PASSWORD, DEBUG, IMDB_API_KEY, SCRAPERAPI_KEY
-from boofilsic.settings import SPOTIFY_CREDENTIAL
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.files.uploadedfile import SimpleUploadedFile
 from common.models import SourceSiteEnum
-from movies.models import Movie, MovieGenreEnum
-from movies.forms import MovieForm
-from books.models import Book
-from books.forms import BookForm
-from music.models import Album, Song
-from music.forms import AlbumForm, SongForm
-from games.models import Game
-from games.forms import GameForm
+from django.conf import settings
+from django.core.exceptions import ValidationError
 
 
 RE_NUMBERS = re.compile(r"\d+\d*")
@@ -43,12 +37,11 @@ DEFAULT_REQUEST_HEADERS = {
     'Cache-Control': 'no-cache',
 }
 
-# in seconds
-TIMEOUT = 60
 
 # luminati account credentials
 PORT = 22225
 
+
 logger = logging.getLogger(__name__)
 
 
@@ -56,6 +49,12 @@ logger = logging.getLogger(__name__)
 scraper_registry = {}
 
 
+def get_normalized_url(raw_url):
+    url = re.sub(r'//m.douban.com/(\w+)/', r'//\1.douban.com/', raw_url)
+    url = re.sub(r'//www.google.com/books/edition/_/([A-Za-z0-9_\-]+)[\?]*', r'//books.google.com/books?id=\1&', url)
+    return url
+
+
 def log_url(func):
     """
     Catch exceptions and log then pass the exceptions.
@@ -67,20 +66,23 @@ def log_url(func):
             return func(*args, **kwargs)
         except Exception as e:
             # log the url and trace stack
-            logger.error(f"Scrape Failed URL: {args[1]}")
-            logger.error("Expections during scraping:", exc_info=e)
+            logger.error(f"Scrape Failed URL: {args[1]}\n{e}")
+            if settings.DEBUG:
+                logger.error("Expections during scraping:", exc_info=e)
             raise e
 
     return wrapper
 
+
 def parse_date(raw_str):
     return dateparser.parse(
-        raw_str, 
+        raw_str,
         settings={
-        "RELATIVE_BASE": datetime.datetime(1900, 1, 1)
+            "RELATIVE_BASE": datetime.datetime(1900, 1, 1)
         }
     )
 
+
 class AbstractScraper:
     """
     Scrape entities. The entities means those defined in the models.py file,
@@ -119,7 +121,7 @@ class AbstractScraper:
 
         # decorate the scrape method
         cls.scrape = classmethod(log_url(cls.scrape))
-        
+
         # register scraper
         if isinstance(cls.host, list):
             for host in cls.host:
@@ -141,26 +143,30 @@ class AbstractScraper:
         """
         The return value should be identical with that saved in DB as `source_url`
         """
-        url = cls.regex.findall(raw_url)
+        url = cls.regex.findall(raw_url.replace('http:', 'https:'))  # force all http to be https
         if not url:
-            raise ValueError("not valid url")
+            raise ValueError(f"not valid url: {raw_url}")
         return url[0]
 
     @classmethod
     def download_page(cls, url, headers):
         url = cls.get_effective_url(url)
 
-        session_id = random.random()
-        proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
-                     (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
-        proxies = {
-            'http': proxy_url,
-            'https': proxy_url,
-        }
-        # if DEBUG:
-        #     proxies = None
+        if settings.LUMINATI_USERNAME is None:
+            proxies = None
+            if settings.SCRAPESTACK_KEY is not None:
+                url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
+        else:
+            session_id = random.random()
+            proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
+                         (settings.LUMINATI_USERNAME, session_id, settings.LUMINATI_PASSWORD, PORT))
+            proxies = {
+                'http': proxy_url,
+                'https': proxy_url,
+            }
+
         r = requests.get(url, proxies=proxies,
-                         headers=headers, timeout=TIMEOUT)
+                         headers=headers, timeout=settings.SCRAPING_TIMEOUT)
 
         if r.status_code != 200:
             raise RuntimeError(f"download page failed, status code {r.status_code}")
@@ -169,19 +175,19 @@ class AbstractScraper:
         return html.fromstring(r.content.decode('utf-8'))
 
     @classmethod
-    def download_image(cls, url):
+    def download_image(cls, url, item_url=None):
         if url is None:
-            return
+            return None, None
         raw_img = None
         session_id = random.random()
         proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %
-                     (LUMINATI_USERNAME, session_id, LUMINATI_PASSWORD, PORT))
+                     (settings.LUMINATI_USERNAME, session_id, settings.LUMINATI_PASSWORD, PORT))
         proxies = {
             'http': proxy_url,
             'https': proxy_url,
         }
-        # if DEBUG:
-        #     proxies = None
+        if settings.LUMINATI_USERNAME is None:
+            proxies = None
         if url:
             img_response = requests.get(
                 url,
@@ -194,7 +200,7 @@ class AbstractScraper:
                     'dnt': '1',
                 },
                 proxies=proxies,
-                timeout=TIMEOUT,
+                timeout=settings.SCRAPING_TIMEOUT,
             )
             if img_response.status_code == 200:
                 raw_img = img_response.content
@@ -205,13 +211,14 @@ class AbstractScraper:
         return raw_img, ext
 
     @classmethod
-    def save(cls, request_user):
+    def save(cls, request_user, instance=None):
         entity_cover = {
             'cover': SimpleUploadedFile('temp.' + cls.img_ext, cls.raw_img)
-        }
-        form = cls.form_class(cls.raw_data, entity_cover)
+        } if cls.img_ext is not None else None
+        form = cls.form_class(data=cls.raw_data, files=entity_cover, instance=instance)
         if form.is_valid():
             form.instance.last_editor = request_user
+            form.instance._change_reason = 'scrape'
             form.save()
             cls.instance = form.instance
         else:
@@ -220,1145 +227,37 @@ class AbstractScraper:
         return form
 
 
-class DoubanScrapperMixin:
-    @classmethod
-    def download_page(cls, url, headers):
-        url = cls.get_effective_url(url)
-
-        scraper_api_endpoint = f'http://api.scraperapi.com?api_key={SCRAPERAPI_KEY}&url={url}'
-
-        r = requests.get(scraper_api_endpoint, timeout=TIMEOUT)
-
-        if r.status_code != 200:
-            raise RuntimeError(f"download page failed, status code {r.status_code}")
-        # with open('temp.html', 'w', encoding='utf-8') as fp:
-        #     fp.write(r.content.decode('utf-8'))
-        return html.fromstring(r.content.decode('utf-8'))
-
-
-class DoubanBookScraper(DoubanScrapperMixin, 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)
-
-        # parsing starts here
-        try:
-            title = content.xpath("/html/body//h1/span/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no book info")
-
-        subtitle_elem = content.xpath(
-            "//div[@id='info']//span[text()='副标题:']/following::text()")
-        subtitle = subtitle_elem[0].strip() if subtitle_elem else None
-
-        orig_title_elem = content.xpath(
-            "//div[@id='info']//span[text()='原作名:']/following::text()")
-        orig_title = orig_title_elem[0].strip() if orig_title_elem else None
-
-        language_elem = content.xpath(
-            "//div[@id='info']//span[text()='语言:']/following::text()")
-        language = language_elem[0].strip() if language_elem else None
-
-        pub_house_elem = content.xpath(
-            "//div[@id='info']//span[text()='出版社:']/following::text()")
-        pub_house = pub_house_elem[0].strip() if pub_house_elem else None
-
-        pub_date_elem = content.xpath(
-            "//div[@id='info']//span[text()='出版年:']/following::text()")
-        pub_date = pub_date_elem[0].strip() if pub_date_elem else ''
-        year_month_day = RE_NUMBERS.findall(pub_date)
-        if len(year_month_day) in (2, 3):
-            pub_year = int(year_month_day[0])
-            pub_month = int(year_month_day[1])
-        elif len(year_month_day) == 1:
-            pub_year = int(year_month_day[0])
-            pub_month = None
-        else:
-            pub_year = None
-            pub_month = None
-        if pub_year and pub_month and pub_year < pub_month:
-            pub_year, pub_month = pub_month, pub_year
-        pub_year = None if pub_year is not None and not pub_year in range(
-            0, 3000) else pub_year
-        pub_month = None if pub_month is not None and not pub_month in range(
-            1, 12) else pub_month
-
-        binding_elem = content.xpath(
-            "//div[@id='info']//span[text()='装帧:']/following::text()")
-        binding = binding_elem[0].strip() if binding_elem else None
-
-        price_elem = content.xpath(
-            "//div[@id='info']//span[text()='定价:']/following::text()")
-        price = price_elem[0].strip() if price_elem else None
-
-        pages_elem = content.xpath(
-            "//div[@id='info']//span[text()='页数:']/following::text()")
-        pages = pages_elem[0].strip() if pages_elem else None
-        if pages is not None:
-            pages = int(RE_NUMBERS.findall(pages)[
-                        0]) if RE_NUMBERS.findall(pages) else None
-
-        isbn_elem = content.xpath(
-            "//div[@id='info']//span[text()='ISBN:']/following::text()")
-        isbn = isbn_elem[0].strip() if isbn_elem else None
-
-        brief_elem = content.xpath(
-            "//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()")
-        brief = '\n'.join(p.strip()
-                          for p in brief_elem) if brief_elem else None
-
-        contents = None
-        try:
-            contents_elem = content.xpath(
-                "//h2/span[text()='目录']/../following-sibling::div[1]")[0]
-            # if next the id of next sibling contains `dir`, that would be the full contents
-            if "dir" in contents_elem.getnext().xpath("@id")[0]:
-                contents_elem = contents_elem.getnext()
-                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
-                    "text()")[:-2]) if contents_elem else None
-            else:
-                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
-                    "text()")) if contents_elem else None
-        except:
-            pass
-
-        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)
-
-        # there are two html formats for authors and translators
-        authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
-            preceding-sibling::a[preceding-sibling::span[text()='作者:']]/text()""")
-        if not authors_elem:
-            authors_elem = content.xpath(
-                """//div[@id='info']//span[text()=' 作者']/following-sibling::a/text()""")
-        if authors_elem:
-            authors = []
-            for author in authors_elem:
-                authors.append(RE_WHITESPACES.sub(' ', author.strip()))
-        else:
-            authors = None
-
-        translators_elem = content.xpath("""//div[@id='info']//span[text()='译者:']/following-sibling::br[1]/
-            preceding-sibling::a[preceding-sibling::span[text()='译者:']]/text()""")
-        if not translators_elem:
-            translators_elem = content.xpath(
-                """//div[@id='info']//span[text()=' 译者']/following-sibling::a/text()""")
-        if translators_elem:
-            translators = []
-            for translator in translators_elem:
-                translators.append(RE_WHITESPACES.sub(' ', translator.strip()))
-        else:
-            translators = None
-
-        other = {}
-        cncode_elem = content.xpath(
-            "//div[@id='info']//span[text()='统一书号:']/following::text()")
-        if cncode_elem:
-            other['统一书号'] = cncode_elem[0].strip()
-        series_elem = content.xpath(
-            "//div[@id='info']//span[text()='丛书:']/following-sibling::a[1]/text()")
-        if series_elem:
-            other['丛书'] = series_elem[0].strip()
-        imprint_elem = content.xpath(
-            "//div[@id='info']//span[text()='出品方:']/following-sibling::a[1]/text()")
-        if imprint_elem:
-            other['出品方'] = imprint_elem[0].strip()
-
-        data = {
-            'title': title,
-            'subtitle': subtitle,
-            'orig_title': orig_title,
-            'author': authors,
-            'translator': translators,
-            'language': language,
-            'pub_house': pub_house,
-            'pub_year': pub_year,
-            'pub_month': pub_month,
-            'binding': binding,
-            'price': price,
-            'pages': pages,
-            'isbn': isbn,
-            'brief': brief,
-            'contents': contents,
-            'other_info': other,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-class DoubanMovieScraper(DoubanScrapperMixin, 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)
-
-        # parsing starts here
-        try:
-            raw_title = content.xpath(
-                "//span[@property='v:itemreviewed']/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no movie info")
-
-        orig_title = content.xpath(
-            "//img[@rel='v:image']/@alt")[0].strip()
-        title = raw_title.split(orig_title)[0].strip()
-        # if has no chinese title
-        if title == '':
-            title = orig_title
-
-        if title == orig_title:
-            orig_title = None
-
-        # there are two html formats for authors and translators
-        other_title_elem = content.xpath(
-            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
-        other_title = other_title_elem[0].strip().split(
-            ' / ') if other_title_elem else None
-
-        imdb_elem = content.xpath(
-            "//div[@id='info']//span[text()='IMDb链接:']/following-sibling::a[1]/text()")
-        if not imdb_elem:
-            imdb_elem = content.xpath(
-                "//div[@id='info']//span[text()='IMDb:']/following-sibling::text()[1]")
-        imdb_code = imdb_elem[0].strip() if imdb_elem else None
-
-        director_elem = content.xpath(
-            "//div[@id='info']//span[text()='导演']/following-sibling::span[1]/a/text()")
-        director = director_elem if director_elem else None
-
-        playwright_elem = content.xpath(
-            "//div[@id='info']//span[text()='编剧']/following-sibling::span[1]/a/text()")
-        playwright = playwright_elem if playwright_elem else None
-
-        actor_elem = content.xpath(
-            "//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()")
-        actor = actor_elem if actor_elem else None
-
-        # construct genre translator
-        genre_translator = {}
-        attrs = [attr for attr in dir(MovieGenreEnum) if not '__' in attr]
-        for attr in attrs:
-            genre_translator[getattr(MovieGenreEnum, attr).label] = getattr(
-                MovieGenreEnum, attr).value
-
-        genre_elem = content.xpath("//span[@property='v:genre']/text()")
-        if genre_elem:
-            genre = []
-            for g in genre_elem:
-                genre.append(genre_translator[g])
-        else:
-            genre = None
-
-        showtime_elem = content.xpath(
-            "//span[@property='v:initialReleaseDate']/text()")
-        if showtime_elem:
-            showtime = []
-            for st in showtime_elem:
-                parts = st.split('(')
-                if len(parts) == 1:
-                    time = st.split('(')[0]
-                    region = ''
-                else:
-                    time = st.split('(')[0]
-                    region = st.split('(')[1][0:-1]
-                showtime.append({time: region})
-        else:
-            showtime = None
-
-        site_elem = content.xpath(
-            "//div[@id='info']//span[text()='官方网站:']/following-sibling::a[1]/@href")
-        site = site_elem[0].strip() if site_elem else None
-
-        area_elem = content.xpath(
-            "//div[@id='info']//span[text()='制片国家/地区:']/following-sibling::text()[1]")
-        if area_elem:
-            area = [a.strip() for a in area_elem[0].split(' / ')]
-        else:
-            area = None
-
-        language_elem = content.xpath(
-            "//div[@id='info']//span[text()='语言:']/following-sibling::text()[1]")
-        if language_elem:
-            language = [a.strip() for a in language_elem[0].split(' / ')]
-        else:
-            language = None
-
-        year_elem = content.xpath("//span[@class='year']/text()")
-        year = int(year_elem[0][1:-1]) if year_elem else None
-
-        duration_elem = content.xpath("//span[@property='v:runtime']/text()")
-        other_duration_elem = content.xpath(
-            "//span[@property='v:runtime']/following-sibling::text()[1]")
-        if duration_elem:
-            duration = duration_elem[0].strip()
-            if other_duration_elem:
-                duration += other_duration_elem[0].rstrip()
-        else:
-            duration = None
-
-        season_elem = content.xpath(
-
-            "//*[@id='season']/option[@selected='selected']/text()")
-        if not season_elem:
-            season_elem = content.xpath(
-                "//div[@id='info']//span[text()='季数:']/following-sibling::text()[1]")
-            season = int(season_elem[0].strip()) if season_elem else None
-        else:
-            season = int(season_elem[0].strip())
-
-        episodes_elem = content.xpath(
-            "//div[@id='info']//span[text()='集数:']/following-sibling::text()[1]")
-        episodes = int(episodes_elem[0].strip()) if episodes_elem else None
-
-        single_episode_length_elem = content.xpath(
-            "//div[@id='info']//span[text()='单集片长:']/following-sibling::text()[1]")
-        single_episode_length = single_episode_length_elem[0].strip(
-        ) if single_episode_length_elem else None
-
-        # if has field `episodes` not none then must be series
-        is_series = True if episodes else False
-
-        brief_elem = content.xpath("//span[@class='all hidden']")
-        if not brief_elem:
-            brief_elem = content.xpath("//span[@property='v:summary']")
-        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
-            './text()')]) if brief_elem else None
-
-        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)
-
-        data = {
-            'title': title,
-            'orig_title': orig_title,
-            'other_title': other_title,
-            'imdb_code': imdb_code,
-            'director': director,
-            'playwright': playwright,
-            'actor': actor,
-            'genre': genre,
-            'showtime': showtime,
-            'site': site,
-            'area': area,
-            'language': language,
-            'year': year,
-            'duration': duration,
-            'season': season,
-            'episodes': episodes,
-            'single_episode_length': single_episode_length,
-            'brief': brief,
-            'is_series': is_series,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-class DoubanAlbumScraper(DoubanScrapperMixin, 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)
-
-        # parsing starts here
-        try:
-            title = content.xpath("//h1/span/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no album info")
-        if not title:
-            raise ValueError("given url contains no album info")
-            
-
-        artists_elem = content.xpath("""//div[@id='info']/span/span[@class='pl']/a/text()""")
-        artist = None if not artists_elem else artists_elem
-
-        genre_elem = content.xpath(
-            "//div[@id='info']//span[text()='流派:']/following::text()[1]")
-        genre = genre_elem[0].strip() if genre_elem else None
-
-        date_elem = content.xpath(
-            "//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
-        release_date = parse_date(date_elem[0].strip()) if date_elem else None
-
-        company_elem = content.xpath(
-            "//div[@id='info']//span[text()='出版者:']/following::text()[1]")
-        company = company_elem[0].strip() if company_elem else None
-
-        track_list_elem = content.xpath(
-            "//div[@class='track-list']/div[@class='indent']/div/text()"
-        )
-        if track_list_elem:
-            track_list = '\n'.join([track.strip() for track in track_list_elem])
-        else:
-            track_list = None
-
-        brief_elem = content.xpath("//span[@class='all hidden']")
-        if not brief_elem:
-            brief_elem = content.xpath("//span[@property='v:summary']")
-        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
-            './text()')]) if brief_elem else None
-
-        other_info = {}
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['又名'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['专辑类型'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['介质'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['ISRC'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['条形码'] = other_elem[0].strip()
-        other_elem = content.xpath(
-            "//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
-        if other_elem:
-            other_info['碟片数'] = other_elem[0].strip()
-
-        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)
-
-        data = {
-            'title': title,
-            'artist': artist,
-            'genre': genre,
-            'release_date': release_date,
-            'duration': None,
-            'company': company,
-            'track_list': track_list,
-            'brief': brief,
-            'other_info': other_info,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-spotify_token = None
-spotify_token_expire_time = time.time()
-
-class SpotifyTrackScraper(AbstractScraper):
-    site_name = SourceSiteEnum.SPOTIFY.value
-    host = 'https://open.spotify.com/track/'
-    data_class = Song
-    form_class = SongForm
-
-    regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+")
-
-    def scrape(self, url):
-        """
-        Request from API, not really scraping
-        """
-        global spotify_token, spotify_token_expire_time
-
-        if spotify_token is None or is_spotify_token_expired():
-            invoke_spotify_token()
-        effective_url = self.get_effective_url(url)
-        if effective_url is None:
-            raise ValueError("not valid url")
-
-        api_url = self.get_api_url(effective_url)
-        headers = {
-            'Authorization': f"Bearer {spotify_token}"
-        }
-        r = requests.get(api_url, headers=headers)
-        res_data = r.json()
-
-        artist = []
-        for artist_dict in res_data['artists']:
-            artist.append(artist_dict['name'])
-        if not artist:
-            artist = None
-
-        title = res_data['name']
-
-        release_date = parse_date(res_data['album']['release_date'])
-
-        duration = res_data['duration_ms']
-
-        if res_data['external_ids'].get('isrc'):
-            isrc = res_data['external_ids']['isrc']
-        else:
-            isrc = None
-
-        raw_img, ext = self.download_image(res_data['album']['images'][0]['url'])
-        
-        data = {
-            'title': title,
-            'artist': artist,
-            'genre': None,
-            'release_date': release_date,
-            'duration': duration,
-            'isrc': isrc,
-            'album': None,
-            'brief': None,
-            'other_info': None,
-            'source_site': self.site_name,
-            'source_url': effective_url,
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-    @classmethod
-    def get_effective_url(cls, raw_url):
-        code = cls.regex.findall(raw_url)
-        if code:
-            return f"https://open.spotify.com/track/{code[0]}"
-        else:
-            return None
-
-    @classmethod
-    def get_api_url(cls, url):
-        return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0]
-
-
-class SpotifyAlbumScraper(AbstractScraper):
-    site_name = SourceSiteEnum.SPOTIFY.value
-    # API URL
-    host = 'https://open.spotify.com/album/'
-    data_class = Album
-    form_class = AlbumForm
-
-    regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+")
-
-    def scrape(self, url):
-        """
-        Request from API, not really scraping
-        """
-        global spotify_token, spotify_token_expire_time
-
-        if spotify_token is None or is_spotify_token_expired():
-            invoke_spotify_token()
-        effective_url = self.get_effective_url(url)
-        if effective_url is None:
-            raise ValueError("not valid url")
-
-        api_url = self.get_api_url(effective_url)
-        headers = {
-            'Authorization': f"Bearer {spotify_token}"
-        }
-        r = requests.get(api_url, headers=headers)
-        res_data = r.json()
-
-        artist = []
-        for artist_dict in res_data['artists']:
-            artist.append(artist_dict['name'])
-
-        title = res_data['name']
-
-        genre = ', '.join(res_data['genres'])
-
-        company = []
-        for com in res_data['copyrights']:
-            company.append(com['text'])
-
-        duration = 0
-        track_list = []
-        track_urls = []
-        for track in res_data['tracks']['items']:
-            track_urls.append(track['external_urls']['spotify'])
-            duration += track['duration_ms']
-            if res_data['tracks']['items'][-1]['disc_number'] > 1:
-                # more than one disc
-                track_list.append(str(
-                    track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name'])
-            else:
-                track_list.append(str(track['track_number']) + '. ' + track['name'])
-        track_list = '\n'.join(track_list)
-
-        release_date = parse_date(res_data['release_date'])
-
-        other_info = {}
-        if res_data['external_ids'].get('upc'):
-            # bar code
-            other_info['UPC'] = res_data['external_ids']['upc']
-
-        raw_img, ext = self.download_image(res_data['images'][0]['url'])
-
-        data = {
-            'title': title,
-            'artist': artist,
-            'genre': genre,
-            'track_list': track_list,
-            'release_date': release_date,
-            'duration': duration,
-            'company': company,
-            'brief': None,
-            'other_info': other_info,
-            'source_site': self.site_name,
-            'source_url': effective_url,
-        }
-
-        # set tracks_data, used for adding tracks
-        self.track_urls = track_urls
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-    @classmethod
-    def get_effective_url(cls, raw_url):
-        code = cls.regex.findall(raw_url)
-        if code:
-            return f"https://open.spotify.com/album/{code[0]}"
-        else:
-            return None
-
-    @classmethod
-    def save(cls, request_user):
-        form = super().save(request_user)
-        task = Thread(
-            target=cls.add_tracks,
-            args=(form.instance, request_user),
-            daemon=True
-        )
-        task.start()
-        return form
-
-    @classmethod
-    def get_api_url(cls, url):
-        return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0]
-
-    @classmethod
-    def add_tracks(cls, album: Album, request_user):
-        to_be_updated_tracks = []
-        for track_url in cls.track_urls:
-            track = cls.get_track_or_none(track_url)
-            # seems lik if fire too many requests at the same time 
-            # spotify would limit access
-            if track is None:
-                task = Thread(
-                    target=cls.scrape_and_save_track,
-                    args=(track_url, album, request_user),
-                    daemon=True
-                )
-                task.start()
-                task.join()
-            else:
-                to_be_updated_tracks.append(track)
-        cls.bulk_update_track_album(to_be_updated_tracks, album, request_user)
-        
-    @classmethod
-    def get_track_or_none(cls, track_url: str):
-        try:
-            instance = Song.objects.get(source_url=track_url)
-            return instance
-        except ObjectDoesNotExist:
-            return None
-        
-    @classmethod
-    def scrape_and_save_track(cls, url: str, album: Album, request_user):
-        data, img = SpotifyTrackScraper.scrape(url)
-        SpotifyTrackScraper.raw_data['album'] = album
-        SpotifyTrackScraper.save(request_user)
-        
-    @classmethod
-    def bulk_update_track_album(cls, tracks, album, request_user):
-        for track in tracks:
-            track.last_editor = request_user
-            track.edited_time = timezone.now()
-            track.album = album
-        Song.objects.bulk_update(tracks, [
-            'last_editor',
-            'edited_time',
-            'album'
-        ])
-
-
-def is_spotify_token_expired():
-    global spotify_token_expire_time
-    return True if spotify_token_expire_time <= time.time() else False
-
-
-def invoke_spotify_token():
-    global spotify_token, spotify_token_expire_time
-    r = requests.post(
-        "https://accounts.spotify.com/api/token",
-        data={
-            "grant_type": "client_credentials"
-        },
-        headers={
-            "Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
-        }
-    )
-    data = r.json()
-    if r.status_code == 401:
-        # token expired, try one more time
-        # this maybe caused by external operations,
-        # for example debugging using a http client
-        r = requests.post(
-            "https://accounts.spotify.com/api/token",
-            data={
-                "grant_type": "client_credentials"
-            },
-            headers={
-                "Authorization": f"Basic {SPOTIFY_CREDENTIAL}"
-            }
-        )
-        data = r.json()
-    elif r.status_code != 200:
-        raise Exception(f"Request to spotify API fails. Reason: {r.reason}")
-    # minus 2 for execution time error
-    spotify_token_expire_time = int(data['expires_in']) + time.time() - 2
-    spotify_token = data['access_token']
-
-
-class ImdbMovieScraper(AbstractScraper):
-    site_name = SourceSiteEnum.IMDB.value
-    host = 'https://www.imdb.com/title/'
-    data_class = Movie
-    form_class = MovieForm
-
-    regex = re.compile(r"(?<=https://www\.imdb\.com/title/)[a-zA-Z0-9]+")
-
-    def scrape(self, url):
-
-        effective_url = self.get_effective_url(url)
-        if effective_url is None:
-            raise ValueError("not valid url")
-
-        api_url = self.get_api_url(effective_url)
-        r = requests.get(api_url)
-        res_data = r.json()
-
-        if not res_data['type'] in ['Movie', 'TVSeries']:
-            raise ValueError("not movie/series item")
-
-        if res_data['type'] == 'Movie':
-            is_series = False
-        elif res_data['type'] == 'TVSeries':
-            is_series = True
-
-        title = res_data['title']
-        orig_title = res_data['originalTitle']
-        imdb_code = self.regex.findall(effective_url)[0]
-        director = []
-        for direct_dict in res_data['directorList']:
-            director.append(direct_dict['name'])
-        playwright = []
-        for writer_dict in res_data['writerList']:
-            playwright.append(writer_dict['name'])
-        actor = []
-        for actor_dict in res_data['actorList']:
-            actor.append(actor_dict['name'])
-        genre = res_data['genres'].split(', ')
-        area = res_data['countries'].split(', ')
-        language = res_data['languages'].split(', ')
-        year = int(res_data['year'])
-        duration = res_data['runtimeStr']
-        brief = res_data['plotLocal'] if res_data['plotLocal'] else res_data['plot']
-        if res_data['releaseDate']:
-            showtime = [{res_data['releaseDate']: "发布日期"}]
-        else:
-            showtime = None
-
-        other_info = {}
-        if res_data['contentRating']:
-            other_info['分级'] = res_data['contentRating'] 
-        if res_data['imDbRating']:
-            other_info['IMDb评分'] = res_data['imDbRating'] 
-        if res_data['metacriticRating']:
-            other_info['Metacritic评分'] = res_data['metacriticRating'] 
-        if res_data['awards']:
-            other_info['奖项'] = res_data['awards'] 
-
-        raw_img, ext = self.download_image(res_data['image'])
-
-        data = {
-            'title': title,
-            'orig_title': orig_title,
-            'other_title': None,
-            'imdb_code': imdb_code,
-            'director': director,
-            'playwright': playwright,
-            'actor': actor,
-            'genre': genre,
-            'showtime': showtime,
-            'site': None,
-            'area': area,
-            'language': language,
-            'year': year,
-            'duration': duration,
-            'season': None,
-            'episodes': None,
-            'single_episode_length': None,
-            'brief': brief,
-            'is_series': is_series,
-            'other_info': other_info,
-            'source_site': self.site_name,
-            'source_url': effective_url,
-        }
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-    @classmethod
-    def get_effective_url(cls, raw_url):
-        code = cls.regex.findall(raw_url)
-        if code:
-            return f"https://www.imdb.com/title/{code[0]}/"
-        else:
-            return None
-
-    @classmethod
-    def get_api_url(cls, url):
-        return f"https://imdb-api.com/zh/API/Title/{IMDB_API_KEY}/{cls.regex.findall(url)[0]}/FullActor,"
-
-
-class DoubanGameScraper(DoubanScrapperMixin, AbstractScraper):
-    site_name = SourceSiteEnum.DOUBAN.value
-    host = 'www.douban.com/game/'
-    data_class = Game
-    form_class = GameForm
-
-    regex = re.compile(r"https://www\.douban\.com/game/\d+/{0,1}")
-
-    def scrape(self, url):
-        headers = DEFAULT_REQUEST_HEADERS.copy()
-        headers['Host'] = 'www.douban.com'
-        content = self.download_page(url, headers)
-
-        try:
-            raw_title = content.xpath(
-                "//div[@id='content']/h1/text()")[0].strip()
-        except IndexError:
-            raise ValueError("given url contains no game info")
-
-        title = raw_title
-
-        other_title_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='别名:']/following-sibling::dd[1]/text()")
-        other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None
-        
-        developer_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='开发商:']/following-sibling::dd[1]/text()")
-        developer = developer_elem[0].strip().split(' / ') if developer_elem else None
-
-        publisher_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='发行商:']/following-sibling::dd[1]/text()")
-        publisher = publisher_elem[0].strip().split(' / ') if publisher_elem else None
-
-        platform_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='平台:']/following-sibling::dd[1]/a/text()")
-        platform = platform_elem if platform_elem else None
-
-        genre_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='类型:']/following-sibling::dd[1]/a/text()")
-        genre = None
-        if genre_elem:
-            genre = [g for g in genre_elem if g != '游戏']
-
-        date_elem = content.xpath(
-            "//dl[@class='game-attr']//dt[text()='发行日期:']/following-sibling::dd[1]/text()")
-        release_date = parse_date(date_elem[0].strip()) if date_elem else None
-
-        brief_elem = content.xpath("//div[@class='mod item-desc']/p/text()")
-        brief = '\n'.join(brief_elem) if brief_elem else None
-
-        img_url_elem = content.xpath(
-            "//div[@class='item-subject-info']/div[@class='pic']//img/@src")
-        img_url = img_url_elem[0].strip() if img_url_elem else None
-        raw_img, ext = self.download_image(img_url)
-
-        data = {
-            'title': title,
-            'other_title': other_title,
-            'developer': developer,
-            'publisher': publisher,
-            'release_date': release_date,
-            'genre': genre,
-            'platform': platform,
-            'brief': brief,
-            'other_info': None,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-class SteamGameScraper(AbstractScraper):
-    site_name = SourceSiteEnum.STEAM.value
-    host = 'store.steampowered.com'
-    data_class = Game
-    form_class = GameForm
-
-    regex = re.compile(r"https://store\.steampowered\.com/app/\d+/{0,1}")
-
-    def scrape(self, url):
-        headers = DEFAULT_REQUEST_HEADERS.copy()
-        headers['Host'] = self.host
-        headers['Cookie'] = "wants_mature_content=1; birthtime=754700401;"
-        content = self.download_page(url, headers)
-        
-        title = content.xpath("//div[@class='apphub_AppName']/text()")[0]
-        developer = content.xpath("//div[@id='developers_list']/a/text()")
-        publisher = content.xpath("//div[@class='glance_ctn']//div[@class='dev_row'][2]//a/text()")
-        release_date = parse_date(
-            content.xpath(
-                "//div[@class='release_date']/div[@class='date']/text()")[0]
-        )
-
-        genre = content.xpath(
-            "//div[@class='details_block']/b[2]/following-sibling::a/text()")
-
-        platform = ['PC']
-
-        brief = content.xpath(
-            "//div[@class='game_description_snippet']/text()")[0].strip()
-
-        img_url = content.xpath(
-            "//img[@class='game_header_image_full']/@src"
-        )[0].replace("header.jpg", "library_600x900.jpg")
-        raw_img, ext = self.download_image(img_url)
-
-        # no 600x900 picture
-        if raw_img is None:
-            img_url = content.xpath("//img[@class='game_header_image_full']/@src")[0]
-            raw_img, ext = self.download_image(img_url)
-
-        data = {
-            'title': title,
-            'other_title': None,
-            'developer': developer,
-            'publisher': publisher,
-            'release_date': release_date,
-            'genre': genre,
-            'platform': platform,
-            'brief': brief,
-            'other_info': None,
-            'source_site': self.site_name,
-            'source_url': self.get_effective_url(url),
-        }
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-def find_entity(source_url):
-    """
-    for bangumi
-    """
-    # to be added when new scrape method is implemented
-    result = Game.objects.filter(source_url=source_url)
-    if result:
-        return result[0]
-    else:
-        raise ObjectDoesNotExist
-
-class BangumiScraper(AbstractScraper):
-    site_name = SourceSiteEnum.BANGUMI.value
-    host = 'bgm.tv'
-
-    # for interface coherence
-    data_class = type("FakeDataClass", (object,), {})()
-    data_class.objects = type("FakeObjectsClass", (object,), {})()
-    data_class.objects.get = find_entity
-    # should be set at scrape_* method
-    form_class = ''
-
-
-    regex = re.compile(r"https{0,1}://bgm\.tv/subject/\d+")
-
-    def scrape(self, url):
-        """
-        This is the scraping portal
-        """
-        headers = DEFAULT_REQUEST_HEADERS.copy()
-        headers['Host'] = self.host
-        content = self.download_page(url, headers)
-
-        # download image
-        img_url = 'http:' + content.xpath("//div[@class='infobox']//img[1]/@src")[0]
-        raw_img, ext = self.download_image(img_url)
-
-        # Test category
-        category_code = content.xpath("//div[@id='headerSearch']//option[@selected]/@value")[0]
-        handler_map = {
-            '1': self.scrape_book,
-            '2': self.scrape_movie,
-            '3': self.scrape_album,
-            '4': self.scrape_game
-        }
-        data = handler_map[category_code](self, content)
-        data['source_url'] = self.get_effective_url(url)
-
-        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
-        return data, raw_img
-
-
-    def scrape_game(self, content):
-        self.data_class = Game
-        self.form_class = GameForm
-
-        title_elem = content.xpath("//a[@property='v:itemreviewed']/text()")
-        if not title_elem:
-            raise ValueError("no game info found on this page")
-            title = None
-        else:
-            title = title_elem[0].strip()
-
-        other_title_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/text()")
-        if not other_title_elem:
-            other_title_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/a/text()")
-        other_title = other_title_elem if other_title_elem else []
-
-        chinese_name_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/text()")
-        if not chinese_name_elem:
-            chinese_name_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/a/text()")
-        if chinese_name_elem:
-            chinese_name = chinese_name_elem[0]
-            # switch chinese name with original name
-            title, chinese_name = chinese_name, title
-            # actually the name appended is original
-            other_title.append(chinese_name)
-            
-        developer_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/text()")
-        if not developer_elem:
-            developer_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/a/text()")
-        developer = developer_elem if developer_elem else None
-
-        publisher_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/text()")
-        if not publisher_elem:
-            publisher_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/a/text()")
-        publisher = publisher_elem if publisher_elem else None
-
-        platform_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/text()")
-        if not platform_elem:
-            platform_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/a/text()")
-        platform = platform_elem if platform_elem else None
-
-        genre_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/text()")
-        if not genre_elem:
-            genre_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/a/text()")
-        genre = genre_elem if genre_elem else None
-
-        date_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/text()")
-        if not date_elem:
-            date_elem = content.xpath(
-                "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/a/text()")
-        release_date = parse_date(date_elem[0]) if date_elem else None
-
-        brief = ''.join(content.xpath("//div[@property='v:summary']/text()"))
-        
-        other_info = {}
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'人数')]]/text()")
-        if other_elem:
-            other_info['游玩人数'] = other_elem[0]
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'引擎')]]/text()")
-        if other_elem:
-            other_info['引擎'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'售价')]]/text()")
-        if other_elem:
-            other_info['售价'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'官方网站')]]/text()")
-        if other_elem:
-            other_info['网站'] = other_elem[0]
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/text()")
-        if other_elem:
-            other_info['剧本'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/text()")
-        if other_elem:
-            other_info['编剧'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/text()")
-        if other_elem:
-            other_info['音乐'] = ' '.join(other_elem)
-        other_elem = content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/a/text()") or content.xpath(
-            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/text()")
-        if other_elem:
-            other_info['美术'] = ' '.join(other_elem)
-
-        data = {
-            'title': title,
-            'other_title': None,
-            'developer': developer,
-            'publisher': publisher,
-            'release_date': release_date,
-            'genre': genre,
-            'platform': platform,
-            'brief': brief,
-            'other_info': other_info,
-            'source_site': self.site_name,
-        }
-
-        return data
-
-    def scrape_movie(self, content):
-        self.data_class = Movie
-        self.form_class = MovieForm
-        raise NotImplementedError
-
-    def scrape_book(self, content):
-        self.data_class = Book
-        self.form_class = BookForm
-        raise NotImplementedError
-
-    def scrape_album(self, content):
-        self.data_class = Album
-        self.form_class = AlbumForm
-        raise NotImplementedError
-
-
-# https://developers.google.com/youtube/v3/docs/?apix=true
-# https://developers.google.com/books/docs/v1/using
-
+from common.scrapers.bandcamp import BandcampAlbumScraper
+from common.scrapers.goodreads import GoodreadsScraper
+from common.scrapers.google import GoogleBooksScraper
+from common.scrapers.tmdb import TmdbMovieScraper
+from common.scrapers.steam import SteamGameScraper
+from common.scrapers.imdb import ImdbMovieScraper
+from common.scrapers.igdb import IgdbGameScraper
+from common.scrapers.spotify import SpotifyAlbumScraper, SpotifyTrackScraper
+from common.scrapers.douban import DoubanAlbumScraper, DoubanBookScraper, DoubanGameScraper, DoubanMovieScraper
+from common.scrapers.bangumi import BangumiScraper
+
+
+def get_scraper_by_url(url):
+    parsed_url = urllib.parse.urlparse(url)
+    hostname = parsed_url.netloc
+    for host in scraper_registry:
+        if host in url:
+            return scraper_registry[host]
+    # TODO move this logic to scraper class
+    try:
+        answers = dns.resolver.query(hostname, 'CNAME')
+        for rdata in answers:
+            if str(rdata.target) == 'dom.bandcamp.com.':
+                return BandcampAlbumScraper
+    except Exception as e:
+        pass
+    try:
+        answers = dns.resolver.query(hostname, 'A')
+        for rdata in answers:
+            if str(rdata.address) == '35.241.62.186':
+                return BandcampAlbumScraper
+    except Exception as e:
+        pass
+    return None
diff --git a/common/scrapers/bandcamp.py b/common/scrapers/bandcamp.py
new file mode 100644
index 00000000..5f939da6
--- /dev/null
+++ b/common/scrapers/bandcamp.py
@@ -0,0 +1,71 @@
+import re
+import dateparser
+import json
+from lxml import html
+from common.models import SourceSiteEnum
+from common.scraper import AbstractScraper
+from music.models import Album
+from music.forms import AlbumForm
+
+
+class BandcampAlbumScraper(AbstractScraper):
+    site_name = SourceSiteEnum.BANDCAMP.value
+    # API URL
+    host = '.bandcamp.com/'
+    data_class = Album
+    form_class = AlbumForm
+
+    regex = re.compile(r"https://[a-zA-Z0-9\-\.]+/album/[^?#]+")
+
+    def scrape(self, url, response=None):
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+        if response is not None:
+            content = html.fromstring(response.content.decode('utf-8'))
+        else:
+            content = self.download_page(url, {})
+        try:
+            title = content.xpath("//h2[@class='trackTitle']/text()")[0].strip()
+            artist = [content.xpath("//div[@id='name-section']/h3/span/a/text()")[0].strip()]
+        except IndexError:
+            raise ValueError("given url contains no valid info")
+
+        genre = []  # TODO: parse tags
+        track_list = []
+        release_nodes = content.xpath("//div[@class='tralbumData tralbum-credits']/text()")
+        release_date = dateparser.parse(re.sub(r'releas\w+ ', '', release_nodes[0].strip())) if release_nodes else None
+        duration = None
+        company = None
+        brief_nodes = content.xpath("//div[@class='tralbumData tralbum-about']/text()")
+        brief = "".join(brief_nodes) if brief_nodes else None
+        cover_url = content.xpath("//div[@id='tralbumArt']/a/@href")[0].strip()
+        bandcamp_page_data = json.loads(content.xpath(
+            "//meta[@name='bc-page-properties']/@content")[0].strip())
+        other_info = {}
+        other_info['bandcamp_album_id'] = bandcamp_page_data['item_id']
+
+        raw_img, ext = self.download_image(cover_url, url)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': genre,
+            'track_list': track_list,
+            'release_date': release_date,
+            'duration': duration,
+            'company': company,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+            'cover_url': cover_url,
+        }
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        url = cls.regex.findall(raw_url)
+        return url[0] if len(url) > 0 else None
diff --git a/common/scrapers/bangumi.py b/common/scrapers/bangumi.py
new file mode 100644
index 00000000..498ba849
--- /dev/null
+++ b/common/scrapers/bangumi.py
@@ -0,0 +1,199 @@
+import re
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from games.models import Game
+from games.forms import GameForm
+from common.scraper import *
+from django.core.exceptions import ObjectDoesNotExist
+
+
+def find_entity(source_url):
+    """
+    for bangumi
+    """
+    # to be added when new scrape method is implemented
+    result = Game.objects.filter(source_url=source_url)
+    if result:
+        return result[0]
+    else:
+        raise ObjectDoesNotExist
+
+
+class BangumiScraper(AbstractScraper):
+    site_name = SourceSiteEnum.BANGUMI.value
+    host = 'bgm.tv'
+
+    # for interface coherence
+    data_class = type("FakeDataClass", (object,), {})()
+    data_class.objects = type("FakeObjectsClass", (object,), {})()
+    data_class.objects.get = find_entity
+    # should be set at scrape_* method
+    form_class = ''
+
+    regex = re.compile(r"https{0,1}://bgm\.tv/subject/\d+")
+
+    def scrape(self, url):
+        """
+        This is the scraping portal
+        """
+        headers = DEFAULT_REQUEST_HEADERS.copy()
+        headers['Host'] = self.host
+        content = self.download_page(url, headers)
+
+        # download image
+        img_url = 'http:' + content.xpath("//div[@class='infobox']//img[1]/@src")[0]
+        raw_img, ext = self.download_image(img_url, url)
+
+        # Test category
+        category_code = content.xpath("//div[@id='headerSearch']//option[@selected]/@value")[0]
+        handler_map = {
+            '1': self.scrape_book,
+            '2': self.scrape_movie,
+            '3': self.scrape_album,
+            '4': self.scrape_game
+        }
+        data = handler_map[category_code](self, content)
+        data['source_url'] = self.get_effective_url(url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    def scrape_game(self, content):
+        self.data_class = Game
+        self.form_class = GameForm
+
+        title_elem = content.xpath("//a[@property='v:itemreviewed']/text()")
+        if not title_elem:
+            raise ValueError("no game info found on this page")
+            title = None
+        else:
+            title = title_elem[0].strip()
+
+        other_title_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/text()")
+        if not other_title_elem:
+            other_title_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'别名')]]/a/text()")
+        other_title = other_title_elem if other_title_elem else []
+
+        chinese_name_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/text()")
+        if not chinese_name_elem:
+            chinese_name_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'中文')]]/a/text()")
+        if chinese_name_elem:
+            chinese_name = chinese_name_elem[0]
+            # switch chinese name with original name
+            title, chinese_name = chinese_name, title
+            # actually the name appended is original
+            other_title.append(chinese_name)
+
+        developer_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/text()")
+        if not developer_elem:
+            developer_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'开发')]]/a/text()")
+        developer = developer_elem if developer_elem else None
+
+        publisher_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/text()")
+        if not publisher_elem:
+            publisher_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'发行:')]]/a/text()")
+        publisher = publisher_elem if publisher_elem else None
+
+        platform_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/text()")
+        if not platform_elem:
+            platform_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'平台')]]/a/text()")
+        platform = platform_elem if platform_elem else None
+
+        genre_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/text()")
+        if not genre_elem:
+            genre_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'类型')]]/a/text()")
+        genre = genre_elem if genre_elem else None
+
+        date_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/text()")
+        if not date_elem:
+            date_elem = content.xpath(
+                "//ul[@id='infobox']/li[child::span[contains(text(),'发行日期')]]/a/text()")
+        release_date = parse_date(date_elem[0]) if date_elem else None
+
+        brief = ''.join(content.xpath("//div[@property='v:summary']/text()"))
+
+        other_info = {}
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'人数')]]/text()")
+        if other_elem:
+            other_info['游玩人数'] = other_elem[0]
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'引擎')]]/text()")
+        if other_elem:
+            other_info['引擎'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'售价')]]/text()")
+        if other_elem:
+            other_info['售价'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'官方网站')]]/text()")
+        if other_elem:
+            other_info['网站'] = other_elem[0]
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'剧本')]]/text()")
+        if other_elem:
+            other_info['剧本'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'编剧')]]/text()")
+        if other_elem:
+            other_info['编剧'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'音乐')]]/text()")
+        if other_elem:
+            other_info['音乐'] = ' '.join(other_elem)
+        other_elem = content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/a/text()") or content.xpath(
+            "//ul[@id='infobox']/li[child::span[contains(text(),'美术')]]/text()")
+        if other_elem:
+            other_info['美术'] = ' '.join(other_elem)
+
+        data = {
+            'title': title,
+            'other_title': None,
+            'developer': developer,
+            'publisher': publisher,
+            'release_date': release_date,
+            'genre': genre,
+            'platform': platform,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+        }
+
+        return data
+
+    def scrape_movie(self, content):
+        self.data_class = Movie
+        self.form_class = MovieForm
+        raise NotImplementedError
+
+    def scrape_book(self, content):
+        self.data_class = Book
+        self.form_class = BookForm
+        raise NotImplementedError
+
+    def scrape_album(self, content):
+        self.data_class = Album
+        self.form_class = AlbumForm
+        raise NotImplementedError
diff --git a/common/scrapers/douban.py b/common/scrapers/douban.py
new file mode 100644
index 00000000..6dcaff69
--- /dev/null
+++ b/common/scrapers/douban.py
@@ -0,0 +1,714 @@
+import requests
+import re
+import filetype
+from lxml import html
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album
+from music.forms import AlbumForm
+from games.models import Game
+from games.forms import GameForm
+from django.core.validators import URLValidator
+from django.conf import settings
+from PIL import Image
+from io import BytesIO
+from common.scraper import *
+
+
+class DoubanScrapperMixin:
+    @classmethod
+    def download_page(cls, url, headers):
+        url = cls.get_effective_url(url)
+        r = None
+        error = 'DoubanScrapper: error occured when downloading ' + url
+        content = None
+        last_error = None
+
+        def get(url):
+            nonlocal r
+            # print('Douban GET ' + url)
+            try:
+                r = requests.get(url, timeout=settings.SCRAPING_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, last_error
+            content = None
+            last_error = 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
+                    last_error = 'network'
+                elif content.find('<title>页面不存在</title>') != -1 or content.find('呃... 你想访问的条目豆瓣不收录。') != -1:  # re.search('不存在[^<]+</title>', content, re.MULTILINE):
+                    content = None
+                    last_error = 'censorship'
+                    error = error + 'Not found or hidden by Douban'
+            elif r.status_code == 204:
+                content = None
+                last_error = 'censorship'
+                error = error + 'Not found or hidden by Douban'
+            else:
+                content = None
+                last_error = 'network'
+                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)
+            if r.status_code == 200:
+                w = r.json()
+                if w['archived_snapshots'] and w['archived_snapshots']['closest']:
+                    get(w['archived_snapshots']['closest']['url'])
+                    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)
+            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)
+                    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 not None:
+                error = error + '\nScrapeStack: '
+                get(f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}')
+            elif settings.SCRAPERAPI_KEY is not None:
+                error = error + '\nScraperAPI: '
+                get(f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}')
+            else:
+                error = error + '\nDirect: '
+                get(url)
+            check_content()
+            if last_error == 'network' and settings.PROXYCRAWL_KEY is not None:
+                error = error + '\nProxyCrawl: '
+                get(f'https://api.proxycrawl.com/?token={settings.PROXYCRAWL_KEY}&url={url}')
+                check_content()
+            if last_error == 'censorship' and settings.LOCAL_PROXY is not None:
+                error = error + '\nLocal: '
+                get(f'{settings.LOCAL_PROXY}?url={url}')
+                check_content()
+
+        latest()
+        if content is None:
+            wayback_cdx()
+
+        if content is None:
+            raise RuntimeError(error)
+        # 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):
+        raw_img = None
+        ext = None
+
+        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}'
+        else:
+            dl_url = url
+
+        try:
+            img_response = requests.get(dl_url, timeout=settings.SCRAPING_TIMEOUT)
+            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.PROXYCRAWL_KEY is not None:
+            try:
+                dl_url = f'https://api.proxycrawl.com/?token={settings.PROXYCRAWL_KEY}&url={url}'
+                img_response = requests.get(dl_url, timeout=settings.SCRAPING_TIMEOUT)
+                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 DoubanBookScraper(DoubanScrapperMixin, 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)
+
+        isbn_elem = content.xpath("//div[@id='info']//span[text()='ISBN:']/following::text()")
+        isbn = isbn_elem[0].strip() if isbn_elem else None
+        title_elem = content.xpath("/html/body//h1/span/text()")
+        title = title_elem[0].strip() if title_elem else None
+        if not title:
+            if isbn:
+                title = 'isbn: ' + isbn
+            else:
+                raise ValueError("given url contains no book title or isbn")
+
+        subtitle_elem = content.xpath(
+            "//div[@id='info']//span[text()='副标题:']/following::text()")
+        subtitle = subtitle_elem[0].strip()[:500] if subtitle_elem else None
+
+        orig_title_elem = content.xpath(
+            "//div[@id='info']//span[text()='原作名:']/following::text()")
+        orig_title = orig_title_elem[0].strip()[:500] if orig_title_elem else None
+
+        language_elem = content.xpath(
+            "//div[@id='info']//span[text()='语言:']/following::text()")
+        language = language_elem[0].strip() if language_elem else None
+
+        pub_house_elem = content.xpath(
+            "//div[@id='info']//span[text()='出版社:']/following::text()")
+        pub_house = pub_house_elem[0].strip() if pub_house_elem else None
+
+        pub_date_elem = content.xpath(
+            "//div[@id='info']//span[text()='出版年:']/following::text()")
+        pub_date = pub_date_elem[0].strip() if pub_date_elem else ''
+        year_month_day = RE_NUMBERS.findall(pub_date)
+        if len(year_month_day) in (2, 3):
+            pub_year = int(year_month_day[0])
+            pub_month = int(year_month_day[1])
+        elif len(year_month_day) == 1:
+            pub_year = int(year_month_day[0])
+            pub_month = None
+        else:
+            pub_year = None
+            pub_month = None
+        if pub_year and pub_month and pub_year < pub_month:
+            pub_year, pub_month = pub_month, pub_year
+        pub_year = None if pub_year is not None and pub_year not in range(
+            0, 3000) else pub_year
+        pub_month = None if pub_month is not None and pub_month not in range(
+            1, 12) else pub_month
+
+        binding_elem = content.xpath(
+            "//div[@id='info']//span[text()='装帧:']/following::text()")
+        binding = binding_elem[0].strip() if binding_elem else None
+
+        price_elem = content.xpath(
+            "//div[@id='info']//span[text()='定价:']/following::text()")
+        price = price_elem[0].strip() if price_elem else None
+
+        pages_elem = content.xpath(
+            "//div[@id='info']//span[text()='页数:']/following::text()")
+        pages = pages_elem[0].strip() if pages_elem else None
+        if pages is not None:
+            pages = int(RE_NUMBERS.findall(pages)[
+                        0]) if RE_NUMBERS.findall(pages) else None
+            if pages and (pages > 999999 or pages < 1):
+                pages = None
+
+        brief_elem = content.xpath(
+            "//h2/span[text()='内容简介']/../following-sibling::div[1]//div[@class='intro'][not(ancestor::span[@class='short'])]/p/text()")
+        brief = '\n'.join(p.strip()
+                          for p in brief_elem) if brief_elem else None
+
+        contents = None
+        try:
+            contents_elem = content.xpath(
+                "//h2/span[text()='目录']/../following-sibling::div[1]")[0]
+            # if next the id of next sibling contains `dir`, that would be the full contents
+            if "dir" in contents_elem.getnext().xpath("@id")[0]:
+                contents_elem = contents_elem.getnext()
+                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
+                    "text()")[:-2]) if contents_elem else None
+            else:
+                contents = '\n'.join(p.strip() for p in contents_elem.xpath(
+                    "text()")) if contents_elem else None
+        except Exception:
+            pass
+
+        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)
+
+        # there are two html formats for authors and translators
+        authors_elem = content.xpath("""//div[@id='info']//span[text()='作者:']/following-sibling::br[1]/
+            preceding-sibling::a[preceding-sibling::span[text()='作者:']]/text()""")
+        if not authors_elem:
+            authors_elem = content.xpath(
+                """//div[@id='info']//span[text()=' 作者']/following-sibling::a/text()""")
+        if authors_elem:
+            authors = []
+            for author in authors_elem:
+                authors.append(RE_WHITESPACES.sub(' ', author.strip())[:200])
+        else:
+            authors = None
+
+        translators_elem = content.xpath("""//div[@id='info']//span[text()='译者:']/following-sibling::br[1]/
+            preceding-sibling::a[preceding-sibling::span[text()='译者:']]/text()""")
+        if not translators_elem:
+            translators_elem = content.xpath(
+                """//div[@id='info']//span[text()=' 译者']/following-sibling::a/text()""")
+        if translators_elem:
+            translators = []
+            for translator in translators_elem:
+                translators.append(RE_WHITESPACES.sub(' ', translator.strip()))
+        else:
+            translators = None
+
+        other = {}
+        cncode_elem = content.xpath(
+            "//div[@id='info']//span[text()='统一书号:']/following::text()")
+        if cncode_elem:
+            other['统一书号'] = cncode_elem[0].strip()
+        series_elem = content.xpath(
+            "//div[@id='info']//span[text()='丛书:']/following-sibling::a[1]/text()")
+        if series_elem:
+            other['丛书'] = series_elem[0].strip()
+        imprint_elem = content.xpath(
+            "//div[@id='info']//span[text()='出品方:']/following-sibling::a[1]/text()")
+        if imprint_elem:
+            other['出品方'] = imprint_elem[0].strip()
+
+        data = {
+            'title': title,
+            'subtitle': subtitle,
+            'orig_title': orig_title,
+            'author': authors,
+            'translator': translators,
+            'language': language,
+            'pub_house': pub_house,
+            'pub_year': pub_year,
+            'pub_month': pub_month,
+            'binding': binding,
+            'price': price,
+            'pages': pages,
+            'isbn': isbn,
+            'brief': brief,
+            'contents': contents,
+            'other_info': other,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+
+class DoubanMovieScraper(DoubanScrapperMixin, 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)
+
+        # parsing starts here
+        try:
+            raw_title = content.xpath(
+                "//span[@property='v:itemreviewed']/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no movie info")
+
+        orig_title = content.xpath(
+            "//img[@rel='v:image']/@alt")[0].strip()
+        title = raw_title.split(orig_title)[0].strip()
+        # if has no chinese title
+        if title == '':
+            title = orig_title
+
+        if title == orig_title:
+            orig_title = None
+
+        # there are two html formats for authors and translators
+        other_title_elem = content.xpath(
+            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
+        other_title = other_title_elem[0].strip().split(
+            ' / ') if other_title_elem else None
+
+        imdb_elem = content.xpath(
+            "//div[@id='info']//span[text()='IMDb链接:']/following-sibling::a[1]/text()")
+        if not imdb_elem:
+            imdb_elem = content.xpath(
+                "//div[@id='info']//span[text()='IMDb:']/following-sibling::text()[1]")
+        imdb_code = imdb_elem[0].strip() if imdb_elem else None
+
+        director_elem = content.xpath(
+            "//div[@id='info']//span[text()='导演']/following-sibling::span[1]/a/text()")
+        director = director_elem if director_elem else None
+
+        playwright_elem = content.xpath(
+            "//div[@id='info']//span[text()='编剧']/following-sibling::span[1]/a/text()")
+        playwright = list(map(lambda a: a[:200], playwright_elem)) if playwright_elem else None
+
+        actor_elem = content.xpath(
+            "//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()")
+        actor = list(map(lambda a: a[:200], actor_elem)) if actor_elem else None
+
+        # construct genre translator
+        genre_translator = {}
+        attrs = [attr for attr in dir(MovieGenreEnum) if '__' not in attr]
+        for attr in attrs:
+            genre_translator[getattr(MovieGenreEnum, attr).label] = getattr(
+                MovieGenreEnum, attr).value
+
+        genre_elem = content.xpath("//span[@property='v:genre']/text()")
+        if genre_elem:
+            genre = []
+            for g in genre_elem:
+                g = g.split(' ')[0]
+                if g == '紀錄片':  # likely some original data on douban was corrupted
+                    g = '纪录片'
+                elif g == '鬼怪':
+                    g = '惊悚'
+                if g in genre_translator:
+                    genre.append(genre_translator[g])
+                elif g in genre_translator.values():
+                    genre.append(g)
+                else:
+                    logger.error(f'unable to map genre {g}')
+        else:
+            genre = None
+
+        showtime_elem = content.xpath(
+            "//span[@property='v:initialReleaseDate']/text()")
+        if showtime_elem:
+            showtime = []
+            for st in showtime_elem:
+                parts = st.split('(')
+                if len(parts) == 1:
+                    time = st.split('(')[0]
+                    region = ''
+                else:
+                    time = st.split('(')[0]
+                    region = st.split('(')[1][0:-1]
+                showtime.append({time: region})
+        else:
+            showtime = None
+
+        site_elem = content.xpath(
+            "//div[@id='info']//span[text()='官方网站:']/following-sibling::a[1]/@href")
+        site = site_elem[0].strip()[:200] if site_elem else None
+        try:
+            validator = URLValidator()
+            validator(site)
+        except ValidationError:
+            site = None
+
+        area_elem = content.xpath(
+            "//div[@id='info']//span[text()='制片国家/地区:']/following-sibling::text()[1]")
+        if area_elem:
+            area = [a.strip()[:100] for a in area_elem[0].split('/')]
+        else:
+            area = None
+
+        language_elem = content.xpath(
+            "//div[@id='info']//span[text()='语言:']/following-sibling::text()[1]")
+        if language_elem:
+            language = [a.strip() for a in language_elem[0].split(' / ')]
+        else:
+            language = None
+
+        year_elem = content.xpath("//span[@class='year']/text()")
+        year = int(re.search(r'\d+', year_elem[0])[0]) if year_elem and re.search(r'\d+', year_elem[0]) else None
+
+        duration_elem = content.xpath("//span[@property='v:runtime']/text()")
+        other_duration_elem = content.xpath(
+            "//span[@property='v:runtime']/following-sibling::text()[1]")
+        if duration_elem:
+            duration = duration_elem[0].strip()
+            if other_duration_elem:
+                duration += other_duration_elem[0].rstrip()
+            duration = duration.split('/')[0].strip()
+        else:
+            duration = None
+
+        season_elem = content.xpath(
+            "//*[@id='season']/option[@selected='selected']/text()")
+        if not season_elem:
+            season_elem = content.xpath(
+                "//div[@id='info']//span[text()='季数:']/following-sibling::text()[1]")
+            season = int(season_elem[0].strip()) if season_elem else None
+        else:
+            season = int(season_elem[0].strip())
+
+        episodes_elem = content.xpath(
+            "//div[@id='info']//span[text()='集数:']/following-sibling::text()[1]")
+        episodes = int(episodes_elem[0].strip()) if episodes_elem and episodes_elem[0].isdigit() else None
+
+        single_episode_length_elem = content.xpath(
+            "//div[@id='info']//span[text()='单集片长:']/following-sibling::text()[1]")
+        single_episode_length = single_episode_length_elem[0].strip(
+        )[:100] if single_episode_length_elem else None
+
+        # if has field `episodes` not none then must be series
+        is_series = True if episodes else False
+
+        brief_elem = content.xpath("//span[@class='all hidden']")
+        if not brief_elem:
+            brief_elem = content.xpath("//span[@property='v:summary']")
+        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
+            './text()')]) if brief_elem else None
+
+        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)
+
+        data = {
+            'title': title,
+            'orig_title': orig_title,
+            'other_title': other_title,
+            'imdb_code': imdb_code,
+            'director': director,
+            'playwright': playwright,
+            'actor': actor,
+            'genre': genre,
+            'showtime': showtime,
+            'site': site,
+            'area': area,
+            'language': language,
+            'year': year,
+            'duration': duration,
+            'season': season,
+            'episodes': episodes,
+            'single_episode_length': single_episode_length,
+            'brief': brief,
+            'is_series': is_series,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+
+class DoubanAlbumScraper(DoubanScrapperMixin, 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)
+
+        # parsing starts here
+        try:
+            title = content.xpath("//h1/span/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no album info")
+        if not title:
+            raise ValueError("given url contains no album info")
+
+        artists_elem = content.xpath("//div[@id='info']/span/span[@class='pl']/a/text()")
+        artist = None if not artists_elem else list(map(lambda a: a[:200], artists_elem))
+
+        genre_elem = content.xpath(
+            "//div[@id='info']//span[text()='流派:']/following::text()[1]")
+        genre = genre_elem[0].strip() if genre_elem else None
+
+        date_elem = content.xpath(
+            "//div[@id='info']//span[text()='发行时间:']/following::text()[1]")
+        release_date = parse_date(date_elem[0].strip()) if date_elem else None
+
+        company_elem = content.xpath(
+            "//div[@id='info']//span[text()='出版者:']/following::text()[1]")
+        company = company_elem[0].strip() if company_elem else None
+
+        track_list_elem = content.xpath(
+            "//div[@class='track-list']/div[@class='indent']/div/text()"
+        )
+        if track_list_elem:
+            track_list = '\n'.join([track.strip() for track in track_list_elem])
+        else:
+            track_list = None
+
+        brief_elem = content.xpath("//span[@class='all hidden']")
+        if not brief_elem:
+            brief_elem = content.xpath("//span[@property='v:summary']")
+        brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
+            './text()')]) if brief_elem else None
+
+        other_info = {}
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['又名'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['专辑类型'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['介质'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['ISRC'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['条形码'] = other_elem[0].strip()
+        other_elem = content.xpath(
+            "//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
+        if other_elem:
+            other_info['碟片数'] = other_elem[0].strip()
+
+        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)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': genre,
+            'release_date': release_date,
+            'duration': None,
+            'company': company,
+            'track_list': track_list,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+
+class DoubanGameScraper(DoubanScrapperMixin, AbstractScraper):
+    site_name = SourceSiteEnum.DOUBAN.value
+    host = 'www.douban.com/game/'
+    data_class = Game
+    form_class = GameForm
+
+    regex = re.compile(r"https://www\.douban\.com/game/\d+/{0,1}")
+
+    def scrape(self, url):
+        headers = DEFAULT_REQUEST_HEADERS.copy()
+        headers['Host'] = 'www.douban.com'
+        content = self.download_page(url, headers)
+
+        try:
+            raw_title = content.xpath(
+                "//div[@id='content']/h1/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no game info")
+
+        title = raw_title
+
+        other_title_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='别名:']/following-sibling::dd[1]/text()")
+        other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None
+
+        developer_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='开发商:']/following-sibling::dd[1]/text()")
+        developer = developer_elem[0].strip().split(' / ') if developer_elem else None
+
+        publisher_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='发行商:']/following-sibling::dd[1]/text()")
+        publisher = publisher_elem[0].strip().split(' / ') if publisher_elem else None
+
+        platform_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='平台:']/following-sibling::dd[1]/a/text()")
+        platform = platform_elem if platform_elem else None
+
+        genre_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='类型:']/following-sibling::dd[1]/a/text()")
+        genre = None
+        if genre_elem:
+            genre = [g for g in genre_elem if g != '游戏']
+
+        date_elem = content.xpath(
+            "//dl[@class='game-attr']//dt[text()='发行日期:']/following-sibling::dd[1]/text()")
+        release_date = parse_date(date_elem[0].strip()) if date_elem else None
+
+        brief_elem = content.xpath("//div[@class='mod item-desc']/p/text()")
+        brief = '\n'.join(brief_elem) if brief_elem else None
+
+        img_url_elem = content.xpath(
+            "//div[@class='item-subject-info']/div[@class='pic']//img/@src")
+        img_url = img_url_elem[0].strip() if img_url_elem else None
+        raw_img, ext = self.download_image(img_url, url)
+
+        data = {
+            'title': title,
+            'other_title': other_title,
+            'developer': developer,
+            'publisher': publisher,
+            'release_date': release_date,
+            'genre': genre,
+            'platform': platform,
+            'brief': brief,
+            'other_info': None,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
diff --git a/common/scrapers/goodreads.py b/common/scrapers/goodreads.py
new file mode 100644
index 00000000..813fd063
--- /dev/null
+++ b/common/scrapers/goodreads.py
@@ -0,0 +1,157 @@
+import requests
+import re
+import filetype
+from lxml import html
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from games.models import Game
+from games.forms import GameForm
+from django.conf import settings
+from PIL import Image
+from io import BytesIO
+from common.scraper import *
+
+
+class GoodreadsScraper(AbstractScraper):
+    site_name = SourceSiteEnum.GOODREADS.value
+    host = "www.goodreads.com"
+    data_class = Book
+    form_class = BookForm
+    regex = re.compile(r"https://www\.goodreads\.com/book/show/\d+")
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        u = re.match(r".+/book/show/(\d+)", raw_url)
+        if not u:
+            u = re.match(r".+book/(\d+)", raw_url)
+        return "https://www.goodreads.com/book/show/" + u[1] if u else None
+
+    def scrape(self, url, response=None):
+        """
+        This is the scraping portal
+        """
+        if response is not None:
+            content = html.fromstring(response.content.decode('utf-8'))
+        else:
+            headers = None  # DEFAULT_REQUEST_HEADERS.copy()
+            content = self.download_page(url, headers)
+
+        try:
+            title = content.xpath("//h1[@id='bookTitle']/text()")[0].strip()
+        except IndexError:
+            raise ValueError("given url contains no book info")
+
+        subtitle = None
+
+        orig_title_elem = content.xpath("//div[@id='bookDataBox']//div[text()='Original Title']/following-sibling::div/text()")
+        orig_title = orig_title_elem[0].strip() if orig_title_elem else None
+
+        language_elem = content.xpath('//div[@itemprop="inLanguage"]/text()')
+        language = language_elem[0].strip() if language_elem else None
+
+        pub_house_elem = content.xpath("//div[contains(text(), 'Published') and @class='row']/text()")
+        try:
+            months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
+            r = re.compile('.*Published.*(' + '|'.join(months) + ').*(\\d\\d\\d\\d).+by\\s*(.+)\\s*', re.DOTALL)
+            pub = r.match(pub_house_elem[0])
+            pub_year = pub[2]
+            pub_month = months.index(pub[1]) + 1
+            pub_house = pub[3].strip()
+        except Exception:
+            pub_year = None
+            pub_month = None
+            pub_house = None
+
+        pub_house_elem = content.xpath("//nobr[contains(text(), 'first published')]/text()")
+        try:
+            pub = re.match(r'.*first published\s+(.+\d\d\d\d).*', pub_house_elem[0], re.DOTALL)
+            first_pub = pub[1]
+        except Exception:
+            first_pub = None
+
+        binding_elem = content.xpath('//span[@itemprop="bookFormat"]/text()')
+        binding = binding_elem[0].strip() if binding_elem else None
+
+        pages_elem = content.xpath('//span[@itemprop="numberOfPages"]/text()')
+        pages = pages_elem[0].strip() if pages_elem else None
+        if pages is not None:
+            pages = int(RE_NUMBERS.findall(pages)[
+                        0]) if RE_NUMBERS.findall(pages) else None
+
+        isbn_elem = content.xpath('//span[@itemprop="isbn"]/text()')
+        if not isbn_elem:
+            isbn_elem = content.xpath('//div[@itemprop="isbn"]/text()')  # this is likely ASIN
+        isbn = isbn_elem[0].strip() if isbn_elem else None
+
+        brief_elem = content.xpath('//div[@id="description"]/span[@style="display:none"]/text()')
+        if brief_elem:
+            brief = '\n'.join(p.strip() for p in brief_elem)
+        else:
+            brief_elem = content.xpath('//div[@id="description"]/span/text()')
+            brief = '\n'.join(p.strip() for p in brief_elem) if brief_elem else None
+
+        genre = content.xpath('//div[@class="bigBoxBody"]/div/div/div/a/text()')
+        genre = genre[0] if genre else None
+        book_title = re.sub('\n', '', content.xpath('//h1[@id="bookTitle"]/text()')[0]).strip()
+        author = content.xpath('//a[@class="authorName"]/span/text()')[0]
+        contents = None
+
+        img_url_elem = content.xpath("//img[@id='coverImage']/@src")
+        img_url = img_url_elem[0].strip() if img_url_elem else None
+        raw_img, ext = self.download_image(img_url, url)
+
+        authors_elem = content.xpath("//a[@class='authorName'][not(../span[@class='authorName greyText smallText role'])]/span/text()")
+        if authors_elem:
+            authors = []
+            for author in authors_elem:
+                authors.append(RE_WHITESPACES.sub(' ', author.strip()))
+        else:
+            authors = None
+
+        translators = None
+        authors_elem = content.xpath("//a[@class='authorName'][../span/text()='(Translator)']/span/text()")
+        if authors_elem:
+            translators = []
+            for translator in authors_elem:
+                translators.append(RE_WHITESPACES.sub(' ', translator.strip()))
+        else:
+            translators = None
+
+        other = {}
+        if first_pub:
+            other['首版时间'] = first_pub
+        if genre:
+            other['分类'] = genre
+        series_elem = content.xpath("//h2[@id='bookSeries']/a/text()")
+        if series_elem:
+            other['丛书'] = re.sub(r'\(\s*(.+[^\s])\s*#.*\)', '\\1', series_elem[0].strip())
+
+        data = {
+            'title': title,
+            'subtitle': subtitle,
+            'orig_title': orig_title,
+            'author': authors,
+            'translator': translators,
+            'language': language,
+            'pub_house': pub_house,
+            'pub_year': pub_year,
+            'pub_month': pub_month,
+            'binding': binding,
+            'pages': pages,
+            'isbn': isbn,
+            'brief': brief,
+            'contents': contents,
+            'other_info': other,
+            'cover_url': img_url,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        data['source_url'] = self.get_effective_url(url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
diff --git a/common/scrapers/google.py b/common/scrapers/google.py
new file mode 100644
index 00000000..0082fb3e
--- /dev/null
+++ b/common/scrapers/google.py
@@ -0,0 +1,102 @@
+import requests
+import re
+import filetype
+from lxml import html
+from common.models import SourceSiteEnum
+from movies.models import Movie, MovieGenreEnum
+from movies.forms import MovieForm
+from books.models import Book
+from books.forms import BookForm
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from games.models import Game
+from games.forms import GameForm
+from django.conf import settings
+from PIL import Image
+from io import BytesIO
+from common.scraper import *
+
+
+# https://developers.google.com/youtube/v3/docs/?apix=true
+# https://developers.google.com/books/docs/v1/using
+class GoogleBooksScraper(AbstractScraper):
+    site_name = SourceSiteEnum.GOOGLEBOOKS.value
+    host = ["books.google.com", "www.google.com/books"]
+    data_class = Book
+    form_class = BookForm
+    regex = re.compile(r"https://books\.google\.com/books\?id=([^&#]+)")
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        # https://books.google.com/books?id=wUHxzgEACAAJ
+        # https://books.google.com/books/about/%E7%8F%BE%E5%A0%B4%E6%AD%B7%E5%8F%B2.html?id=nvNoAAAAIAAJ
+        # https://www.google.com/books/edition/_/nvNoAAAAIAAJ?hl=en&gbpv=1
+        u = re.match(r"https://books\.google\.com/books.*id=([^&#]+)", raw_url)
+        if not u:
+            u = re.match(r"https://www\.google\.com/books/edition/[^/]+/([^&#?]+)", raw_url)
+        return 'https://books.google.com/books?id=' + u[1] if u else None
+
+    def scrape(self, url, response=None):
+        url = self.get_effective_url(url)
+        m = self.regex.match(url)
+        if m:
+            api_url = f'https://www.googleapis.com/books/v1/volumes/{m[1]}'
+        else:
+            raise ValueError("not valid url")
+        b = requests.get(api_url).json()
+        other = {}
+        title = b['volumeInfo']['title']
+        subtitle = b['volumeInfo']['subtitle'] if 'subtitle' in b['volumeInfo'] else None
+        pub_year = None
+        pub_month = None
+        if 'publishedDate' in b['volumeInfo']:
+            pub_date = b['volumeInfo']['publishedDate'].split('-')
+            pub_year = pub_date[0]
+            pub_month = pub_date[1] if len(pub_date) > 1 else None
+        pub_house = b['volumeInfo']['publisher'] if 'publisher' in b['volumeInfo'] else None
+        language = b['volumeInfo']['language'] if 'language' in b['volumeInfo'] else None
+        pages = b['volumeInfo']['pageCount'] if 'pageCount' in b['volumeInfo'] else None
+        if 'mainCategory' in b['volumeInfo']:
+            other['分类'] = b['volumeInfo']['mainCategory']
+        authors = b['volumeInfo']['authors'] if 'authors' in b['volumeInfo'] else None
+        if 'description' in b['volumeInfo']:
+            brief = b['volumeInfo']['description']
+        elif 'textSnippet' in b['volumeInfo']:
+            brief = b["volumeInfo"]["textSnippet"]["searchInfo"]
+        else:
+            brief = ''
+        brief = re.sub(r'<.*?>', '', brief.replace('<br', '\n<br'))
+        img_url = b['volumeInfo']['imageLinks']['thumbnail'] if 'imageLinks' in b['volumeInfo'] else None
+        isbn10 = None
+        isbn13 = None
+        for iid in b['volumeInfo']['industryIdentifiers'] if 'industryIdentifiers' in b['volumeInfo'] else []:
+            if iid['type'] == 'ISBN_10':
+                isbn10 = iid['identifier']
+            if iid['type'] == 'ISBN_13':
+                isbn13 = iid['identifier']
+        isbn = isbn13 if isbn13 is not None else isbn10
+
+        data = {
+            'title': title,
+            'subtitle': subtitle,
+            'orig_title': None,
+            'author': authors,
+            'translator': None,
+            'language': language,
+            'pub_house': pub_house,
+            'pub_year': pub_year,
+            'pub_month': pub_month,
+            'binding': None,
+            'pages': pages,
+            'isbn': isbn,
+            'brief': brief,
+            'contents': None,
+            'other_info': other,
+            'cover_url': img_url,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        raw_img, ext = self.download_image(img_url, url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
diff --git a/common/scrapers/igdb.py b/common/scrapers/igdb.py
new file mode 100644
index 00000000..eb635f5c
--- /dev/null
+++ b/common/scrapers/igdb.py
@@ -0,0 +1,88 @@
+import requests
+import re
+from common.models import SourceSiteEnum
+from games.models import Game
+from games.forms import GameForm
+from django.conf import settings
+from common.scraper import *
+from igdb.wrapper import IGDBWrapper
+import json
+import datetime
+
+
+wrapper = IGDBWrapper(settings.IGDB_CLIENT_ID, settings.IGDB_ACCESS_TOKEN)
+
+
+class IgdbGameScraper(AbstractScraper):
+    site_name = SourceSiteEnum.IGDB.value
+    host = 'https://www.igdb.com/'
+    data_class = Game
+    form_class = GameForm
+    regex = re.compile(r"https://www\.igdb\.com/games/([a-zA-Z0-9\-_]+)")
+
+    def scrape_steam(self, steam_url):
+        r = json.loads(wrapper.api_request('websites', f'fields *, game.*; where url = "{steam_url}";'))
+        if not r:
+            raise ValueError("Cannot find steam url in IGDB")
+        r = sorted(r, key=lambda w: w['game']['id'])
+        return self.scrape(r[0]['game']['url'])
+
+    def scrape(self, url):
+        m = self.regex.match(url)
+        if m:
+            effective_url = m[0]
+        else:
+            raise ValueError("not valid url")
+        effective_url = m[0]
+        slug = m[1]
+        fields = '*, cover.url, genres.name, platforms.name, involved_companies.*, involved_companies.company.name'
+        r = json.loads(wrapper.api_request('games', f'fields {fields}; where url = "{effective_url}";'))[0]
+        brief = r['summary'] if 'summary' in r else ''
+        brief += "\n\n" + r['storyline'] if 'storyline' in r else ''
+        developer = None
+        publisher = None
+        release_date = None
+        genre = None
+        platform = None
+        if 'involved_companies' in r:
+            developer = next(iter([c['company']['name'] for c in r['involved_companies'] if c['developer'] == True]), None)
+            publisher = next(iter([c['company']['name'] for c in r['involved_companies'] if c['publisher'] == True]), None)
+        if 'platforms' in r:
+            ps = sorted(r['platforms'], key=lambda p: p['id'])
+            platform = [(p['name'] if p['id'] != 6 else 'Windows') for p in ps]
+        if 'first_release_date' in r:
+            release_date = datetime.datetime.fromtimestamp(r['first_release_date'], datetime.timezone.utc)
+        if 'genres' in r:
+            genre = [g['name'] for g in r['genres']]
+        other_info = {'igdb_id': r['id']}
+        websites = json.loads(wrapper.api_request('websites', f'fields *; where game.url = "{effective_url}";'))
+        for website in websites:
+            if website['category'] == 1:
+                other_info['official_site'] = website['url']
+            elif website['category'] == 13:
+                other_info['steam_url'] = website['url']
+        data = {
+            'title': r['name'],
+            'other_title': None,
+            'developer': developer,
+            'publisher': publisher,
+            'release_date': release_date,
+            'genre': genre,
+            'platform': platform,
+            'brief': brief,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': self.get_effective_url(url),
+        }
+        raw_img, ext = self.download_image('https:' + r['cover']['url'].replace('t_thumb', 't_cover_big'), url)
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        m = cls.regex.match(raw_url)
+        if m:
+            return m[0]
+        else:
+            return None
diff --git a/common/scrapers/imdb.py b/common/scrapers/imdb.py
new file mode 100644
index 00000000..97c040bb
--- /dev/null
+++ b/common/scrapers/imdb.py
@@ -0,0 +1,116 @@
+import requests
+import re
+from common.models import SourceSiteEnum
+from movies.forms import MovieForm
+from movies.models import Movie
+from django.conf import settings
+from common.scraper import *
+
+
+class ImdbMovieScraper(AbstractScraper):
+    site_name = SourceSiteEnum.IMDB.value
+    host = 'https://www.imdb.com/title/'
+    data_class = Movie
+    form_class = MovieForm
+
+    regex = re.compile(r"(?<=https://www\.imdb\.com/title/)[a-zA-Z0-9]+")
+
+    def scrape(self, url):
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+        code = self.regex.findall(effective_url)[0]
+        s = TmdbMovieScraper()
+        s.scrape_imdb(code)
+        self.raw_data = s.raw_data
+        self.raw_img = s.raw_img
+        self.img_ext = s.img_ext
+        self.raw_data['source_site'] = self.site_name
+        self.raw_data['source_url'] = effective_url
+        return self.raw_data, self.raw_img
+
+        api_url = self.get_api_url(effective_url)
+        r = requests.get(api_url)
+        res_data = r.json()
+
+        if not res_data['type'] in ['Movie', 'TVSeries']:
+            raise ValueError("not movie/series item")
+
+        if res_data['type'] == 'Movie':
+            is_series = False
+        elif res_data['type'] == 'TVSeries':
+            is_series = True
+
+        title = res_data['title']
+        orig_title = res_data['originalTitle']
+        imdb_code = self.regex.findall(effective_url)[0]
+        director = []
+        for direct_dict in res_data['directorList']:
+            director.append(direct_dict['name'])
+        playwright = []
+        for writer_dict in res_data['writerList']:
+            playwright.append(writer_dict['name'])
+        actor = []
+        for actor_dict in res_data['actorList']:
+            actor.append(actor_dict['name'])
+        genre = res_data['genres'].split(', ')
+        area = res_data['countries'].split(', ')
+        language = res_data['languages'].split(', ')
+        year = int(res_data['year'])
+        duration = res_data['runtimeStr']
+        brief = res_data['plotLocal'] if res_data['plotLocal'] else res_data['plot']
+        if res_data['releaseDate']:
+            showtime = [{res_data['releaseDate']: "发布日期"}]
+        else:
+            showtime = None
+
+        other_info = {}
+        if res_data['contentRating']:
+            other_info['分级'] = res_data['contentRating']
+        if res_data['imDbRating']:
+            other_info['IMDb评分'] = res_data['imDbRating']
+        if res_data['metacriticRating']:
+            other_info['Metacritic评分'] = res_data['metacriticRating']
+        if res_data['awards']:
+            other_info['奖项'] = res_data['awards']
+
+        raw_img, ext = self.download_image(res_data['image'], url)
+
+        data = {
+            'title': title,
+            'orig_title': orig_title,
+            'other_title': None,
+            'imdb_code': imdb_code,
+            'director': director,
+            'playwright': playwright,
+            'actor': actor,
+            'genre': genre,
+            'showtime': showtime,
+            'site': None,
+            'area': area,
+            'language': language,
+            'year': year,
+            'duration': duration,
+            'season': None,
+            'episodes': None,
+            'single_episode_length': None,
+            'brief': brief,
+            'is_series': is_series,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        code = cls.regex.findall(raw_url)
+        if code:
+            return f"https://www.imdb.com/title/{code[0]}/"
+        else:
+            return None
+
+    @classmethod
+    def get_api_url(cls, url):
+        return f"https://imdb-api.com/zh/API/Title/{settings.IMDB_API_KEY}/{cls.regex.findall(url)[0]}/FullActor,"
diff --git a/common/scrapers/spotify.py b/common/scrapers/spotify.py
new file mode 100644
index 00000000..da891d69
--- /dev/null
+++ b/common/scrapers/spotify.py
@@ -0,0 +1,287 @@
+import requests
+import re
+import time
+from common.models import SourceSiteEnum
+from music.models import Album, Song
+from music.forms import AlbumForm, SongForm
+from django.conf import settings
+from common.scraper import *
+from threading import Thread
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils import timezone
+
+
+spotify_token = None
+spotify_token_expire_time = time.time()
+
+
+class SpotifyTrackScraper(AbstractScraper):
+    site_name = SourceSiteEnum.SPOTIFY.value
+    host = 'https://open.spotify.com/track/'
+    data_class = Song
+    form_class = SongForm
+
+    regex = re.compile(r"(?<=https://open\.spotify\.com/track/)[a-zA-Z0-9]+")
+
+    def scrape(self, url):
+        """
+        Request from API, not really scraping
+        """
+        global spotify_token, spotify_token_expire_time
+
+        if spotify_token is None or is_spotify_token_expired():
+            invoke_spotify_token()
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+
+        api_url = self.get_api_url(effective_url)
+        headers = {
+            'Authorization': f"Bearer {spotify_token}"
+        }
+        r = requests.get(api_url, headers=headers)
+        res_data = r.json()
+
+        artist = []
+        for artist_dict in res_data['artists']:
+            artist.append(artist_dict['name'])
+        if not artist:
+            artist = None
+
+        title = res_data['name']
+
+        release_date = parse_date(res_data['album']['release_date'])
+
+        duration = res_data['duration_ms']
+
+        if res_data['external_ids'].get('isrc'):
+            isrc = res_data['external_ids']['isrc']
+        else:
+            isrc = None
+
+        raw_img, ext = self.download_image(res_data['album']['images'][0]['url'], url)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': None,
+            'release_date': release_date,
+            'duration': duration,
+            'isrc': isrc,
+            'album': None,
+            'brief': None,
+            'other_info': None,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        code = cls.regex.findall(raw_url)
+        if code:
+            return f"https://open.spotify.com/track/{code[0]}"
+        else:
+            return None
+
+    @classmethod
+    def get_api_url(cls, url):
+        return "https://api.spotify.com/v1/tracks/" + cls.regex.findall(url)[0]
+
+
+class SpotifyAlbumScraper(AbstractScraper):
+    site_name = SourceSiteEnum.SPOTIFY.value
+    # API URL
+    host = 'https://open.spotify.com/album/'
+    data_class = Album
+    form_class = AlbumForm
+
+    regex = re.compile(r"(?<=https://open\.spotify\.com/album/)[a-zA-Z0-9]+")
+
+    def scrape(self, url):
+        """
+        Request from API, not really scraping
+        """
+        global spotify_token, spotify_token_expire_time
+
+        if spotify_token is None or is_spotify_token_expired():
+            invoke_spotify_token()
+        effective_url = self.get_effective_url(url)
+        if effective_url is None:
+            raise ValueError("not valid url")
+
+        api_url = self.get_api_url(effective_url)
+        headers = {
+            'Authorization': f"Bearer {spotify_token}"
+        }
+        r = requests.get(api_url, headers=headers)
+        res_data = r.json()
+
+        artist = []
+        for artist_dict in res_data['artists']:
+            artist.append(artist_dict['name'])
+
+        title = res_data['name']
+
+        genre = ', '.join(res_data['genres'])
+
+        company = []
+        for com in res_data['copyrights']:
+            company.append(com['text'])
+
+        duration = 0
+        track_list = []
+        track_urls = []
+        for track in res_data['tracks']['items']:
+            track_urls.append(track['external_urls']['spotify'])
+            duration += track['duration_ms']
+            if res_data['tracks']['items'][-1]['disc_number'] > 1:
+                # more than one disc
+                track_list.append(str(
+                    track['disc_number']) + '-' + str(track['track_number']) + '. ' + track['name'])
+            else:
+                track_list.append(str(track['track_number']) + '. ' + track['name'])
+        track_list = '\n'.join(track_list)
+
+        release_date = parse_date(res_data['release_date'])
+
+        other_info = {}
+        if res_data['external_ids'].get('upc'):
+            # bar code
+            other_info['UPC'] = res_data['external_ids']['upc']
+
+        raw_img, ext = self.download_image(res_data['images'][0]['url'], url)
+
+        data = {
+            'title': title,
+            'artist': artist,
+            'genre': genre,
+            'track_list': track_list,
+            'release_date': release_date,
+            'duration': duration,
+            'company': company,
+            'brief': None,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+
+        # set tracks_data, used for adding tracks
+        self.track_urls = track_urls
+
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        code = cls.regex.findall(raw_url)
+        if code:
+            return f"https://open.spotify.com/album/{code[0]}"
+        else:
+            return None
+
+    # @classmethod
+    # def save(cls, request_user):
+    #     form = super().save(request_user)
+    #     task = Thread(
+    #         target=cls.add_tracks,
+    #         args=(form.instance, request_user),
+    #         daemon=True
+    #     )
+    #     task.start()
+    #     return form
+
+    @classmethod
+    def get_api_url(cls, url):
+        return "https://api.spotify.com/v1/albums/" + cls.regex.findall(url)[0]
+
+    @classmethod
+    def add_tracks(cls, album: Album, request_user):
+        to_be_updated_tracks = []
+        for track_url in cls.track_urls:
+            track = cls.get_track_or_none(track_url)
+            # seems lik if fire too many requests at the same time
+            # spotify would limit access
+            if track is None:
+                task = Thread(
+                    target=cls.scrape_and_save_track,
+                    args=(track_url, album, request_user),
+                    daemon=True
+                )
+                task.start()
+                task.join()
+            else:
+                to_be_updated_tracks.append(track)
+        cls.bulk_update_track_album(to_be_updated_tracks, album, request_user)
+
+    @classmethod
+    def get_track_or_none(cls, track_url: str):
+        try:
+            instance = Song.objects.get(source_url=track_url)
+            return instance
+        except ObjectDoesNotExist:
+            return None
+
+    @classmethod
+    def scrape_and_save_track(cls, url: str, album: Album, request_user):
+        data, img = SpotifyTrackScraper.scrape(url)
+        SpotifyTrackScraper.raw_data['album'] = album
+        SpotifyTrackScraper.save(request_user)
+
+    @classmethod
+    def bulk_update_track_album(cls, tracks, album, request_user):
+        for track in tracks:
+            track.last_editor = request_user
+            track.edited_time = timezone.now()
+            track.album = album
+        Song.objects.bulk_update(tracks, [
+            'last_editor',
+            'edited_time',
+            'album'
+        ])
+
+
+def get_spotify_token():
+    global spotify_token, spotify_token_expire_time
+    if spotify_token is None or is_spotify_token_expired():
+        invoke_spotify_token()
+    return spotify_token
+
+    
+def is_spotify_token_expired():
+    global spotify_token_expire_time
+    return True if spotify_token_expire_time <= time.time() else False
+
+
+def invoke_spotify_token():
+    global spotify_token, spotify_token_expire_time
+    r = requests.post(
+        "https://accounts.spotify.com/api/token",
+        data={
+            "grant_type": "client_credentials"
+        },
+        headers={
+            "Authorization": f"Basic {settings.SPOTIFY_CREDENTIAL}"
+        }
+    )
+    data = r.json()
+    if r.status_code == 401:
+        # token expired, try one more time
+        # this maybe caused by external operations,
+        # for example debugging using a http client
+        r = requests.post(
+            "https://accounts.spotify.com/api/token",
+            data={
+                "grant_type": "client_credentials"
+            },
+            headers={
+                "Authorization": f"Basic {settings.SPOTIFY_CREDENTIAL}"
+            }
+        )
+        data = r.json()
+    elif r.status_code != 200:
+        raise Exception(f"Request to spotify API fails. Reason: {r.reason}")
+    # minus 2 for execution time error
+    spotify_token_expire_time = int(data['expires_in']) + time.time() - 2
+    spotify_token = data['access_token']
diff --git a/common/scrapers/steam.py b/common/scrapers/steam.py
new file mode 100644
index 00000000..43f1c76b
--- /dev/null
+++ b/common/scrapers/steam.py
@@ -0,0 +1,92 @@
+import re
+from common.models import SourceSiteEnum
+from games.models import Game
+from games.forms import GameForm
+from common.scraper import *
+from common.scrapers.igdb import IgdbGameScraper
+
+
+class SteamGameScraper(AbstractScraper):
+    site_name = SourceSiteEnum.STEAM.value
+    host = 'store.steampowered.com'
+    data_class = Game
+    form_class = GameForm
+
+    regex = re.compile(r"https://store\.steampowered\.com/app/\d+")
+
+    def scrape(self, url):
+        m = self.regex.match(url)
+        if m:
+            effective_url = m[0]
+        else:
+            raise ValueError("not valid url")
+        try:
+            s = IgdbGameScraper()
+            s.scrape_steam(effective_url)
+            self.raw_data = s.raw_data
+            self.raw_img = s.raw_img
+            self.img_ext = s.img_ext
+            self.raw_data['source_site'] = self.site_name
+            self.raw_data['source_url'] = effective_url
+            # return self.raw_data, self.raw_img
+        except:
+            self.raw_img = None
+            self.raw_data = {}
+        headers = DEFAULT_REQUEST_HEADERS.copy()
+        headers['Host'] = self.host
+        headers['Cookie'] = "wants_mature_content=1; birthtime=754700401;"
+        content = self.download_page(url, headers)
+
+        title = content.xpath("//div[@class='apphub_AppName']/text()")[0]
+        developer = content.xpath("//div[@id='developers_list']/a/text()")
+        publisher = content.xpath("//div[@class='glance_ctn']//div[@class='dev_row'][2]//a/text()")
+        release_date = parse_date(
+            content.xpath(
+                "//div[@class='release_date']/div[@class='date']/text()")[0]
+        )
+
+        genre = content.xpath(
+            "//div[@class='details_block']/b[2]/following-sibling::a/text()")
+
+        platform = ['PC']
+
+        brief = content.xpath(
+            "//div[@class='game_description_snippet']/text()")[0].strip()
+
+        img_url = content.xpath(
+            "//img[@class='game_header_image_full']/@src"
+        )[0].replace("header.jpg", "library_600x900.jpg")
+        raw_img, img_ext = self.download_image(img_url, url)
+
+        # no 600x900 picture
+        if raw_img is None:
+            img_url = content.xpath("//img[@class='game_header_image_full']/@src")[0]
+            raw_img, img_ext = self.download_image(img_url, url)
+
+        if raw_img is not None:
+            self.raw_img = raw_img
+            self.img_ext = img_ext
+
+        data = {
+            'title': title if title else self.raw_data['title'],
+            'other_title': None,
+            'developer': developer if 'developer' not in self.raw_data else self.raw_data['developer'],
+            'publisher': publisher if 'publisher' not in self.raw_data else self.raw_data['publisher'],
+            'release_date': release_date if 'release_date' not in self.raw_data else self.raw_data['release_date'],
+            'genre': genre if 'genre' not in self.raw_data else self.raw_data['genre'],
+            'platform': platform if 'platform' not in self.raw_data else self.raw_data['platform'],
+            'brief': brief if brief else self.raw_data['brief'],
+            'other_info': None if 'other_info' not in self.raw_data else self.raw_data['other_info'],
+            'source_site': self.site_name,
+            'source_url': effective_url
+        }
+        self.raw_data = data
+        return self.raw_data, self.raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        m = cls.regex.match(raw_url)
+        if m:
+            return m[0]
+        else:
+            return None
diff --git a/common/scrapers/tmdb.py b/common/scrapers/tmdb.py
new file mode 100644
index 00000000..15072add
--- /dev/null
+++ b/common/scrapers/tmdb.py
@@ -0,0 +1,150 @@
+import requests
+import re
+from common.models import SourceSiteEnum
+from movies.models import Movie
+from movies.forms import MovieForm
+from django.conf import settings
+from common.scraper import *
+
+
+class TmdbMovieScraper(AbstractScraper):
+    site_name = SourceSiteEnum.TMDB.value
+    host = 'https://www.themoviedb.org/'
+    data_class = Movie
+    form_class = MovieForm
+    regex = re.compile(r"https://www\.themoviedb\.org/(movie|tv)/([a-zA-Z0-9]+)")
+    # http://api.themoviedb.org/3/genre/movie/list?api_key=&language=zh
+    # http://api.themoviedb.org/3/genre/tv/list?api_key=&language=zh
+    genre_map = {
+        'Sci-Fi & Fantasy': 'Sci-Fi',
+        'War & Politics':   'War',
+        '儿童':             'Kids',
+        '冒险':             'Adventure',
+        '剧情':             'Drama',
+        '动作':             'Action',
+        '动作冒险':         'Action',
+        '动画':             'Animation',
+        '历史':             'History',
+        '喜剧':             'Comedy',
+        '奇幻':             'Fantasy',
+        '家庭':             'Family',
+        '恐怖':             'Horror',
+        '悬疑':             'Mystery',
+        '惊悚':             'Thriller',
+        '战争':             'War',
+        '新闻':             'News',
+        '爱情':             'Romance',
+        '犯罪':             'Crime',
+        '电视电影':         'TV Movie',
+        '真人秀':           'Reality-TV',
+        '科幻':             'Sci-Fi',
+        '纪录':             'Documentary',
+        '肥皂剧':           'Soap',
+        '脱口秀':           'Talk-Show',
+        '西部':             'Western',
+        '音乐':             'Music',
+    }
+
+    def scrape_imdb(self, imdb_code):
+        api_url = f"https://api.themoviedb.org/3/find/{imdb_code}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&external_source=imdb_id"
+        r = requests.get(api_url)
+        res_data = r.json()
+        if 'movie_results' in res_data and len(res_data['movie_results']) > 0:
+            url = f"https://www.themoviedb.org/movie/{res_data['movie_results'][0]['id']}"
+        elif 'tv_results' in res_data and len(res_data['tv_results']) > 0:
+            url = f"https://www.themoviedb.org/tv/{res_data['tv_results'][0]['id']}"
+        else:
+            raise ValueError("Cannot find IMDb ID in TMDB")
+        return self.scrape(url)
+
+    def scrape(self, url):
+        m = self.regex.match(url)
+        if m:
+            effective_url = m[0]
+        else:
+            raise ValueError("not valid url")
+        effective_url = m[0]
+        is_series = m[1] == 'tv'
+        id = m[2]
+        if is_series:
+            api_url = f"https://api.themoviedb.org/3/tv/{id}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&append_to_response=external_ids,credits"
+        else:
+            api_url = f"https://api.themoviedb.org/3/movie/{id}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&append_to_response=external_ids,credits"
+        r = requests.get(api_url)
+        res_data = r.json()
+
+        if is_series:
+            title = res_data['name']
+            orig_title = res_data['original_name']
+            year = int(res_data['first_air_date'].split('-')[0]) if res_data['first_air_date'] else None
+            imdb_code = res_data['external_ids']['imdb_id']
+            showtime = [{res_data['first_air_date']: "首播日期"}] if res_data['first_air_date'] else None
+            duration = None
+        else:
+            title = res_data['title']
+            orig_title = res_data['original_title']
+            year = int(res_data['release_date'].split('-')[0]) if res_data['release_date'] else None
+            showtime = [{res_data['release_date']: "发布日期"}] if res_data['release_date'] else None
+            imdb_code = res_data['imdb_id']
+            duration = res_data['runtime'] if res_data['runtime'] else None # in minutes
+
+        genre = list(map(lambda x: self.genre_map[x['name']] if x['name'] in self.genre_map else 'Other', res_data['genres']))
+        language = list(map(lambda x: x['name'], res_data['spoken_languages']))
+        brief = res_data['overview']
+
+        if is_series:
+            director = list(map(lambda x: x['name'], res_data['created_by']))
+        else:
+            director = list(map(lambda x: x['name'], filter(lambda c: c['job'] == 'Director', res_data['credits']['crew'])))
+        playwright = list(map(lambda x: x['name'], filter(lambda c: c['job'] == 'Screenplay', res_data['credits']['crew'])))
+        actor = list(map(lambda x: x['name'], res_data['credits']['cast']))
+        area = []
+
+        other_info = {}
+        other_info['TMDB评分'] = res_data['vote_average']
+        # other_info['分级'] = res_data['contentRating']
+        # other_info['Metacritic评分'] = res_data['metacriticRating']
+        # other_info['奖项'] = res_data['awards']
+        other_info['TMDB_ID'] = id
+        if is_series:
+            other_info['Seasons'] = res_data['number_of_seasons']
+            other_info['Episodes'] = res_data['number_of_episodes']
+
+        img_url = ('https://image.tmdb.org/t/p/original/' + res_data['poster_path']) if res_data['poster_path'] is not None else None
+        # TODO: use GET /configuration to get base url
+        raw_img, ext = self.download_image(img_url, url)
+
+        data = {
+            'title': title,
+            'orig_title': orig_title,
+            'other_title': None,
+            'imdb_code': imdb_code,
+            'director': director,
+            'playwright': playwright,
+            'actor': actor,
+            'genre': genre,
+            'showtime': showtime,
+            'site': None,
+            'area': area,
+            'language': language,
+            'year': year,
+            'duration': duration,
+            'season': None,
+            'episodes': None,
+            'single_episode_length': None,
+            'brief': brief,
+            'is_series': is_series,
+            'other_info': other_info,
+            'source_site': self.site_name,
+            'source_url': effective_url,
+        }
+        self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext
+        return data, raw_img
+
+    @classmethod
+    def get_effective_url(cls, raw_url):
+        m = cls.regex.match(raw_url)
+        if raw_url:
+            return m[0]
+        else:
+            return None
diff --git a/common/search/meilisearch.py b/common/search/meilisearch.py
new file mode 100644
index 00000000..d7a13a2f
--- /dev/null
+++ b/common/search/meilisearch.py
@@ -0,0 +1,183 @@
+import logging
+import meilisearch
+from django.conf import settings
+from django.db.models.signals import post_save, post_delete
+import types
+
+
+INDEX_NAME = 'items'
+SEARCHABLE_ATTRIBUTES = ['title', 'orig_title', 'other_title', 'subtitle', 'artist', 'author', 'translator', 'developer', 'director', 'actor', 'playwright', 'pub_house', 'company', 'publisher', 'isbn', 'imdb_code']
+INDEXABLE_DIRECT_TYPES = ['BigAutoField', 'BooleanField', 'CharField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'TextField', 'ArrayField']
+INDEXABLE_TIME_TYPES = ['DateTimeField']
+INDEXABLE_DICT_TYPES = ['JSONField']
+INDEXABLE_FLOAT_TYPES = ['DecimalField']
+# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
+SEARCH_PAGE_SIZE = 20
+
+
+logger = logging.getLogger(__name__)
+
+
+def item_post_save_handler(sender, instance, created, **kwargs):
+    if not created and settings.SEARCH_INDEX_NEW_ONLY:
+        return
+    Indexer.replace_item(instance)
+
+
+def item_post_delete_handler(sender, instance, **kwargs):
+    Indexer.delete_item(instance)
+
+
+def tag_post_save_handler(sender, instance, **kwargs):
+    pass
+
+
+def tag_post_delete_handler(sender, instance, **kwargs):
+    pass
+
+
+class Indexer:
+    class_map = {}
+    _instance = None
+
+    @classmethod
+    def instance(self):
+        if self._instance is None:
+            self._instance = meilisearch.Client(settings.MEILISEARCH_SERVER, settings.MEILISEARCH_KEY).index(INDEX_NAME)
+        return self._instance
+
+    @classmethod
+    def init(self):
+        meilisearch.Client(settings.MEILISEARCH_SERVER, settings.MEILISEARCH_KEY).create_index(INDEX_NAME, {'primaryKey': '_id'})
+        self.update_settings()
+
+    @classmethod
+    def update_settings(self):
+        self.instance().update_searchable_attributes(SEARCHABLE_ATTRIBUTES)
+        self.instance().update_filterable_attributes(['_class', 'tags', 'source_site'])
+        self.instance().update_settings({'displayedAttributes': ['_id', '_class', 'id', 'title', 'tags']})
+
+    @classmethod
+    def get_stats(self):
+        return self.instance().get_stats()
+
+    @classmethod
+    def busy(self):
+        return self.instance().get_stats()['isIndexing']
+
+    @classmethod
+    def update_model_indexable(self, model):
+        if settings.SEARCH_BACKEND is None:
+            return
+        self.class_map[model.__name__] = model
+        model.indexable_fields = ['tags']
+        model.indexable_fields_time = []
+        model.indexable_fields_dict = []
+        model.indexable_fields_float = []
+        for field in model._meta.get_fields():
+            type = field.get_internal_type()
+            if type in INDEXABLE_DIRECT_TYPES:
+                model.indexable_fields.append(field.name)
+            elif type in INDEXABLE_TIME_TYPES:
+                model.indexable_fields_time.append(field.name)
+            elif type in INDEXABLE_DICT_TYPES:
+                model.indexable_fields_dict.append(field.name)
+            elif type in INDEXABLE_FLOAT_TYPES:
+                model.indexable_fields_float.append(field.name)
+        post_save.connect(item_post_save_handler, sender=model)
+        post_delete.connect(item_post_delete_handler, sender=model)
+
+    @classmethod
+    def obj_to_dict(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        item = {
+            '_id': pk,
+            '_class': obj.__class__.__name__,
+            # 'id': obj.id
+        }
+        for field in obj.__class__.indexable_fields:
+            item[field] = getattr(obj, field)
+        for field in obj.__class__.indexable_fields_time:
+            item[field] = getattr(obj, field).timestamp()
+        for field in obj.__class__.indexable_fields_float:
+            item[field] = float(getattr(obj, field)) if getattr(obj, field) else None
+        for field in obj.__class__.indexable_fields_dict:
+            d = getattr(obj, field)
+            if d.__class__ is dict:
+                item.update(d)
+        item = {k: v for k, v in item.items() if v}
+        return item
+
+    @classmethod
+    def replace_item(self, obj):
+        try:
+            self.instance().add_documents([self.obj_to_dict(obj)])
+        except Exception as e:
+            logger.error(f"replace item error: \n{e}")
+
+    @classmethod
+    def replace_batch(self, objects):
+        try:
+            self.instance().update_documents(documents=objects)
+        except Exception as e:
+            logger.error(f"replace batch error: \n{e}")
+
+    @classmethod
+    def delete_item(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        try:
+            self.instance().delete_document(pk)
+        except Exception as e:
+            logger.error(f"delete item error: \n{e}")
+
+    @classmethod
+    def patch_item(self, obj, fields):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        data = {}
+        for f in fields:
+            data[f] = getattr(obj, f)
+        try:
+            self.instance().update_documents(documents=[data], primary_key=[pk])
+        except Exception as e:
+            logger.error(f"patch item error: \n{e}")
+
+    @classmethod
+    def search(self, q, page=1, category=None, tag=None, sort=None):
+        if category or tag:
+            f = []
+            if category == 'music':
+                f.append("(_class = 'Album' OR _class = 'Song')")
+            elif category:
+                f.append(f"_class = '{category}'")
+            if tag:
+                t = tag.replace("'", "\'")
+                f.append(f"tags = '{t}'")
+            filter = ' AND '.join(f)
+        else:
+            filter = None
+        options = {
+            'offset': (page - 1) * SEARCH_PAGE_SIZE,
+            'limit': SEARCH_PAGE_SIZE,
+            'filter': filter,
+            'facetsDistribution': ['_class'],
+            'sort': None
+        }
+        try:
+            r = self.instance().search(q, options)
+        except Exception as e:
+            logger.error(f"MeiliSearch error: \n{e}")
+            r = {'nbHits': 0, 'hits': []}
+        # print(r)
+        results = types.SimpleNamespace()
+        results.items = list([x for x in map(lambda i: self.item_to_obj(i), r['hits']) if x is not None])
+        results.num_pages = (r['nbHits'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
+        # print(results)
+        return results
+
+    @classmethod
+    def item_to_obj(self, item):
+        try:
+            return self.class_map[item['_class']].objects.get(id=item['id'])
+        except Exception as e:
+            logger.error(f"unable to load search result item from db:\n{item}")
+            return None
diff --git a/common/search/typesense.py b/common/search/typesense.py
new file mode 100644
index 00000000..523fb370
--- /dev/null
+++ b/common/search/typesense.py
@@ -0,0 +1,215 @@
+import logging
+import typesense
+from django.conf import settings
+from django.db.models.signals import post_save, post_delete
+
+
+INDEX_NAME = 'items'
+SEARCHABLE_ATTRIBUTES = ['title', 'orig_title', 'other_title', 'subtitle', 'artist', 'author', 'translator',
+                         'developer', 'director', 'actor', 'playwright', 'pub_house', 'company', 'publisher', 'isbn', 'imdb_code']
+FILTERABLE_ATTRIBUTES = ['_class', 'tags', 'source_site']
+INDEXABLE_DIRECT_TYPES = ['BigAutoField', 'BooleanField', 'CharField',
+                          'PositiveIntegerField', 'PositiveSmallIntegerField', 'TextField', 'ArrayField']
+INDEXABLE_TIME_TYPES = ['DateTimeField']
+INDEXABLE_DICT_TYPES = ['JSONField']
+INDEXABLE_FLOAT_TYPES = ['DecimalField']
+SORTING_ATTRIBUTE = None
+# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
+SEARCH_PAGE_SIZE = 20
+
+
+logger = logging.getLogger(__name__)
+
+
+def item_post_save_handler(sender, instance, created, **kwargs):
+    if not created and settings.SEARCH_INDEX_NEW_ONLY:
+        return
+    Indexer.replace_item(instance)
+
+
+def item_post_delete_handler(sender, instance, **kwargs):
+    Indexer.delete_item(instance)
+
+
+def tag_post_save_handler(sender, instance, **kwargs):
+    pass
+
+
+def tag_post_delete_handler(sender, instance, **kwargs):
+    pass
+
+
+class Indexer:
+    class_map = {}
+    _instance = None
+
+    @classmethod
+    def instance(self):
+        if self._instance is None:
+            self._instance = typesense.Client(settings.TYPESENSE_CONNECTION)
+        return self._instance
+
+    @classmethod
+    def init(self):
+        # self.instance().collections[INDEX_NAME].delete()
+        # fields = [
+        #     {"name": "_class", "type": "string", "facet": True},
+        #     {"name": "source_site", "type": "string", "facet": True},
+        #     {"name": ".*", "type": "auto", "locale": "zh"},
+        # ]
+        # use dumb schema below before typesense fix a bug
+        fields = [
+            {'name': 'id', 'type': 'string'},
+            {'name': '_id', 'type': 'int64'},
+            {'name': '_class', 'type': 'string', "facet": True},
+            {'name': 'source_site', 'type': 'string', "facet": True},
+            {'name': 'isbn', 'optional': True, 'type': 'string'},
+            {'name': 'imdb_code', 'optional': True, 'type': 'string'},
+            {'name': 'author', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'orig_title', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'pub_house', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'title', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'translator', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'subtitle', 'optional': True, 'locale': 'zh', 'type': 'string'},
+            {'name': 'artist', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'company', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'developer', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'other_title', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'publisher', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'actor', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'director', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'playwright', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': 'tags', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
+            {'name': '.*', 'optional': True, 'locale': 'zh', 'type': 'auto'},
+        ]
+
+        self.instance().collections.create({
+            "name": INDEX_NAME,
+            "fields": fields
+        })
+
+    @classmethod
+    def update_settings(self):
+        # https://github.com/typesense/typesense/issues/96
+        print('not supported by typesense yet')
+        pass
+
+    @classmethod
+    def get_stats(self):
+        return self.instance().collections[INDEX_NAME].retrieve()
+
+    @classmethod
+    def busy(self):
+        return False
+
+    @classmethod
+    def update_model_indexable(self, model):
+        if settings.SEARCH_BACKEND is None:
+            return
+        self.class_map[model.__name__] = model
+        model.indexable_fields = ['tags']
+        model.indexable_fields_time = []
+        model.indexable_fields_dict = []
+        model.indexable_fields_float = []
+        for field in model._meta.get_fields():
+            type = field.get_internal_type()
+            if type in INDEXABLE_DIRECT_TYPES:
+                model.indexable_fields.append(field.name)
+            elif type in INDEXABLE_TIME_TYPES:
+                model.indexable_fields_time.append(field.name)
+            elif type in INDEXABLE_DICT_TYPES:
+                model.indexable_fields_dict.append(field.name)
+            elif type in INDEXABLE_FLOAT_TYPES:
+                model.indexable_fields_float.append(field.name)
+        post_save.connect(item_post_save_handler, sender=model)
+        post_delete.connect(item_post_delete_handler, sender=model)
+
+    @classmethod
+    def obj_to_dict(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        item = {
+            '_class': obj.__class__.__name__,
+        }
+        for field in obj.__class__.indexable_fields:
+            item[field] = getattr(obj, field)
+        for field in obj.__class__.indexable_fields_time:
+            item[field] = getattr(obj, field).timestamp()
+        for field in obj.__class__.indexable_fields_float:
+            item[field] = float(getattr(obj, field)) if getattr(
+                obj, field) else None
+        for field in obj.__class__.indexable_fields_dict:
+            d = getattr(obj, field)
+            if d.__class__ is dict:
+                item.update(d)
+        item = {k: v for k, v in item.items() if v and (
+            k in SEARCHABLE_ATTRIBUTES or k in FILTERABLE_ATTRIBUTES or k == 'id')}
+        item['_id'] = item['id']
+        # typesense requires primary key to be named 'id', type string
+        item['id'] = pk
+        return item
+
+    @classmethod
+    def replace_item(self, obj):
+        try:
+            self.instance().collections[INDEX_NAME].documents.upsert(self.obj_to_dict(obj), {
+                'dirty_values': 'coerce_or_drop'
+            })
+        except Exception as e:
+            logger.error(f"replace item error: \n{e}")
+
+    @classmethod
+    def replace_batch(self, objects):
+        try:
+            self.instance().collections[INDEX_NAME].documents.import_(
+                objects, {'action': 'upsert'})
+        except Exception as e:
+            logger.error(f"replace batch error: \n{e}")
+
+    @classmethod
+    def delete_item(self, obj):
+        pk = f'{obj.__class__.__name__}-{obj.id}'
+        try:
+            self.instance().collections[INDEX_NAME].documents[pk].delete()
+        except Exception as e:
+            logger.error(f"delete item error: \n{e}")
+
+    @classmethod
+    def search(self, q, page=1, category=None, tag=None, sort=None):
+        f = []
+        if category == 'music':
+            f.append('_class:= [Album, Song]')
+        elif category:
+            f.append('_class:= ' + category)
+        else:
+            f.append('')
+        if tag:
+            f.append(f"tags:= '{tag}'")
+        filter = ' && '.join(f)
+        options = {
+            'q': q,
+            'page': page,
+            'per_page': SEARCH_PAGE_SIZE,
+            'query_by': ','.join(SEARCHABLE_ATTRIBUTES),
+            'filter_by': filter,
+            # 'facetsDistribution': ['_class'],
+            # 'sort_by': None,
+        }
+        # print(q)
+        r = self.instance().collections[INDEX_NAME].documents.search(options)
+        # print(r)
+        import types
+        results = types.SimpleNamespace()
+        results.items = list([x for x in map(lambda i: self.item_to_obj(
+            i['document']), r['hits']) if x is not None])
+        results.num_pages = (
+            r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
+        # print(results)
+        return results
+
+    @classmethod
+    def item_to_obj(self, item):
+        try:
+            return self.class_map[item['_class']].objects.get(id=item['_id'])
+        except Exception as e:
+            logger.error(f"unable to load search result item from db:\n{item}")
+            return None
diff --git a/common/searcher.py b/common/searcher.py
new file mode 100644
index 00000000..48c43af9
--- /dev/null
+++ b/common/searcher.py
@@ -0,0 +1,209 @@
+from urllib.parse import quote_plus
+from enum import Enum
+from common.models import SourceSiteEnum
+from django.conf import settings
+from common.scrapers.goodreads import GoodreadsScraper
+from common.scrapers.spotify import get_spotify_token
+import requests
+from lxml import html
+import logging
+
+SEARCH_PAGE_SIZE = 5  # not all apis support page size
+logger = logging.getLogger(__name__)
+
+
+class Category(Enum):
+    Book = '书籍'
+    Movie = '电影'
+    Music = '音乐'
+    Game = '游戏'
+    TV = '剧集'
+
+
+class SearchResultItem:
+    def __init__(self, category, source_site, source_url, title, subtitle, brief, cover_url):
+        self.category = category
+        self.source_site = source_site
+        self.source_url = source_url
+        self.title = title
+        self.subtitle = subtitle
+        self.brief = brief
+        self.cover_url = cover_url
+
+    @property
+    def verbose_category_name(self):
+        return self.category.value
+
+    @property
+    def link(self):
+        return f"/search?q={quote_plus(self.source_url)}"
+
+    @property
+    def scraped(self):
+        return False
+
+
+class ProxiedRequest:
+    @classmethod
+    def get(cls, url):
+        u = f'http://api.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={quote_plus(url)}'
+        return requests.get(u, timeout=10)
+
+
+class Goodreads:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            search_url = f'https://www.goodreads.com/search?page={page}&q={quote_plus(q)}'
+            r = requests.get(search_url)
+            if r.url.startswith('https://www.goodreads.com/book/show/'):
+                # Goodreads will 302 if only one result matches ISBN
+                data, img = GoodreadsScraper.scrape(r.url, r)
+                subtitle = f"{data['pub_year']} {', '.join(data['author'])} {', '.join(data['translator'] if data['translator'] else [])}"
+                results.append(SearchResultItem(Category.Book, SourceSiteEnum.GOODREADS,
+                                                data['source_url'], data['title'], subtitle,
+                                                data['brief'], data['cover_url']))
+            else:
+                h = html.fromstring(r.content.decode('utf-8'))
+                for c in h.xpath('//tr[@itemtype="http://schema.org/Book"]'):
+                    el_cover = c.xpath('.//img[@class="bookCover"]/@src')
+                    cover = el_cover[0] if el_cover else None
+                    el_title = c.xpath('.//a[@class="bookTitle"]//text()')
+                    title = ''.join(el_title).strip() if el_title else None
+                    el_url = c.xpath('.//a[@class="bookTitle"]/@href')
+                    url = 'https://www.goodreads.com' + \
+                        el_url[0] if el_url else None
+                    el_authors = c.xpath('.//a[@class="authorName"]//text()')
+                    subtitle = ', '.join(el_authors) if el_authors else None
+                    results.append(SearchResultItem(
+                        Category.Book, SourceSiteEnum.GOODREADS, url, title, subtitle, '', cover))
+        except Exception as e:
+            logger.error(f"Goodreads search '{q}' error: {e}")
+        return results
+
+
+class GoogleBooks:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            api_url = f'https://www.googleapis.com/books/v1/volumes?country=us&q={quote_plus(q)}&startIndex={SEARCH_PAGE_SIZE*(page-1)}&maxResults={SEARCH_PAGE_SIZE}&maxAllowedMaturityRating=MATURE'
+            j = requests.get(api_url).json()
+            if 'items' in j:
+                for b in j['items']:
+                    if 'title' not in b['volumeInfo']:
+                        continue
+                    title = b['volumeInfo']['title']
+                    subtitle = ''
+                    if 'publishedDate' in b['volumeInfo']:
+                        subtitle += b['volumeInfo']['publishedDate'] + ' '
+                    if 'authors' in b['volumeInfo']:
+                        subtitle += ', '.join(b['volumeInfo']['authors'])
+                    if 'description' in b['volumeInfo']:
+                        brief = b['volumeInfo']['description']
+                    elif 'textSnippet' in b['volumeInfo']:
+                        brief = b["volumeInfo"]["textSnippet"]["searchInfo"]
+                    else:
+                        brief = ''
+                    category = Category.Book
+                    # b['volumeInfo']['infoLink'].replace('http:', 'https:')
+                    url = 'https://books.google.com/books?id=' + b['id']
+                    cover = b['volumeInfo']['imageLinks']['thumbnail'] if 'imageLinks' in b['volumeInfo'] else None
+                    results.append(SearchResultItem(
+                        category, SourceSiteEnum.GOOGLEBOOKS, url, title, subtitle, brief, cover))
+        except Exception as e:
+            logger.error(f"GoogleBooks search '{q}' error: {e}")
+        return results
+
+
+class TheMovieDatabase:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            api_url = f'https://api.themoviedb.org/3/search/multi?query={quote_plus(q)}&page={page}&api_key={settings.TMDB_API3_KEY}&language=zh-CN&include_adult=true'
+            j = requests.get(api_url).json()
+            for m in j['results']:
+                if m['media_type'] in ['tv', 'movie']:
+                    url = f"https://www.themoviedb.org/{m['media_type']}/{m['id']}"
+                    if m['media_type'] == 'tv':
+                        cat = Category.TV
+                        title = m['name']
+                        subtitle = f"{m.get('first_air_date')} {m.get('original_name')}"
+                    else:
+                        cat = Category.Movie
+                        title = m['title']
+                        subtitle = f"{m.get('release_date')} {m.get('original_name')}"
+                    cover = f"https://image.tmdb.org/t/p/w500/{m.get('poster_path')}"
+                    results.append(SearchResultItem(
+                        cat, SourceSiteEnum.TMDB, url, title, subtitle, m.get('overview'), cover))
+        except Exception as e:
+            logger.error(f"TMDb search '{q}' error: {e}")
+        return results
+
+
+class Spotify:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            api_url = f"https://api.spotify.com/v1/search?q={q}&type=album&limit={SEARCH_PAGE_SIZE}&offset={page*SEARCH_PAGE_SIZE}"
+            headers = {
+                'Authorization': f"Bearer {get_spotify_token()}"
+            }
+            j = requests.get(api_url, headers=headers).json()
+            for a in j['albums']['items']:
+                title = a['name']
+                subtitle = a['release_date']
+                for artist in a['artists']:
+                    subtitle += ' ' + artist['name']
+                url = a['external_urls']['spotify']
+                cover = a['images'][0]['url']
+                results.append(SearchResultItem(
+                    Category.Music, SourceSiteEnum.SPOTIFY, url, title, subtitle, '', cover))
+        except Exception as e:
+            logger.error(f"Spotify search '{q}' error: {e}")
+        return results
+
+
+class Bandcamp:
+    @classmethod
+    def search(self, q, page=1):
+        results = []
+        try:
+            search_url = f'https://bandcamp.com/search?from=results&item_type=a&page={page}&q={quote_plus(q)}'
+            r = requests.get(search_url)
+            h = html.fromstring(r.content.decode('utf-8'))
+            for c in h.xpath('//li[@class="searchresult data-search"]'):
+                el_cover = c.xpath('.//div[@class="art"]/img/@src')
+                cover = el_cover[0] if el_cover else None
+                el_title = c.xpath('.//div[@class="heading"]//text()')
+                title = ''.join(el_title).strip() if el_title else None
+                el_url = c.xpath('..//div[@class="itemurl"]/a/@href')
+                url = el_url[0] if el_url else None
+                el_authors = c.xpath('.//div[@class="subhead"]//text()')
+                subtitle = ', '.join(el_authors) if el_authors else None
+                results.append(SearchResultItem(Category.Music, SourceSiteEnum.BANDCAMP, url, title, subtitle, '', cover))
+        except Exception as e:
+            logger.error(f"Goodreads search '{q}' error: {e}")
+        return results
+
+
+class ExternalSources:
+    @classmethod
+    def search(self, c, q, page=1):
+        if not q:
+            return []
+        results = []
+        if c == '' or c is None:
+            c = 'all'
+        if c == 'all' or c == 'movie':
+            results.extend(TheMovieDatabase.search(q, page))
+        if c == 'all' or c == 'book':
+            results.extend(GoogleBooks.search(q, page))
+            results.extend(Goodreads.search(q, page))
+        if c == 'all' or c == 'music':
+            results.extend(Spotify.search(q, page))
+            results.extend(Bandcamp.search(q, page))
+        return results
diff --git a/common/static/css/boofilsic.css b/common/static/css/boofilsic.css
index 514f1aba..b93d83b6 100644
--- a/common/static/css/boofilsic.css
+++ b/common/static/css/boofilsic.css
@@ -270,16 +270,13 @@ h6 {
 
 img {
   max-width: 100%;
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
 }
 
 img.emoji {
   height: 14px;
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
-  -o-object-fit: contain;
-     object-fit: contain;
+  box-sizing: border-box;
+  object-fit: contain;
   position: relative;
   top: 3px;
 }
@@ -315,13 +312,11 @@ img.emoji--large {
 *,
 *:after,
 *:before {
-  -webkit-box-sizing: inherit;
-          box-sizing: inherit;
+  box-sizing: inherit;
 }
 
 html {
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
+  box-sizing: border-box;
   height: 100%;
 }
 
@@ -379,16 +374,12 @@ input[type='time'],
 input[type='color'],
 textarea,
 select {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
+  appearance: none;
   background-color: transparent;
   border: 0.1rem solid #ccc;
   border-radius: .4rem;
-  -webkit-box-shadow: none;
-          box-shadow: none;
-  -webkit-box-sizing: inherit;
-          box-sizing: inherit;
+  box-shadow: none;
+  box-sizing: inherit;
   padding: .6rem 1.0rem;
 }
 
@@ -408,51 +399,6 @@ select:focus {
   outline: 0;
 }
 
-input[type='email']::-webkit-input-placeholder,
-input[type='number']::-webkit-input-placeholder,
-input[type='password']::-webkit-input-placeholder,
-input[type='search']::-webkit-input-placeholder,
-input[type='tel']::-webkit-input-placeholder,
-input[type='text']::-webkit-input-placeholder,
-input[type='url']::-webkit-input-placeholder,
-input[type='date']::-webkit-input-placeholder,
-input[type='time']::-webkit-input-placeholder,
-input[type='color']::-webkit-input-placeholder,
-textarea::-webkit-input-placeholder,
-select::-webkit-input-placeholder {
-  color: #ccc;
-}
-
-input[type='email']:-ms-input-placeholder,
-input[type='number']:-ms-input-placeholder,
-input[type='password']:-ms-input-placeholder,
-input[type='search']:-ms-input-placeholder,
-input[type='tel']:-ms-input-placeholder,
-input[type='text']:-ms-input-placeholder,
-input[type='url']:-ms-input-placeholder,
-input[type='date']:-ms-input-placeholder,
-input[type='time']:-ms-input-placeholder,
-input[type='color']:-ms-input-placeholder,
-textarea:-ms-input-placeholder,
-select:-ms-input-placeholder {
-  color: #ccc;
-}
-
-input[type='email']::-ms-input-placeholder,
-input[type='number']::-ms-input-placeholder,
-input[type='password']::-ms-input-placeholder,
-input[type='search']::-ms-input-placeholder,
-input[type='tel']::-ms-input-placeholder,
-input[type='text']::-ms-input-placeholder,
-input[type='url']::-ms-input-placeholder,
-input[type='date']::-ms-input-placeholder,
-input[type='time']::-ms-input-placeholder,
-input[type='color']::-ms-input-placeholder,
-textarea::-ms-input-placeholder,
-select::-ms-input-placeholder {
-  color: #ccc;
-}
-
 input[type='email']::placeholder,
 input[type='number']::placeholder,
 input[type='password']::placeholder,
@@ -468,11 +414,6 @@ select::placeholder {
   color: #ccc;
 }
 
-::-moz-selection {
-  color: white;
-  background-color: #00a1cc;
-}
-
 ::selection {
   color: white;
   background-color: #00a1cc;
@@ -480,29 +421,21 @@ select::placeholder {
 
 .navbar {
   background-color: #f7f7f7;
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
+  box-sizing: border-box;
   padding: 10px 0;
   margin-bottom: 50px;
   border-bottom: #ccc 0.5px solid;
 }
 
 .navbar .navbar__wrapper {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: justify;
-      -ms-flex-pack: justify;
-          justify-content: space-between;
-  -webkit-box-align: center;
-      -ms-flex-align: center;
-          align-items: center;
+  justify-content: space-between;
+  align-items: center;
   position: relative;
 }
 
 .navbar .navbar__logo {
-  -ms-flex-preferred-size: 100px;
-      flex-basis: 100px;
+  flex-basis: 100px;
 }
 
 .navbar .navbar__logo-link {
@@ -511,11 +444,8 @@ select::placeholder {
 
 .navbar .navbar__link-list {
   margin: 0;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -ms-flex-pack: distribute;
-      justify-content: space-around;
+  justify-content: space-around;
 }
 
 .navbar .navbar__link {
@@ -533,12 +463,8 @@ select::placeholder {
 
 .navbar .navbar__search-box {
   margin: 0 12% 0 15px;
-  display: -webkit-inline-box;
-  display: -ms-inline-flexbox;
   display: inline-flex;
-  -webkit-box-flex: 1;
-      -ms-flex: 1;
-          flex: 1;
+  flex: 1;
 }
 
 .navbar .navbar__search-box > input[type="search"] {
@@ -556,9 +482,7 @@ select::placeholder {
   padding: 0;
   padding-left: 10px;
   color: #606c76;
-  -webkit-appearance: auto;
-     -moz-appearance: auto;
-          appearance: auto;
+  appearance: auto;
   background-color: white;
   height: 32px;
   width: 80px;
@@ -596,7 +520,6 @@ select::placeholder {
   .navbar .navbar__link-list {
     margin-top: 7px;
     max-height: 0;
-    -webkit-transition: max-height 0.6s ease-out;
     transition: max-height 0.6s ease-out;
     overflow: hidden;
   }
@@ -605,12 +528,10 @@ select::placeholder {
     position: absolute;
     right: 5px;
     top: 3px;
-    -webkit-transform: scale(0.7);
-            transform: scale(0.7);
+    transform: scale(0.7);
   }
   .navbar .navbar__dropdown-btn:hover + .navbar__link-list {
     max-height: 500px;
-    -webkit-transition: max-height 0.6s ease-in;
     transition: max-height 0.6s ease-in;
   }
   .navbar .navbar__search-box {
@@ -654,15 +575,9 @@ select::placeholder {
   width: 26%;
   float: right;
   position: relative;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-orient: vertical;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: column;
-          flex-direction: column;
-  -ms-flex-pack: distribute;
-      justify-content: space-around;
+  flex-direction: column;
+  justify-content: space-around;
 }
 
 .grid::after {
@@ -673,10 +588,7 @@ select::placeholder {
 
 @media (max-width: 575.98px) {
   .grid .grid__aside {
-    -webkit-box-orient: vertical !important;
-    -webkit-box-direction: normal !important;
-        -ms-flex-direction: column !important;
-            flex-direction: column !important;
+    flex-direction: column !important;
   }
 }
 
@@ -688,28 +600,19 @@ select::placeholder {
   .grid .grid__aside {
     width: 100%;
     float: none;
-    -webkit-box-orient: horizontal;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: row;
-            flex-direction: row;
+    flex-direction: row;
   }
   .grid .grid__aside--tablet-column {
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
   }
   .grid--reverse-order {
-    -webkit-transform: scaleY(-1);
-            transform: scaleY(-1);
+    transform: scaleY(-1);
   }
   .grid .grid__main--reverse-order {
-    -webkit-transform: scaleY(-1);
-            transform: scaleY(-1);
+    transform: scaleY(-1);
   }
   .grid .grid__aside--reverse-order {
-    -webkit-transform: scaleY(-1);
-            transform: scaleY(-1);
+    transform: scaleY(-1);
   }
 }
 
@@ -778,8 +681,7 @@ select::placeholder {
   margin-bottom: 4px !important;
   position: absolute !important;
   left: 50%;
-  -webkit-transform: translateX(-50%);
-          transform: translateX(-50%);
+  transform: translateX(-50%);
   bottom: 0;
   width: 100%;
 }
@@ -839,17 +741,14 @@ select::placeholder {
   display: inline-block;
   position: relative;
   left: 50%;
-  -webkit-transform: translateX(-50%) scale(0.4);
-          transform: translateX(-50%) scale(0.4);
+  transform: translateX(-50%) scale(0.4);
   width: 80px;
   height: 80px;
 }
 
 .spinner div {
-  -webkit-transform-origin: 40px 40px;
-          transform-origin: 40px 40px;
-  -webkit-animation: spinner 1.2s linear infinite;
-          animation: spinner 1.2s linear infinite;
+  transform-origin: 40px 40px;
+  animation: spinner 1.2s linear infinite;
 }
 
 .spinner div::after {
@@ -865,96 +764,63 @@ select::placeholder {
 }
 
 .spinner div:nth-child(1) {
-  -webkit-transform: rotate(0deg);
-          transform: rotate(0deg);
-  -webkit-animation-delay: -1.1s;
-          animation-delay: -1.1s;
+  transform: rotate(0deg);
+  animation-delay: -1.1s;
 }
 
 .spinner div:nth-child(2) {
-  -webkit-transform: rotate(30deg);
-          transform: rotate(30deg);
-  -webkit-animation-delay: -1s;
-          animation-delay: -1s;
+  transform: rotate(30deg);
+  animation-delay: -1s;
 }
 
 .spinner div:nth-child(3) {
-  -webkit-transform: rotate(60deg);
-          transform: rotate(60deg);
-  -webkit-animation-delay: -0.9s;
-          animation-delay: -0.9s;
+  transform: rotate(60deg);
+  animation-delay: -0.9s;
 }
 
 .spinner div:nth-child(4) {
-  -webkit-transform: rotate(90deg);
-          transform: rotate(90deg);
-  -webkit-animation-delay: -0.8s;
-          animation-delay: -0.8s;
+  transform: rotate(90deg);
+  animation-delay: -0.8s;
 }
 
 .spinner div:nth-child(5) {
-  -webkit-transform: rotate(120deg);
-          transform: rotate(120deg);
-  -webkit-animation-delay: -0.7s;
-          animation-delay: -0.7s;
+  transform: rotate(120deg);
+  animation-delay: -0.7s;
 }
 
 .spinner div:nth-child(6) {
-  -webkit-transform: rotate(150deg);
-          transform: rotate(150deg);
-  -webkit-animation-delay: -0.6s;
-          animation-delay: -0.6s;
+  transform: rotate(150deg);
+  animation-delay: -0.6s;
 }
 
 .spinner div:nth-child(7) {
-  -webkit-transform: rotate(180deg);
-          transform: rotate(180deg);
-  -webkit-animation-delay: -0.5s;
-          animation-delay: -0.5s;
+  transform: rotate(180deg);
+  animation-delay: -0.5s;
 }
 
 .spinner div:nth-child(8) {
-  -webkit-transform: rotate(210deg);
-          transform: rotate(210deg);
-  -webkit-animation-delay: -0.4s;
-          animation-delay: -0.4s;
+  transform: rotate(210deg);
+  animation-delay: -0.4s;
 }
 
 .spinner div:nth-child(9) {
-  -webkit-transform: rotate(240deg);
-          transform: rotate(240deg);
-  -webkit-animation-delay: -0.3s;
-          animation-delay: -0.3s;
+  transform: rotate(240deg);
+  animation-delay: -0.3s;
 }
 
 .spinner div:nth-child(10) {
-  -webkit-transform: rotate(270deg);
-          transform: rotate(270deg);
-  -webkit-animation-delay: -0.2s;
-          animation-delay: -0.2s;
+  transform: rotate(270deg);
+  animation-delay: -0.2s;
 }
 
 .spinner div:nth-child(11) {
-  -webkit-transform: rotate(300deg);
-          transform: rotate(300deg);
-  -webkit-animation-delay: -0.1s;
-          animation-delay: -0.1s;
+  transform: rotate(300deg);
+  animation-delay: -0.1s;
 }
 
 .spinner div:nth-child(12) {
-  -webkit-transform: rotate(330deg);
-          transform: rotate(330deg);
-  -webkit-animation-delay: 0s;
-          animation-delay: 0s;
-}
-
-@-webkit-keyframes spinner {
-  0% {
-    opacity: 1;
-  }
-  100% {
-    opacity: 0;
-  }
+  transform: rotate(330deg);
+  animation-delay: 0s;
 }
 
 @keyframes spinner {
@@ -969,8 +835,7 @@ select::placeholder {
 .bg-mask {
   background-color: black;
   z-index: 1;
-  -webkit-filter: opacity(20%);
-          filter: opacity(20%);
+  filter: opacity(20%);
   position: fixed;
   width: 100%;
   height: 100%;
@@ -986,8 +851,7 @@ select::placeholder {
   width: 500px;
   top: 50%;
   left: 50%;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   background-color: #f7f7f7;
   padding: 20px 20px 10px 20px;
   color: #606c76;
@@ -1107,8 +971,7 @@ select::placeholder {
   width: 500px;
   top: 50%;
   left: 50%;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   background-color: #f7f7f7;
   padding: 20px 20px 10px 20px;
   color: #606c76;
@@ -1146,8 +1009,7 @@ select::placeholder {
   width: 500px;
   top: 50%;
   left: 50%;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   background-color: #f7f7f7;
   padding: 20px 20px 10px 20px;
   color: #606c76;
@@ -1196,8 +1058,46 @@ select::placeholder {
   word-break: break-all;
 }
 
+.add-to-list-modal {
+  z-index: 2;
+  display: none;
+  position: fixed;
+  width: 500px;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background-color: #f7f7f7;
+  padding: 20px 20px 10px 20px;
+  color: #606c76;
+}
+
+.add-to-list-modal .add-to-list-modal__head {
+  margin-bottom: 20px;
+}
+
+.add-to-list-modal .add-to-list-modal__head::after {
+  content: ' ';
+  clear: both;
+  display: table;
+}
+
+.add-to-list-modal .add-to-list-modal__title {
+  font-weight: bold;
+  font-size: 1.2em;
+  float: left;
+}
+
+.add-to-list-modal .add-to-list-modal__close-button {
+  float: right;
+  cursor: pointer;
+}
+
+.add-to-list-modal .add-to-list-modal__confirm-button {
+  float: right;
+}
+
 @media (max-width: 575.98px) {
-  .mark-modal, .confirm-modal, .announcement-modal {
+  .mark-modal, .confirm-modal, .announcement-modal .add-to-list-modal {
     width: 100%;
   }
 }
@@ -1246,6 +1146,13 @@ select::placeholder {
   font-weight: bold;
 }
 
+.source-label.source-label__igdb {
+  background-color: #323A44;
+  color: #DFE1E2;
+  border: none;
+  font-weight: bold;
+}
+
 .source-label.source-label__steam {
   background: linear-gradient(30deg, #1387b8, #111d2e);
   color: white;
@@ -1261,6 +1168,37 @@ select::placeholder {
   font-weight: 600;
 }
 
+.source-label.source-label__goodreads {
+  background: #F4F1EA;
+  color: #372213;
+  font-weight: lighter;
+}
+
+.source-label.source-label__tmdb {
+  background: linear-gradient(90deg, #91CCA3, #1FB4E2);
+  color: white;
+  border: none;
+  font-weight: lighter;
+  padding-top: 2px;
+}
+
+.source-label.source-label__googlebooks {
+  color: white;
+  background-color: #4285F4;
+  border-color: #4285F4;
+}
+
+.source-label.source-label__bandcamp {
+  color: white;
+  background-color: #28A0C1;
+  display: inline-block;
+}
+
+.source-label.source-label__bandcamp span {
+  display: inline-block;
+  margin: 0 4px;
+}
+
 .main-section-wrapper {
   padding: 32px 48px 32px 36px;
   background-color: #f7f7f7;
@@ -1276,8 +1214,6 @@ select::placeholder {
 }
 
 .entity-list .entity-list__entity {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   margin-bottom: 36px;
 }
@@ -1289,8 +1225,7 @@ select::placeholder {
 }
 
 .entity-list .entity-list__entity-img {
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
   min-width: 130px;
   max-width: 130px;
 }
@@ -1368,15 +1303,12 @@ select::placeholder {
 .entity-detail .entity-detail__img {
   height: 210px;
   float: left;
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
   max-width: 150px;
-  -o-object-position: top;
-     object-position: top;
+  object-position: top;
 }
 
 .entity-detail .entity-detail__img-origin {
-  cursor: -webkit-zoom-in;
   cursor: zoom-in;
 }
 
@@ -1451,14 +1383,10 @@ select::placeholder {
 }
 
 .entity-desc .entity-desc__unfold-button {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   color: #00a1cc;
   background-color: transparent;
-  -webkit-box-pack: center;
-      -ms-flex-pack: center;
-          justify-content: center;
+  justify-content: center;
   text-align: center;
 }
 
@@ -1597,20 +1525,14 @@ select::placeholder {
 }
 
 .entity-sort .entity-sort__entity-list {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: start;
-      -ms-flex-pack: start;
-          justify-content: flex-start;
-  -ms-flex-wrap: wrap;
-      flex-wrap: wrap;
+  justify-content: flex-start;
+  flex-wrap: wrap;
 }
 
 .entity-sort .entity-sort__entity {
   padding: 0 10px;
-  -ms-flex-preferred-size: 20%;
-      flex-basis: 20%;
+  flex-basis: 20%;
   text-align: center;
   display: inline-block;
   color: #606c76;
@@ -1658,12 +1580,8 @@ select::placeholder {
 }
 
 .entity-sort-control {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: end;
-      -ms-flex-pack: end;
-          justify-content: flex-end;
+  justify-content: flex-end;
 }
 
 .entity-sort-control__button {
@@ -1693,12 +1611,8 @@ select::placeholder {
 }
 
 .related-user-list .related-user-list__user {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: start;
-      -ms-flex-pack: start;
-          justify-content: flex-start;
+  justify-content: flex-start;
   margin-bottom: 20px;
 }
 
@@ -1791,12 +1705,9 @@ select::placeholder {
   overflow: auto;
   scroll-behavior: smooth;
   scrollbar-width: none;
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   margin: auto;
-  -webkit-box-sizing: border-box;
-          box-sizing: border-box;
+  box-sizing: border-box;
   padding-bottom: 10px;
 }
 
@@ -1820,8 +1731,7 @@ select::placeholder {
 }
 
 .track-carousel__track img {
-  -o-object-fit: contain;
-     object-fit: contain;
+  object-fit: contain;
 }
 
 .track-carousel__track-title {
@@ -1829,14 +1739,9 @@ select::placeholder {
 }
 
 .track-carousel__button {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: center;
-      -ms-flex-pack: center;
-          justify-content: center;
-  -ms-flex-line-pack: center;
-      align-content: center;
+  justify-content: center;
+  align-content: center;
   background: white;
   border: none;
   padding: 8px;
@@ -1849,22 +1754,17 @@ select::placeholder {
 
 .track-carousel__button--prev {
   left: 0;
-  -webkit-transform: translate(50%, -50%);
-          transform: translate(50%, -50%);
+  transform: translate(50%, -50%);
 }
 
 .track-carousel__button--next {
   right: 0;
-  -webkit-transform: translate(-50%, -50%);
-          transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
 }
 
 @media (max-width: 575.98px) {
   .entity-list .entity-list__entity {
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
     margin-bottom: 30px;
   }
   .entity-list .entity-list__entity-text {
@@ -1883,10 +1783,7 @@ select::placeholder {
     -webkit-line-clamp: 5;
   }
   .entity-detail {
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
   }
   .entity-detail .entity-detail__title {
     margin-bottom: 5px;
@@ -1894,13 +1791,8 @@ select::placeholder {
   .entity-detail .entity-detail__info {
     margin-left: 0;
     float: none;
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: column;
-            flex-direction: column;
+    flex-direction: column;
     width: 100%;
   }
   .entity-detail .entity-detail__img {
@@ -1920,8 +1812,7 @@ select::placeholder {
     margin-top: 24px;
   }
   .entity-sort .entity-sort__entity {
-    -ms-flex-preferred-size: 50%;
-        flex-basis: 50%;
+    flex-basis: 50%;
   }
   .entity-sort .entity-sort__entity-img {
     height: 130px;
@@ -1947,23 +1838,14 @@ select::placeholder {
     padding: 32px 28px 28px 28px;
   }
   .entity-detail {
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
   }
 }
 
 .aside-section-wrapper {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-flex: 1;
-      -ms-flex: 1;
-          flex: 1;
-  -webkit-box-orient: vertical;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: column;
-          flex-direction: column;
+  flex: 1;
+  flex-direction: column;
   width: 100%;
   padding: 28px 25px 12px 25px;
   background-color: #f7f7f7;
@@ -2005,18 +1887,12 @@ select::placeholder {
 }
 
 .action-panel .action-panel__button-group {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: justify;
-      -ms-flex-pack: justify;
-          justify-content: space-between;
+  justify-content: space-between;
 }
 
 .action-panel .action-panel__button-group--center {
-  -webkit-box-pack: center;
-      -ms-flex-pack: center;
-          justify-content: center;
+  justify-content: center;
 }
 
 .action-panel .action-panel__button {
@@ -2084,12 +1960,8 @@ select::placeholder {
 }
 
 .user-profile .user-profile__header {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-align: start;
-      -ms-flex-align: start;
-          align-items: flex-start;
+  align-items: flex-start;
   margin-bottom: 15px;
 }
 
@@ -2118,12 +1990,8 @@ select::placeholder {
 }
 
 .user-relation .user-relation__related-user-list {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
-  -webkit-box-pack: start;
-      -ms-flex-pack: start;
-          justify-content: flex-start;
+  justify-content: flex-start;
 }
 
 .user-relation .user-relation__related-user-list:last-of-type {
@@ -2131,8 +1999,7 @@ select::placeholder {
 }
 
 .user-relation .user-relation__related-user {
-  -ms-flex-preferred-size: 25%;
-      flex-basis: 25%;
+  flex-basis: 25%;
   padding: 0px 3px;
   text-align: center;
   display: inline-block;
@@ -2268,7 +2135,7 @@ select::placeholder {
   background-color: #d5d5d5;
   border-radius: 0;
   height: 10px;
-  width: 65%;
+  width: 54%;
 }
 
 .import-panel .import-panel__progress progress::-webkit-progress-bar {
@@ -2310,21 +2177,13 @@ select::placeholder {
 }
 
 .entity-card {
-  display: -webkit-box;
-  display: -ms-flexbox;
   display: flex;
   margin-bottom: 10px;
-  -webkit-box-orient: vertical;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: column;
-          flex-direction: column;
+  flex-direction: column;
 }
 
 .entity-card--horizontal {
-  -webkit-box-orient: horizontal;
-  -webkit-box-direction: normal;
-      -ms-flex-direction: row;
-          flex-direction: row;
+  flex-direction: row;
 }
 
 .entity-card .entity-card__img {
@@ -2353,8 +2212,7 @@ select::placeholder {
 }
 
 .entity-card .entity-card__img-wrapper {
-  -ms-flex-preferred-size: 100px;
-      flex-basis: 100px;
+  flex-basis: 100px;
 }
 
 @media (max-width: 575.98px) {
@@ -2373,16 +2231,10 @@ select::placeholder {
     margin-bottom: 20px !important;
   }
   .action-panel {
-    -webkit-box-orient: vertical !important;
-    -webkit-box-direction: normal !important;
-        -ms-flex-direction: column !important;
-            flex-direction: column !important;
+    flex-direction: column !important;
   }
   .entity-card--horizontal {
-    -webkit-box-orient: vertical !important;
-    -webkit-box-direction: normal !important;
-        -ms-flex-direction: column !important;
-            flex-direction: column !important;
+    flex-direction: column !important;
   }
   .entity-card .entity-card__info-wrapper {
     margin-left: 10px !important;
@@ -2394,11 +2246,8 @@ select::placeholder {
 
 @media (max-width: 991.98px) {
   .add-entity-entries {
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
-    -ms-flex-pack: distribute;
-        justify-content: space-around;
+    justify-content: space-around;
   }
   .aside-section-wrapper {
     padding: 24px 25px 10px 25px;
@@ -2419,15 +2268,10 @@ select::placeholder {
     margin: 0;
   }
   .action-panel {
-    -webkit-box-orient: horizontal;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: row;
-            flex-direction: row;
+    flex-direction: row;
   }
   .action-panel .action-panel__button-group {
-    -webkit-box-pack: space-evenly;
-        -ms-flex-pack: space-evenly;
-            justify-content: space-evenly;
+    justify-content: space-evenly;
   }
   .relation-dropdown {
     margin-bottom: 20px;
@@ -2436,54 +2280,36 @@ select::placeholder {
     padding-bottom: 10px;
     background-color: #f7f7f7;
     width: 100%;
-    display: -webkit-box;
-    display: -ms-flexbox;
     display: flex;
-    -webkit-box-pack: center;
-        -ms-flex-pack: center;
-            justify-content: center;
-    -webkit-box-align: center;
-        -ms-flex-align: center;
-            align-items: center;
+    justify-content: center;
+    align-items: center;
     cursor: pointer;
-    -webkit-transition: -webkit-transform 0.3s;
-    transition: -webkit-transform 0.3s;
     transition: transform 0.3s;
-    transition: transform 0.3s, -webkit-transform 0.3s;
   }
   .relation-dropdown .relation-dropdown__button:focus {
     background-color: red;
   }
   .relation-dropdown .relation-dropdown__button > .icon-arrow {
-    -webkit-transition: -webkit-transform 0.3s;
-    transition: -webkit-transform 0.3s;
     transition: transform 0.3s;
-    transition: transform 0.3s, -webkit-transform 0.3s;
   }
   .relation-dropdown .relation-dropdown__button:hover > .icon-arrow > svg {
     fill: #00a1cc;
   }
   .relation-dropdown .relation-dropdown__button > .icon-arrow--expand {
-    -webkit-transform: rotate(-180deg);
-            transform: rotate(-180deg);
+    transform: rotate(-180deg);
   }
   .relation-dropdown .relation-dropdown__button + .relation-dropdown__body--expand {
     max-height: 2000px;
-    -webkit-transition: max-height 1s ease-in;
     transition: max-height 1s ease-in;
   }
   .relation-dropdown .relation-dropdown__body {
     background-color: #f7f7f7;
     max-height: 0;
-    -webkit-transition: max-height 1s ease-out;
     transition: max-height 1s ease-out;
     overflow: hidden;
   }
   .entity-card {
-    -webkit-box-orient: horizontal;
-    -webkit-box-direction: normal;
-        -ms-flex-direction: row;
-            flex-direction: row;
+    flex-direction: row;
   }
   .entity-card .entity-card__info-wrapper {
     margin-left: 30px;
@@ -2510,21 +2336,7 @@ select::placeholder {
   overflow: auto;
 }
 
-.entity-form > input[type='email'],
-.entity-form > input[type='number'],
-.entity-form > input[type='password'],
-.entity-form > input[type='search'],
-.entity-form > input[type='tel'],
-.entity-form > input[type='text'],
-.entity-form > input[type='url'],
-.entity-form textarea, .review-form > input[type='email'],
-.review-form > input[type='number'],
-.review-form > input[type='password'],
-.review-form > input[type='search'],
-.review-form > input[type='tel'],
-.review-form > input[type='text'],
-.review-form > input[type='url'],
-.review-form textarea {
+.entity-form > input[type='email'], .entity-form > input[type='number'], .entity-form > input[type='password'], .entity-form > input[type='search'], .entity-form > input[type='tel'], .entity-form > input[type='text'], .entity-form > input[type='url'], .entity-form textarea, .review-form > input[type='email'], .review-form > input[type='number'], .review-form > input[type='password'], .review-form > input[type='search'], .review-form > input[type='tel'], .review-form > input[type='text'], .review-form > input[type='url'], .review-form textarea {
   width: 100%;
 }
 
@@ -2614,16 +2426,12 @@ select::placeholder {
 
 .ms-parent > .ms-choice {
   margin-bottom: 1.5rem;
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
+  appearance: none;
   background-color: transparent;
   border: 0.1rem solid #ccc;
   border-radius: .4rem;
-  -webkit-box-shadow: none;
-          box-shadow: none;
-  -webkit-box-sizing: inherit;
-          box-sizing: inherit;
+  box-shadow: none;
+  box-sizing: inherit;
   padding: .6rem 1.0rem;
   width: 100%;
   height: 30.126px;
@@ -2664,7 +2472,9 @@ select::placeholder {
 }
 
 .tag-input input {
-  -webkit-box-flex: 1;
-      -ms-flex-positive: 1;
-          flex-grow: 1;
+  flex-grow: 1;
+}
+
+.tools-section-wrapper input, .tools-section-wrapper select {
+  width: unset;
 }
diff --git a/common/static/css/boofilsic.min.css b/common/static/css/boofilsic.min.css
index 19ea74b6..4a5c94d6 100644
--- a/common/static/css/boofilsic.min.css
+++ b/common/static/css/boofilsic.min.css
@@ -1 +1 @@
-@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;-o-object-fit:contain;object-fit:contain}img.emoji{height:14px;-webkit-box-sizing:border-box;box-sizing:border-box;-o-object-fit:contain;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::-webkit-input-placeholder,input[type='number']::-webkit-input-placeholder,input[type='password']::-webkit-input-placeholder,input[type='search']::-webkit-input-placeholder,input[type='tel']::-webkit-input-placeholder,input[type='text']::-webkit-input-placeholder,input[type='url']::-webkit-input-placeholder,input[type='date']::-webkit-input-placeholder,input[type='time']::-webkit-input-placeholder,input[type='color']::-webkit-input-placeholder,textarea::-webkit-input-placeholder,select::-webkit-input-placeholder{color:#ccc}input[type='email']:-ms-input-placeholder,input[type='number']:-ms-input-placeholder,input[type='password']:-ms-input-placeholder,input[type='search']:-ms-input-placeholder,input[type='tel']:-ms-input-placeholder,input[type='text']:-ms-input-placeholder,input[type='url']:-ms-input-placeholder,input[type='date']:-ms-input-placeholder,input[type='time']:-ms-input-placeholder,input[type='color']:-ms-input-placeholder,textarea:-ms-input-placeholder,select:-ms-input-placeholder{color:#ccc}input[type='email']::-ms-input-placeholder,input[type='number']::-ms-input-placeholder,input[type='password']::-ms-input-placeholder,input[type='search']::-ms-input-placeholder,input[type='tel']::-ms-input-placeholder,input[type='text']::-ms-input-placeholder,input[type='url']::-ms-input-placeholder,input[type='date']::-ms-input-placeholder,input[type='time']::-ms-input-placeholder,input[type='color']::-ms-input-placeholder,textarea::-ms-input-placeholder,select::-ms-input-placeholder{color:#ccc}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::-moz-selection{color:white;background-color:#00a1cc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative}.navbar .navbar__logo{-ms-flex-preferred-size:100px;flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;-webkit-transform:scale(0.7);transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.grid .grid__aside--tablet-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.grid--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__main--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__aside--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;-webkit-transform:translateX(-50%) scale(0.4);transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:spinner 1.2s linear infinite;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){-webkit-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg);-webkit-animation-delay:-1s;animation-delay:-1s}.spinner div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation-delay:-.8s;animation-delay:-.8s}.spinner div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg);-webkit-animation-delay:-.7s;animation-delay:-.7s}.spinner div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg);-webkit-animation-delay:-.6s;animation-delay:-.6s}.spinner div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation-delay:-.5s;animation-delay:-.5s}.spinner div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg);-webkit-animation-delay:-.4s;animation-delay:-.4s}.spinner div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg);-webkit-animation-delay:-.3s;animation-delay:-.3s}.spinner div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg);-webkit-animation-delay:-.2s;animation-delay:-.2s}.spinner div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg);-webkit-animation-delay:-.1s;animation-delay:-.1s}.spinner div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg);-webkit-animation-delay:0s;animation-delay:0s}@-webkit-keyframes spinner{0%{opacity:1}100%{opacity:0}}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;-webkit-filter:opacity(20%);filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.source-label.source-label__bangumi{background:#FCFCFC;color:#F09199;font-style:italic;font-weight:600}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{-o-object-fit:contain;object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;-o-object-fit:contain;object-fit:contain;max-width:150px;-o-object-position:top;object-position:top}.entity-detail .entity-detail__img-origin{cursor:-webkit-zoom-in;cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:-webkit-box;display:-ms-flexbox;display:flex;color:#00a1cc;background-color:transparent;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:8px}.entity-sort .entity-sort__count{color:#bbb}.entity-sort .entity-sort__count::before{content:'('}.entity-sort .entity-sort__count::after{content:')'}.entity-sort .entity-sort__entity-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;-ms-flex-preferred-size:20%;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:-webkit-box;display:-ms-flexbox;display:flex;margin:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{-o-object-fit:contain;object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-line-pack:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;-webkit-transform:translate(50%, -50%);transform:translate(50%, -50%)}.track-carousel__button--next{right:0;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{-ms-flex-preferred-size:50%;flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:-webkit-box;display:-ms-flexbox;display:flex}}.aside-section-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.action-panel .action-panel__button-group--center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.user-relation .user-relation__related-user-list:last-of-type{margin-bottom:0}.user-relation .user-relation__related-user{-ms-flex-preferred-size:25%;flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");width:48px;height:48px}@media (min-width: 575.98px) and (max-width: 991.98px){.user-relation .user-relation__related-user-avatar{height:unset;width:60%;max-width:96px}}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__body{padding-left:0}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.import-panel{overflow-x:hidden}.import-panel .import-panel__label{display:inline-block;margin-bottom:10px}.import-panel .import-panel__body{padding-left:0;border:2px dashed #00a1cc;padding:6px 9px}.import-panel .import-panel__body form{margin:0}@media (max-width: 991.98px){.import-panel .import-panel__body{border:unset;padding-left:0}}.import-panel .import-panel__help{background-color:#e5e5e5;border-radius:100000px;display:inline-block;width:16px;height:16px;text-align:center;font-size:12px;cursor:help}.import-panel .import-panel__checkbox{display:inline-block;margin-right:10px}.import-panel .import-panel__checkbox label{display:inline}.import-panel .import-panel__checkbox input[type="checkbox"]{margin:0;position:relative;top:2px}.import-panel .import-panel__checkbox--last{margin-right:0}.import-panel .import-panel__file-input{margin-top:10px}.import-panel .import-panel__button{line-height:unset;height:unset;padding:4px 15px}.import-panel .import-panel__progress{padding-top:10px}.import-panel .import-panel__progress:not(:first-child){border-top:#bbb 1px dashed}.import-panel .import-panel__progress label{display:inline}.import-panel .import-panel__progress progress{background-color:#d5d5d5;border-radius:0;height:10px;width:65%}.import-panel .import-panel__progress progress::-webkit-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__progress progress::-webkit-progress-value{background-color:#00a1cc}.import-panel .import-panel__progress progress::-moz-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__last-task:not(:first-child){padding-top:4px;border-top:#bbb 1px dashed}.import-panel .import-panel__last-task .index:not(:last-of-type){margin-right:8px}.import-panel .import-panel__fail-urls{margin-top:10px}.import-panel .import-panel__fail-urls li{word-break:break-all}.import-panel .import-panel__fail-urls ul{max-height:100px;overflow-y:auto}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-card--horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{-ms-flex-preferred-size:100px;flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card--horizontal{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.action-panel .action-panel__button-group{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:2000px;-webkit-transition:max-height 1s ease-in;transition:max-height 1s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;-webkit-transition:max-height 1s ease-out;transition:max-height 1s ease-out;overflow:hidden}.entity-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset}.tippy-box{border:#606c76 1px solid;background-color:#f7f7f7;padding:3px 5px}.tag-input input{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}
+@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;object-fit:contain}img.emoji{height:14px;box-sizing:border-box;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:flex;justify-content:space-between;align-items:center;position:relative}.navbar .navbar__logo{flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:flex;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:inline-flex;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:flex;flex-direction:column;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;flex-direction:row}.grid .grid__aside--tablet-column{flex-direction:column}.grid--reverse-order{transform:scaleY(-1)}.grid .grid__main--reverse-order{transform:scaleY(-1)}.grid .grid__aside--reverse-order{transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{transform-origin:40px 40px;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){transform:rotate(0deg);animation-delay:-1.1s}.spinner div:nth-child(2){transform:rotate(30deg);animation-delay:-1s}.spinner div:nth-child(3){transform:rotate(60deg);animation-delay:-.9s}.spinner div:nth-child(4){transform:rotate(90deg);animation-delay:-.8s}.spinner div:nth-child(5){transform:rotate(120deg);animation-delay:-.7s}.spinner div:nth-child(6){transform:rotate(150deg);animation-delay:-.6s}.spinner div:nth-child(7){transform:rotate(180deg);animation-delay:-.5s}.spinner div:nth-child(8){transform:rotate(210deg);animation-delay:-.4s}.spinner div:nth-child(9){transform:rotate(240deg);animation-delay:-.3s}.spinner div:nth-child(10){transform:rotate(270deg);animation-delay:-.2s}.spinner div:nth-child(11){transform:rotate(300deg);animation-delay:-.1s}.spinner div:nth-child(12){transform:rotate(330deg);animation-delay:0s}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}.add-to-list-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.add-to-list-modal .add-to-list-modal__head{margin-bottom:20px}.add-to-list-modal .add-to-list-modal__head::after{content:' ';clear:both;display:table}.add-to-list-modal .add-to-list-modal__title{font-weight:bold;font-size:1.2em;float:left}.add-to-list-modal .add-to-list-modal__close-button{float:right;cursor:pointer}.add-to-list-modal .add-to-list-modal__confirm-button{float:right}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal .add-to-list-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__igdb{background-color:#323A44;color:#DFE1E2;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.source-label.source-label__bangumi{background:#FCFCFC;color:#F09199;font-style:italic;font-weight:600}.source-label.source-label__goodreads{background:#F4F1EA;color:#372213;font-weight:lighter}.source-label.source-label__tmdb{background:linear-gradient(90deg, #91CCA3, #1FB4E2);color:white;border:none;font-weight:lighter;padding-top:2px}.source-label.source-label__googlebooks{color:white;background-color:#4285F4;border-color:#4285F4}.source-label.source-label__bandcamp{color:#fff;background-color:#28A0C1;display:inline-block}.source-label.source-label__bandcamp span{display:inline-block;margin:0 4px}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;object-fit:contain;max-width:150px;object-position:top}.entity-detail .entity-detail__img-origin{cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:flex;color:#00a1cc;background-color:transparent;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:8px}.entity-sort .entity-sort__count{color:#bbb}.entity-sort .entity-sort__count::before{content:'('}.entity-sort .entity-sort__count::after{content:')'}.entity-sort .entity-sort__entity-list{display:flex;justify-content:flex-start;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:flex;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:flex;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:flex;margin:auto;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:flex;justify-content:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;transform:translate(50%, -50%)}.track-carousel__button--next{right:0;transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:flex;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:flex}}.aside-section-wrapper{display:flex;flex:1;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:flex;justify-content:space-between}.action-panel .action-panel__button-group--center{justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:flex;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:flex;justify-content:flex-start}.user-relation .user-relation__related-user-list:last-of-type{margin-bottom:0}.user-relation .user-relation__related-user{flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");width:48px;height:48px}@media (min-width: 575.98px) and (max-width: 991.98px){.user-relation .user-relation__related-user-avatar{height:unset;width:60%;max-width:96px}}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__body{padding-left:0}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.import-panel{overflow-x:hidden}.import-panel .import-panel__label{display:inline-block;margin-bottom:10px}.import-panel .import-panel__body{padding-left:0;border:2px dashed #00a1cc;padding:6px 9px}.import-panel .import-panel__body form{margin:0}@media (max-width: 991.98px){.import-panel .import-panel__body{border:unset;padding-left:0}}.import-panel .import-panel__help{background-color:#e5e5e5;border-radius:100000px;display:inline-block;width:16px;height:16px;text-align:center;font-size:12px;cursor:help}.import-panel .import-panel__checkbox{display:inline-block;margin-right:10px}.import-panel .import-panel__checkbox label{display:inline}.import-panel .import-panel__checkbox input[type="checkbox"]{margin:0;position:relative;top:2px}.import-panel .import-panel__checkbox--last{margin-right:0}.import-panel .import-panel__file-input{margin-top:10px}.import-panel .import-panel__button{line-height:unset;height:unset;padding:4px 15px}.import-panel .import-panel__progress{padding-top:10px}.import-panel .import-panel__progress:not(:first-child){border-top:#bbb 1px dashed}.import-panel .import-panel__progress label{display:inline}.import-panel .import-panel__progress progress{background-color:#d5d5d5;border-radius:0;height:10px;width:54%}.import-panel .import-panel__progress progress::-webkit-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__progress progress::-webkit-progress-value{background-color:#00a1cc}.import-panel .import-panel__progress progress::-moz-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__last-task:not(:first-child){padding-top:4px;border-top:#bbb 1px dashed}.import-panel .import-panel__last-task .index:not(:last-of-type){margin-right:8px}.import-panel .import-panel__fail-urls{margin-top:10px}.import-panel .import-panel__fail-urls li{word-break:break-all}.import-panel .import-panel__fail-urls ul{max-height:100px;overflow-y:auto}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:flex;margin-bottom:10px;flex-direction:column}.entity-card--horizontal{flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{flex-direction:column !important}.entity-card--horizontal{flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:flex;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{flex-direction:row}.action-panel .action-panel__button-group{justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:flex;justify-content:center;align-items:center;cursor:pointer;transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:2000px;transition:max-height 1s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;transition:max-height 1s ease-out;overflow:hidden}.entity-card{flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset}.tippy-box{border:#606c76 1px solid;background-color:#f7f7f7;padding:3px 5px}.tag-input input{flex-grow:1}.tools-section-wrapper input,.tools-section-wrapper select{width:unset}
diff --git a/common/static/img/fediverse.svg b/common/static/img/fediverse.svg
new file mode 100644
index 00000000..c3ab108c
--- /dev/null
+++ b/common/static/img/fediverse.svg
@@ -0,0 +1,5 @@
+<svg width="850" height="850" xmlns="http://www.w3.org/2000/svg" version="1.1">
+ <g>
+  <path d="m464.16327,0q-32,0 -55,22t-25,55t20.5,58t56,27t58.5,-20.5t27,-56t-20.5,-59t-56.5,-26.5l-5,0zm-87,95l-232,118q20,20 25,48l231,-118q-19,-20 -24,-48zm167,27q-13,25 -38,38l183,184q13,-25 39,-38l-184,-184zm-142,22l-135,265l40,40l143,-280q-28,-5 -48,-25zm104,16q-22,11 -46,10l-8,-1l21,132l56,9l-23,-150zm-426,34q-32,0 -55,22.5t-25,55t20.5,58t56.5,27t59,-21t26.5,-56t-21,-58.5t-55.5,-27l-6,0zm90,68q1,9 1,18q-1,19 -10,35l132,21l26,-50l-149,-24zm225,36l-26,51l311,49q-1,-8 -1,-17q1,-19 10,-36l-294,-47zm372,6q-32,1 -55,23t-24.5,55t21,58t56,27t58.5,-20.5t27,-56.5t-20.5,-59t-56.5,-27l-6,0zm-606,13q-13,25 -39,38l210,210l51,-25l-222,-223zm-40,38q-21,11 -44,10l-9,-1l40,256q21,-10 45,-9l8,1l-40,-257zm364,22l48,311q21,-10 44,-9l10,1l-46,-294l-56,-9zm195,23l-118,60l8,56l135,-68q-20,-20 -25,-48zm26,49l-119,231q28,5 48,25l119,-231q-28,-5 -48,-25zm-475,29l-68,134q28,5 48,25l60,-119l-40,-40zm262,17l-281,143q19,20 24,48l265,-135l-8,-56zm-55,100l-51,25l106,107q13,-25 39,-38l-94,-94zm-291,24q-32,0 -55.5,22.5t-25,55t21,57.5t56,27t58.5,-20.5t27,-56t-20.5,-58.5t-56.5,-27l-5,0zm89,68q2,9 1,18q-1,19 -9,35l256,41q-1,-9 -1,-18q1,-18 10,-35l-257,-41zm335,0q-32,0 -55,22.5t-24.5,55t20.5,58t56,27t59,-21t27,-56t-20.5,-58.5t-56.5,-27l-6,0z"/>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/common/static/img/logo.svg b/common/static/img/logo.svg
index 0570333b..cc10cff8 100644
--- a/common/static/img/logo.svg
+++ b/common/static/img/logo.svg
@@ -1 +1,140 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 101.25 37.29"><title>logo</title><path d="M1.55,30.26H2.78l2.1,3.81.7,1.44h0c-.06-.7-.16-1.56-.16-2.31V30.26H6.61V37H5.38l-2.1-3.81-.7-1.44h0c.06.72.15,1.53.15,2.29v3H1.55Z" style="fill:#606c76"/><path d="M13.09,30.26h1.2V37h-1.2Z" style="fill:#606c76"/><path d="M20.41,33.67c0-2.22,1.38-3.53,3.1-3.53a2.66,2.66,0,0,1,1.95.86l-.64.77a1.78,1.78,0,0,0-1.29-.59c-1.1,0-1.89.93-1.89,2.45s.73,2.48,1.86,2.48A1.94,1.94,0,0,0,25,35.42l.64.75a2.68,2.68,0,0,1-2.13,1C21.76,37.15,20.41,35.91,20.41,33.67Z" style="fill:#606c76"/><path d="M31.48,30.26h4.07v1H32.68V33h2.43v1H32.68v2h3v1H31.48Z" style="fill:#606c76"/><path d="M41.79,30.26H43.6a3,3,0,0,1,3.3,3.36c0,2.24-1.23,3.41-3.24,3.41H41.79Zm1.73,5.8c1.36,0,2.14-.78,2.14-2.44s-.78-2.38-2.14-2.38H43v4.82Z" style="fill:#606c76"/><path d="M53,30.26h2.17c1.39,0,2.41.43,2.41,1.67a1.53,1.53,0,0,1-.94,1.47v0A1.55,1.55,0,0,1,58,35c0,1.36-1.11,2-2.6,2H53Zm2.06,2.8c.94,0,1.35-.37,1.35-1s-.45-.9-1.32-.9h-.89v1.86Zm.16,3c1,0,1.55-.35,1.55-1.11s-.54-1-1.55-1h-1v2.14Z" style="fill:#606c76"/><path d="M63.73,36.35a.77.77,0,1,1,1.54,0,.77.77,0,1,1-1.54,0Z" style="fill:#606c76"/><path d="M71.19,33.62c0-2.19,1.22-3.48,3-3.48s3,1.3,3,3.48-1.22,3.53-3,3.53S71.19,35.81,71.19,33.62Zm4.73,0c0-1.52-.69-2.44-1.75-2.44s-1.75.92-1.75,2.44.69,2.49,1.75,2.49S75.92,35.14,75.92,33.62Z" style="fill:#606c76"/><path d="M83.27,30.26h2.28c1.41,0,2.49.5,2.49,2S87,34.39,85.55,34.39H84.47V37h-1.2Zm2.16,3.16c.92,0,1.43-.38,1.43-1.15s-.51-1.05-1.43-1.05h-1v2.2Zm-.1.64.86-.71L88.31,37H87Z" style="fill:#606c76"/><path d="M93.79,33.67A3.2,3.2,0,0,1,97,30.14,2.78,2.78,0,0,1,99,31l-.65.77A1.81,1.81,0,0,0,97,31.18c-1.2,0-2,.93-2,2.45s.72,2.48,2.07,2.48a1.54,1.54,0,0,0,1-.3V34.35H96.78v-1h2.36v3a3.08,3.08,0,0,1-2.16.8C95.15,37.15,93.79,35.91,93.79,33.67Z" style="fill:#606c76"/><path d="M15.48,26.35h-.15L1.39,23.41a.71.71,0,0,1-.56-.69V2.32a.74.74,0,0,1,.26-.55.73.73,0,0,1,.59-.14L15.62,4.56a.71.71,0,0,1,.56.69v20.4a.7.7,0,0,1-.26.54A.66.66,0,0,1,15.48,26.35ZM2.23,22.15l12.55,2.64v-19L2.23,3.18Z" style="fill:#00a1cc"/><path d="M15.48,26.35a.65.65,0,0,1-.44-.16.67.67,0,0,1-.26-.54V5.25a.7.7,0,0,1,.55-.69l14-2.93a.7.7,0,0,1,.84.69v20.4a.71.71,0,0,1-.55.69L15.62,26.34Zm.7-20.54v19l12.54-2.64v-19ZM29.42,22.72h0Z" style="fill:#00a1cc"/><path d="M87.55,26.48a13.17,13.17,0,1,1,13.17-13.17A13.19,13.19,0,0,1,87.55,26.48Zm0-24.94A11.77,11.77,0,1,0,99.32,13.31,11.78,11.78,0,0,0,87.55,1.54Z" style="fill:#00a1cc"/><path d="M87.55,18.38a5.08,5.08,0,1,1,5.08-5.07A5.08,5.08,0,0,1,87.55,18.38Zm0-8.75a3.68,3.68,0,1,0,3.67,3.68A3.68,3.68,0,0,0,87.55,9.63Z" style="fill:#00a1cc"/><path d="M60.63,22.63H36.21a.7.7,0,0,1-.7-.7V4.69a.7.7,0,0,1,.7-.7H60.63a.7.7,0,0,1,.7.7V21.93A.7.7,0,0,1,60.63,22.63Zm-23.72-1.4h23V5.39h-23Z" style="fill:#00a1cc"/><path d="M69.17,22.63a.68.68,0,0,1-.31-.08l-8.55-4.29a.71.71,0,0,1-.38-.62V9a.71.71,0,0,1,.38-.63l8.55-4.29a.7.7,0,0,1,1,.63V21.93a.68.68,0,0,1-.33.59A.7.7,0,0,1,69.17,22.63Zm-7.84-5.42,7.14,3.58v-15L61.33,9.41Z" style="fill:#00a1cc"/></svg>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="220.10335"
+   height="107.60636"
+   viewBox="0 0 220.10335 107.60636"
+   data-svgdocument=""
+   id="_3tPmdtZQ0LGlljQGEkT-3"
+   class="fl-svgdocument"
+   x="0"
+   y="0"
+   version="1.1"
+   sodipodi:docname="logo.svg"
+   inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview21"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     units="px"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="1.4707031"
+     inkscape:cx="298.83666"
+     inkscape:cy="77.853919"
+     inkscape:window-width="1792"
+     inkscape:window-height="1067"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="_3tPmdtZQ0LGlljQGEkT-3"
+     inkscape:document-units="mm"
+     inkscape:snap-global="false" />
+  <defs
+     id="_SabXoCmWUJmxvMwSINRGH"
+     transform="matrix(1.1728515134752755, 0, 0, 1.1728515134752755, -34.46172819025348, -40.976997984526726)">
+    <rect
+       x="86.353256"
+       y="25.158035"
+       width="217.58301"
+       height="60.515274"
+       id="rect4965" />
+  </defs>
+  <g
+     id="_WTrHMt3eZfxcQKrYJirRK"
+     transform="matrix(0.95294163,0,0,0.95294163,-45.741197,-190.14988)">
+    <path
+       id="_1MYZsbAYvxe5D-50H3pho"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.519653,192.79657)"
+       data-type="polygon"
+       d="M 82.1,74 75,81.1 v 0 L 82.1,74 74,65.9 v 0 z" />
+    <path
+       id="_oL3QJlwkbW1Nmdh3qAlDC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.45702,193.17499)"
+       data-type="rect"
+       data-x="64.5"
+       data-y="51.5"
+       data-width="0"
+       data-height="9.8"
+       d="M 64.5,51.5" />
+    <path
+       id="_sK1htog-8Op55rgRI5hDg"
+       d="m 61.1,52.9 v 0 0 l -4.9,4.9 c 2.5,1.2 4.3,3.8 4.3,6.9 0,4.2 -3.4,7.6 -7.6,7.6 -3,0 -5.6,-1.8 -6.9,-4.3 L 40.1,73.9 40,74 61,95 69.1,86.9 v 0 c -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 l 7.1,-7.1 -8.1,-8 v 0 l -0.6,-0.6 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 0,-1.5 -0.6,-2.8 -1.5,-3.9 -1,-0.9 -2.4,-1.5 -3.9,-1.5 -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 68,59.9 v 0 z"
+       fill="#083b66"
+       transform="matrix(1.2602785,0,0,1.2602785,27.51993,192.73364)"
+       style="fill:#ffb380" />
+    <path
+       id="_OMWHsyuaoclMfPE9tvM7V"
+       d="m 46.5,49.5 c -4.2,0 -7.6,-3.4 -7.6,-7.6 0,-3 1.7,-5.6 4.2,-6.8 l -5.9,-5.9 v 0 l -21,21 22.2,22.2 9.5,-9.5 c -0.3,0.6 -0.4,1.3 -0.4,2 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-2.9 -2.4,-5.3 -5.3,-5.3 -0.7,0 -1.4,0.1 -2,0.4 l 8.6,-8.6 -6,-6 c -1.3,2.3 -3.9,4.1 -6.9,4.1 z"
+       fill="#8dcaff"
+       transform="matrix(1.2602785,0,0,1.2602785,27.583487,192.79709)" />
+    <path
+       id="_jAk8zpl6O80_21e1JT4hk"
+       d="m 83.3,27.4 -8.7,-8.7 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 C 80.9,9.6 78.3,7 75,7 c -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 61,5.2 l -21,21 -0.7,0.8 -0.4,0.4 9.5,9.5 c -0.6,-0.2 -1.2,-0.3 -1.8,-0.3 -2.9,0 -5.3,2.4 -5.3,5.3 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-0.5 -0.1,-0.9 -0.2,-1.4 0,-0.2 -0.1,-0.3 -0.1,-0.5 l 0.5,0.5 9.2,9.2 0.4,-0.4 0.8,-0.8 6.8,-6.8 c 0,0 0,0 0,0 -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 0,0 0,0 0,0 z"
+       fill="#c0d9b4"
+       transform="matrix(1.2602785,0,0,1.2602785,27.709267,192.98645)" />
+    <path
+       id="_GDSYpBtYoW2GJpRZoPCot"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_NldltLD5fZQlQrgFsakaC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_TbX_b0jxU8J0Cb3YWVDy6"
+       d="m 51.6,40.5 h 0.3 L 51.5,40 c 0,0.1 0,0.3 0.1,0.5 z"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.708767,192.98587)" />
+  </g>
+  <g
+     aria-label="NeoDB"
+     transform="matrix(1.5308318,0,0,1.7424549,-43.057047,-45.264217)"
+     id="text4963"
+     style="font-size:40px;line-height:1.25;white-space:pre;shape-inside:url(#rect4965);fill:#083b66">
+    <path
+       d="m 107.58017,70.426948 c 0,-1.866665 -0.85333,-6.026663 -1.28,-7.786662 -0.10667,-0.426666 0,-0.853333 -0.10667,-1.279999 -0.90666,-3.786665 -1.44,-7.733329 -1.92,-11.573327 -0.15999,-1.546665 -0.58666,-3.039998 -0.58666,-4.586664 0,-0.266666 -0.10667,-1.919998 0.42667,-1.919998 0.74666,0 0.90666,1.226666 1.59999,1.226666 0.26667,0 0.64,-0.106667 0.64,-0.426667 0,-1.013333 -1.92,-1.866665 -2.82666,-1.866665 -3.09333,0 -2.93333,3.626664 -2.93333,5.919996 0,2.079999 0.32,4.373331 0.58666,6.45333 0.16,1.279999 0.10667,2.559998 0.32,3.839997 0.16,1.12 0.64,2.719999 0.64,3.839998 0,0.906666 0.16,1.706666 0.26667,2.559998 -0.48,-0.746666 -0.69333,-1.493332 -1.12,-2.239998 -0.64,-1.066666 -1.28,-2.186666 -1.813332,-3.306665 -0.373333,-0.746666 -0.8,-1.439999 -1.226666,-2.186665 -0.426667,-0.746667 -0.693333,-1.546666 -1.119999,-2.293332 -1.919999,-3.253332 -3.626665,-6.666663 -5.493331,-9.919994 -0.213333,-0.373333 -0.693332,-0.426667 -1.066666,-0.426667 -0.693332,0 -2.933331,0.533333 -3.039998,1.333333 -0.16,1.333332 -0.16,2.719998 -0.16,4.053331 v 4.53333 c 0,1.013333 0.106667,1.973332 0.106667,2.933332 0,1.386666 0.16,2.719998 0.16,4.053331 0,0.799999 0.16,1.546666 0.16,2.346665 0,1.333333 0.05333,2.826665 0.266666,4.159998 0.106667,0.533333 0.213333,1.279999 0.213333,1.866665 0,2.399999 -0.266666,4.053331 2.826665,4.053331 0.8,0 3.093332,-0.426666 3.093332,-1.439999 0,-0.48 -0.16,-0.959999 -0.16,-1.493333 0,-2.666665 -0.213333,-5.759996 -0.64,-8.426661 -0.159999,-1.119999 -0.426666,-2.559999 -0.426666,-3.679998 0,-2.559998 -0.693333,-4.746664 -0.693333,-7.413329 1.279999,2.933332 9.599998,20.853321 11.786658,20.853321 0.8,0 3.52,-0.799999 3.52,-1.759999 z M 90.246847,53.573625 c 0.266666,0.106666 0.266666,1.333332 0.266666,1.546666 0,1.119999 -0.16,2.239998 -0.16,3.359998 -0.16,-1.493333 -0.16,-2.986665 -0.16,-4.479998 0,-0.16 0,-0.266666 0.05333,-0.426666 z m 0.266666,18.453322 0.05333,-0.106666 c 0,-0.373334 -0.106666,-0.746667 -0.106666,-1.12 v -0.48 c 0.05333,0.266667 0.106666,0.906667 0.32,1.12 0,-0.64 -0.16,-1.226666 -0.16,-1.866666 0,-0.213333 0,-0.639999 0.213333,-0.799999 0,1.119999 0.05333,2.293332 0.05333,3.413331 l -0.16,-0.16 v 0.106667 h -0.213333 z m -0.266666,-7.199996 c -0.05333,-0.266666 -0.05333,-0.479999 -0.05333,-0.746666 v -0.693333 c 0.213334,-0.266666 0.16,-2.453332 0.16,-2.719998 0.106667,0.426666 0.16,1.813332 0.16,2.346665 0,0.266667 0,1.653333 -0.266666,1.813332 z m -1.28,-15.83999 c 0.106667,-0.64 0.05333,-1.333333 0.05333,-1.973332 0.213333,1.706665 0,3.413331 0.426666,5.066663 l -0.106666,0.05333 -0.213334,-1.066666 c 0,0.533333 0.05333,1.119999 0.05333,1.653332 l -0.05333,-0.05333 v -0.533333 c -0.05333,-0.106667 -0.16,-0.32 -0.16,-0.426667 0,-0.32 0.05333,-0.639999 0.05333,-0.959999 l 0.106666,0.32 c -0.05333,-0.693333 0,-1.386666 -0.16,-2.079999 z m 0.266667,12.373326 c 0.05333,-0.373333 -0.05333,-2.239999 -0.16,-2.559999 0.16,-0.266666 0.16,-1.493332 0.16,-1.813332 0.106667,0.213333 0.106667,1.813332 0.106667,2.133332 0,0.426667 0,1.866666 -0.106667,2.239999 z m 14.613326,-0.373333 c -0.0533,0 -0.10667,-0.693333 -0.10667,-0.746667 -0.0533,0.106667 -0.0533,0.266667 -0.0533,0.373334 0,0.106666 0.0533,0.213333 0.0533,0.319999 l -0.0533,0.05333 h 0.0533 c 0,0.32 0.0533,0.639999 -0.0533,0.959999 0,-0.533333 -0.0533,-1.066666 -0.0533,-1.599999 v -0.64 0.266667 c 0.0533,-0.213333 0.0533,-0.426666 0.0533,-0.64 0.16,0.05333 0.16,1.173333 0.16,1.386666 z m -0.10667,-3.466665 c 0,0.213333 -0.0533,0.373333 0,0.586666 v -0.319999 c 0.0533,0.16 0.10667,0.373333 0.10667,0.586666 0,0.213333 -0.0533,0.48 -0.16,0.693333 0,-0.213333 0,-0.426667 -0.0533,-0.64 v 0.266667 -1.973332 c 0,0.05333 0.10666,0.213333 0.10666,0.266666 z m -13.546657,1.493333 c -0.106666,-0.373334 -0.16,-0.853333 -0.106666,-1.226666 l -0.05333,0.16 c 0.106667,-0.586667 -0.05333,-1.12 -0.05333,-1.706666 0,-0.213333 0,-0.426667 0.106667,-0.64 0.05333,1.12 0.05333,2.293332 0.106666,3.413332 z m 0.16,12.586659 c -0.05333,0.16 -0.05333,0.32 -0.05333,0.479999 l -0.16,-0.05333 c 0,-0.266666 -0.05333,-1.119999 0.16,-1.333332 0,0.159999 -0.05333,0.426666 0.05333,0.533333 0.05333,0.266666 0.106667,0.586666 0.106667,0.853332 v 0.05333 L 90.353513,72.08028 Z M 89.926847,57.733622 c 0,0.16 0,0.266667 0.05333,0.426667 -0.16,0.799999 -0.05333,1.759999 -0.05333,2.506665 -0.106667,-0.48 -0.106667,-0.96 -0.106667,-1.493333 0,-0.479999 0,-0.959999 0.106667,-1.439999 z m 0.05333,4.053331 c 0,-0.426666 0.106667,-0.799999 0,-1.279999 l 0.16,0.16 v 0.959999 c 0,0.373333 0,0.8 -0.05333,1.173333 -0.05333,-0.32 -0.106667,-0.693333 -0.106667,-1.013333 z m 0.586666,-11.093326 c 0,0.266666 -0.05333,0.479999 -0.106666,0.746666 -0.05333,-0.05333 -0.05333,-0.16 -0.05333,-0.266667 V 50.58696 c 0,-0.266667 0,-0.48 0.05333,-0.746666 0.05333,0.213333 0.05333,0.479999 0.05333,0.693333 h 0.05333 z m -0.05333,17.173323 c 0,0.213333 0,0.426666 -0.05333,0.639999 -0.05333,-0.213333 -0.05333,-0.479999 -0.05333,-0.693333 V 67.22695 c 0,-0.106667 0,-0.213333 0.05333,-0.32 0.05333,0.32 0.106666,0.64 0.106666,0.96 z m 13.706657,-3.679998 0.16,0.32 c 0,0.213333 -0.10667,0.373333 -0.21333,0.533333 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.16 0,-0.266667 0.0533,-0.426667 z m -0.32,-0.16 c 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 l 0.0533,-0.106667 -0.0533,0.05333 c 0,-0.106666 -0.0533,-0.159999 -0.0533,-0.266666 v -0.64 l 0.10666,-0.05333 c 0,0.48 0,0.906666 0,1.386666 z M 89.180181,54.213624 c -0.05333,-0.16 -0.106667,-0.319999 -0.106667,-0.533333 0,-0.266666 0.05333,-0.586666 0.05333,-0.906666 0.05333,0.16 0.05333,0.373333 0.05333,0.533333 z M 102.78017,51.22696 c -0.0533,0.373333 0.21334,0.853332 0.21334,1.226665 v 0.05333 c -0.16,-0.373333 -0.21334,-0.8 -0.37334,-1.226666 z m -11.359991,8.799994 c -0.05333,-0.32 -0.05333,-0.639999 -0.05333,-0.959999 v -0.373333 c 0.05333,0.213333 0.106667,0.586666 0.106667,0.799999 0,0.16 -0.05333,0.373333 -0.05333,0.533333 z m -2.239998,-4.319997 -0.106667,0.266666 v -0.266666 c 0,-0.213333 0,-0.373333 0.05333,-0.533333 0.05333,0.106666 0.05333,0.213333 0.05333,0.32 l 0.106666,-0.106667 v 0.426666 l -0.05333,-0.05333 -0.05333,0.213333 z m 0.213333,-1.226666 c 0,-0.05333 -0.106667,-0.586666 0,-0.48 -0.05333,-0.266666 0,-0.693333 -0.106667,-0.906666 l 0.106667,0.05333 c 0,0.32 0.05333,0.586667 0.05333,0.906666 0,0.16 0,0.32 -0.05333,0.426667 z m 0.906666,-6.399996 c 0,0.32 0,0.639999 0,0.959999 l -0.106667,-0.48 c 0,-0.106666 0.05333,-0.319999 0.106667,-0.479999 z m 12.74666,5.599996 c 0,0.213334 -0.0533,0.373333 -0.0533,0.586667 -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 v -0.32 c 0.0533,0.106667 0.10667,0.266666 0.10667,0.426666 z m 1.06667,8.693329 0.0533,-0.05333 V 62.05362 l 0.10666,0.32 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 l -0.10666,-0.213334 v 0.32 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0,-0.16 0,-0.373333 0.0533,-0.533333 z m -13.81333,6.613329 c 0,0.266666 -0.05333,0.906666 0.05333,1.226666 l -0.05333,0.106666 c -0.106667,-0.106666 -0.05333,-1.226665 0,-1.333332 z m 0.32,-2.399999 c -0.106667,0 -0.16,0.106667 -0.16,0.213334 0.05333,-0.266667 -0.05333,-0.586667 0,-0.746667 l 0.106666,0.05333 v 0.213333 h 0.05333 z m 12.53333,-8.746661 c 0.0533,0.266666 0.0533,0.586666 0,0.853333 -0.0533,-0.32 -0.0533,-0.533333 0,-0.853333 z m -0.53334,-7.253329 c 0.0533,-0.213333 0.0533,-0.426667 0.0533,-0.64 l 0.0533,0.05333 v 0.693333 z m -13.386656,6.346663 c 0,-0.16 0,-0.373333 -0.106667,-0.533333 l 0.106667,-0.266667 c 0,0.16 0,0.373333 0,0.533333 z m 1.333332,-4.853331 c -0.05333,-0.16 -0.106666,-0.319999 -0.106666,-0.479999 0,0.106666 0.05333,0.16 0.16,0.16 0,0.106666 -0.05333,0.159999 -0.05333,0.319999 0,0 0.05333,-0.05333 0.05333,-0.05333 v 0.05333 z m -0.319999,-4.479997 c -0.106667,-0.32 -0.106667,-0.693333 -0.106667,-1.066666 z m 0.16,11.839993 c 0.159999,0.05333 0.05333,0.373333 0,0.48 z m 0.213333,-6.45333 c 0,0.106667 -0.05333,0.266667 -0.05333,0.373334 v -0.48 z m -0.213333,5.493331 c 0,0.05333 0.159999,0.426666 0,0.426666 z m 0.159999,6.666662 c 0.05333,0.05333 0.05333,0.16 0.05333,0.266667 l -0.05333,0.05333 z m 13.866654,-2.773331 0.0533,0.05333 c 0,0.213333 0,0.426666 -0.0533,0.639999 z M 90.353513,53.200292 c 0,0.106666 0,0.16 -0.05333,0.266666 0,-0.106666 -0.05333,-0.266666 -0.05333,-0.373333 z m 0.16,2.879998 v -0.586666 c 0.05333,0.213333 0.05333,0.373333 0.05333,0.586666 z M 104.9135,70.160282 c 0,0.05333 0.0533,0.106666 0.10667,0.159999 l -0.10667,0.05333 h -0.0533 l -0.0533,-0.05333 z m -0.69333,-9.066662 c 0,0.16 0.10667,0.32 -0.0533,0.48 0,-0.16 0,-0.32 0.0533,-0.48 z m -13.973323,4.159998 c 0,-0.106667 0,-0.213333 0.05333,-0.32 l 0.05333,0.16 c 0,0.106667 -0.05333,0.16 -0.106666,0.16 z m 0.319999,-2.933332 c 0,0.16 0,0.373333 -0.05333,0.533333 v -0.533333 z m 0,-10.18666 v 0.106666 l 0.05333,0.106667 -0.05333,0.05333 v -0.16 l -0.106666,0.05333 v -0.05333 z m 14.079994,11.466659 c 0,-0.106666 0,-0.159999 0.0533,-0.266666 v 0.32 z m -2.13333,-11.893326 c 0,-0.106666 0,-0.16 0.0533,-0.266666 0,0.106666 0,0.16 0.0533,0.266666 z M 90.46018,69.840282 c 0,-0.106667 0,-0.213333 0.05333,-0.32 v 0.213333 z m 0.32,-1.333333 c 0,0.05333 0.05333,0.106667 0.05333,0.106667 0,0.05333 -0.05333,0.106666 -0.05333,0.106666 l -0.05333,-0.05333 c 0,-0.05333 0,-0.106666 0.05333,-0.16 z m -1.386666,-3.466664 c -0.05333,-0.106667 -0.05333,-0.266667 -0.05333,-0.373333 0.05333,0.106666 0.05333,0.266666 0.05333,0.373333 z m 14.933326,0.586666 0.0533,-0.106667 v 0.266667 z m -0.21333,-5.119997 c -0.0533,-0.16 -0.0533,-0.266667 0,-0.373333 z m 0,4.746664 h 0.0533 v 0.16 l -0.0533,-0.05333 z m -13.386664,6.293329 c 0,0.05333 0,0.16 0.05333,0.213334 0,-0.05333 0,-0.16 -0.05333,-0.213334 z m -0.213333,-14.773324 0.05333,0.106666 c 0,0.05333 0,0.05333 -0.05333,0.106667 z m -1.333332,-2.186665 c -0.05333,-0.05333 -0.05333,-0.16 0,-0.266667 z m 15.199989,11.946659 c -0.0533,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.0533,-2.613332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 0,-0.05333 0.0533,-0.05333 0.0533,-0.05333 z m -0.21333,-3.199998 -0.0533,-0.05333 0.0533,-0.106667 z m 0.16,5.33333 c -0.0533,-0.05333 -0.0533,-0.16 0,-0.213333 z m -1.44,-13.173325 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0.0533,0.05333 0.0533,0.106667 0.0533,0.16 z m -13.599989,0 c 0.05333,0.05333 0.05333,0.106666 0.05333,0.16 -0.05333,-0.05333 -0.05333,-0.106667 -0.05333,-0.16 z m 14.773329,8.373328 -0.0533,-0.106666 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m -13.599997,7.679996 v -0.106667 h 0.05333 z m -1.119999,-4.906664 v -0.106667 z m 1.173332,1.973332 v 0.106667 z m 13.119994,-8.533328 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -14.613326,-0.16 v -0.05333 h 0.05333 v 0.05333 z m 1.493332,-0.693333 c 0,0 -0.05333,-0.05333 -0.05333,-0.05333 0,0 0.05333,-0.05333 0.05333,-0.05333 z m 13.599994,7.413329 v -0.05333 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106666 z M 89.446847,63.653619 h -0.05333 v -0.05333 z m 15.253323,-0.533333 h -0.0533 l 0.0533,-0.05333 z m -13.91999,4.959997 0.05333,0.05333 h -0.05333 z m 13.38666,-7.146663 0.0533,-0.05333 v 0.05333 z M 91.366846,71.653614 h 0.05333 l -0.05333,0.05333 z M 104.06017,61.57362 v -0.05333 h 0.0533 z m 0.0533,-1.546666 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -14.719996,-5.33333 0.05333,0.05333 h -0.05333 z m 15.626656,15.626657 v -0.05333 l 0.0533,0.05333 z m -2.02666,-15.306657 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z m -12.479997,6.826663 0.05333,-0.05333 v 0.05333 z M 103.63351,60.93362 c 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 1.33333,8.906662 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path920" />
+    <path
+       d="m 126.07266,60.666954 c 0,-0.266667 -0.16,-0.586666 -0.48,-0.586666 -0.58667,0 -0.64,0.693332 -0.69333,1.119999 -0.42667,2.506665 -2.77334,7.306662 -5.70667,7.306662 -1.76,0 -1.97333,-0.479999 -3.14666,-1.706665 -0.32,-0.96 -0.96,-1.919999 -0.96,-2.986665 v -0.106667 c 0.37333,-0.106667 0.69333,-0.373333 1.01333,-0.586666 1.97333,-1.12 3.94667,-2.613332 3.94667,-5.119997 0,-1.706666 -1.44,-3.413331 -3.2,-3.413331 -4.16,0 -5.6,2.773331 -6.29333,6.293329 -0.10667,0.533333 -0.32,1.173333 -0.32,1.706666 0,1.706665 0.53333,3.253331 1.01333,4.85333 0.16,0.64 2.08,2.506665 2.66667,2.879998 0.8,0.533333 2.02666,0.8 2.98666,0.8 0.64,0 2.82667,-0.266667 3.30667,-0.746666 3.30666,-1.066666 5.86666,-6.45333 5.86666,-9.706661 z m -9.28,-4.213331 c 0.74667,0 1.33334,0.746666 1.33334,1.439999 0,1.973332 -1.65334,3.306665 -3.25333,4.159998 -0.0533,-0.213333 -0.10667,-0.96 -0.10667,-1.173333 v -0.64 c 0,-1.226665 0.42667,-3.786664 2.02666,-3.786664 z m -3.67999,4.266664 c 0,-0.16 0,-0.32 -0.0533,-0.48 v 0.48 l 0.0533,0.05333 v 0.266667 c -0.0533,-0.106667 -0.0533,-0.213333 -0.0533,-0.32 v 0.586667 l 0.0533,-0.106667 c 0.10666,0.213333 0.16,0.426666 0.16,0.64 0,0.159999 -0.0533,0.319999 -0.0533,0.426666 -0.0533,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.106667 0.0533,-0.426667 -0.10667,-0.48 -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 -0.10667,-0.586666 -0.16,-1.279999 -0.21333,-1.973332 -0.0533,0.426666 -0.0533,0.959999 -0.10667,1.013332 0,-0.16 -0.0533,-0.319999 -0.10667,-0.479999 l 0.0533,-0.106667 c -0.0533,0 -0.10667,-0.05333 -0.10667,-0.16 v -0.05333 h 0.16 v -0.213333 c 0,-0.106667 0,-0.266667 -0.0533,-0.373333 l -0.0533,0.213333 v -0.266667 l -0.0533,-0.106666 v -0.05333 l 0.16,-0.05333 c 0,-0.106667 0,-0.213333 -0.0533,-0.32 l 0.0533,-0.106666 c 0,0 0.0533,0.106666 0.0533,0.213333 0,-0.32 0.0533,-0.64 0.32,-0.906666 v 0.479999 l 0.0533,-0.05333 v 0.213333 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0.0533,0.213333 0.10666,0.426666 0.10666,0.639999 0,0.32 0,0.906666 -0.10666,1.226666 z m 1.54666,7.626662 c -0.0533,0.16 -0.0533,0.32 -0.10666,0.48 l -0.16,-0.16 c 0,-0.373333 0.0533,-0.853333 0.16,-1.173333 0.0533,0.213334 0.10666,0.586667 0.10666,0.8 L 114.766,68.02695 v -0.213334 c 0.16,0.16 0.32,0.32 0.42667,0.48 -0.0533,0.16 -0.0533,0.373333 -0.21334,0.533333 0,0.106667 0.0533,0.213333 0.0533,0.266667 l -0.10667,-0.05333 v -0.479999 c -0.0533,0.159999 -0.0533,0.319999 -0.0533,0.479999 L 114.766,68.933616 c 0.0533,-0.106667 0.0533,-0.16 0.0533,-0.266667 l -0.0533,-0.05333 -0.0533,0.16 c 0,-0.16 0,-0.32 -0.0533,-0.426667 z m -1.76,-1.439999 c -0.0533,-0.32 -0.0533,-0.693333 -0.0533,-1.013333 v -1.866665 l -0.0533,0.106666 v -0.106666 l -0.0533,-0.106667 h 0.10667 v -0.639999 l 0.0533,-0.106667 c 0,0.64 0.0533,1.226666 0.0533,1.866666 v 1.013332 c 0,0.266667 0,0.586667 -0.0533,0.853333 z m 1.17334,1.066666 c 0,-0.106666 0,-0.16 0.0533,-0.266666 v -0.48 l -0.0533,0.05333 c 0,-0.266666 0.0533,-0.479999 0.0533,-0.746666 0.0533,0.05333 0.16,0.32 0.16,0.373333 v 0.64 c 0,0.213333 -0.0533,0.48 -0.10667,0.693333 z M 113.006,59.546955 c 0.0533,-0.16 0,-0.32 -0.10667,-0.48 v 0.213333 c 0,0.32 0,0.64 0.0533,0.959999 0.0533,-0.106666 0.10666,-0.159999 0.10666,-0.266666 l 0.0533,0.16 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,-0.16 -0.10667,-0.373333 -0.10667,-0.533333 z m -0.32,3.519997 c -0.0533,-0.159999 -0.10667,-1.013332 0,-1.173332 z m 0.90667,-5.49333 c 0,0.16 0,0.32 -0.0533,0.426667 l -0.0533,0.05333 v -0.373333 z m -1.49334,6.346663 c 0.0533,0.32 0.0533,0.586667 0.0533,0.906666 l -0.0533,-0.106666 z m 0.69334,-3.999997 c 0,-0.16 0,-0.32 0,-0.426667 0,-0.05333 0,-0.266666 -0.0533,-0.426666 0,0.266666 0,0.533333 0.0533,0.853333 z M 112.206,61.41362 c -0.0533,-0.16 -0.0533,-0.32 -0.0533,-0.48 l 0.0533,-0.106666 z m 0.32,-0.64 c 0.0533,0.106667 0.0533,0.266667 0,0.373334 l -0.0533,-0.213334 z m 0.10667,1.12 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 v 0.32 z m -0.74667,0.426666 c 0,0.106667 0.0533,0.213333 0,0.32 z m 2.08,4.586664 -0.0533,-0.106666 0.0533,-0.05333 z m 0,0.373333 -0.0533,-0.106666 0.0533,-0.05333 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path922" />
+    <path
+       d="m 134.04765,67.86695 c 1.17334,-0.64 1.92,-1.866666 2.4,-3.093332 1.38667,-0.533333 4.69333,-2.399998 4.69333,-4.106664 0,-0.213333 -0.21333,-0.426667 -0.42666,-0.426667 -0.32,0 -0.58667,0.16 -0.74667,0.426667 -0.8,1.226666 -1.92,2.239999 -3.30667,2.773332 0,-2.079999 -0.63999,-4.159998 -2.13333,-5.65333 -0.48,-0.48 -2.50666,-1.706666 -3.2,-1.706666 -0.26666,0 -0.58666,0 -0.85333,0 -0.96,0 -1.97333,0.05333 -2.77333,0.586666 -0.42667,0.266667 -0.53333,0.586667 -0.85333,0.906666 -1.44,1.599999 -2.18667,2.133332 -2.18667,4.533331 0,3.946664 3.09333,6.45333 6.82666,6.45333 0.32,0 2.24,-0.533333 2.56,-0.693333 z m -4.95999,-9.386661 c 0,-0.373334 0.26666,-1.44 0.8,-1.44 2.29333,0 2.93333,4.906664 3.14666,6.506663 -1.97333,-0.746666 -2.88,-1.706665 -3.52,-3.733331 -0.10666,-0.426666 -0.42666,-0.906666 -0.42666,-1.333332 z m 2.55999,8.159995 c -1.65333,0 -1.91999,-2.826665 -2.34666,-3.946665 1.22667,1.546666 1.86666,1.706666 3.68,2.293332 -0.21334,0.746667 -0.32,1.653333 -1.33334,1.653333 z m 2.98667,-2.986665 c 0,-0.426667 -0.10667,-0.8 -0.10667,-1.226666 v -0.106667 c 0.10667,-0.106666 0.0533,-0.799999 0,-0.906666 h -0.0533 v 0.05333 l -0.0533,0.05333 c 0.0533,-0.16 0.10666,-0.373333 0.16,-0.533333 h 0.10666 c 0.0533,0.373333 0.10667,0.746666 0.10667,1.066666 0,0.213333 -0.0533,0.426666 -0.10667,0.586666 0,0.373333 0.10667,0.693333 -0.0533,1.013333 z m -7.2,0.106666 c 0.21334,0.213334 0.16,0.64 0.16,0.906667 v 0.853332 c -0.10666,-0.373333 -0.16,-0.799999 -0.16,-1.226666 0.10667,-0.106666 -0.0533,-0.319999 -0.0533,-0.426666 0,-0.05333 0.0533,-0.106667 0.0533,-0.106667 z m 7.25333,1.066666 c 0,-0.213333 0.0533,-1.333332 0.10667,-1.439999 0,0.05333 0.0533,0.106667 0.0533,0.16 0,-0.05333 0,-0.05333 0.0533,-0.106666 v 0.213333 c 0,0.373333 -0.0533,0.693333 -0.10667,1.013333 z m 0.26667,-0.693333 c 0,-0.266666 0.0533,-0.533333 -0.0533,-0.799999 0.10666,-0.426666 0.0533,-0.959999 0.0533,-1.386666 0.0533,0.213333 0.10667,0.426667 0.10667,0.64 0,0.373333 0,1.386666 -0.10667,1.546665 z m -0.16,-0.799999 v -0.05333 l 0.0533,0.05333 z m -0.21333,-2.079999 0.0533,0.05333 v -0.05333 z m 0.0533,1.439999 v -0.106666 z m -7.14666,-0.639999 v -0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path924" />
+    <path
+       d="m 155.71431,60.400287 c 0,-1.653332 -0.21334,-5.386663 -0.8,-6.879996 -0.16,-0.373333 0,-0.853332 -0.16,-1.226665 -0.26667,-0.8 -0.42667,-1.599999 -0.74667,-2.399999 -1.17333,-2.986665 -3.46666,-8.159995 -7.30666,-8.159995 -1.44,0 -3.30666,0.746666 -4,2.133332 -0.26666,-0.266667 -0.69333,-0.533333 -1.06666,-0.533333 -1.97334,0 -1.86667,0.906666 -1.86667,2.346665 0,0.426667 -0.10667,0.853333 -0.10667,1.279999 v 11.679993 c 0,0.853333 0.16,1.706666 0.21334,2.559999 0.16,2.613332 0.26666,5.119997 0.58666,7.733329 -0.26666,0.16 -0.48,0.426666 -0.48,0.746666 0,0.48 0.37334,0.533333 0.69334,0.799999 0.42666,1.333333 -0.16,1.653333 1.33333,2.239999 0.37333,0.16 0.8,0.32 1.22667,0.32 1.43999,0 2.61333,-0.32 2.61333,-2.026666 6.34666,-0.799999 9.86666,-4.21333 9.86666,-10.613327 z m -9.81333,9.013328 c 0,-2.079998 0.0533,-4.21333 -0.0533,-6.293329 -0.0533,-0.586667 -0.16,-1.12 -0.16,-1.706666 0,-2.933331 -0.64,-5.919996 -0.64,-8.853328 0,-1.546666 -0.21334,-3.093331 -0.21334,-4.639997 0,-1.226666 -0.0533,-4.853331 1.70667,-4.853331 0.64,0 1.12,0.64 1.44,1.173333 2.24,3.679998 3.25333,10.399994 3.30666,14.613325 0,0.426666 0.16,0.906666 0.16,1.333332 0,3.039998 -0.53333,6.45333 -3.14666,8.266662 -0.53333,0.373333 -0.53333,0.586666 -1.17333,0.746666 -0.42667,0.106667 -0.85334,0.05333 -1.22667,0.213333 z m -4.42666,-6.879996 c 0,-1.333332 -0.21334,-2.613331 -0.21334,-3.946664 v -4.85333 c 0.37334,0.639999 0.37334,3.679997 0.37334,4.479997 0,1.226666 -0.0533,2.506665 -0.10667,3.733331 h 0.0533 v 0.48 z m 2.4,-4.266664 v 1.066666 l -0.0533,0.05333 h 0.0533 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.319999 -0.16,0.319999 0,-0.266666 -0.0533,-0.533333 -0.0533,-0.799999 0,-0.853333 0.26667,-2.079999 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.10666,-0.213333 0.10666,-0.479999 0.16,-0.693333 v 0.106667 l 0.0533,0.106667 c 0,0.693332 0.16,1.493332 0.16,2.186665 l -0.0533,0.05333 h 0.0533 c 0,0.106667 0.0533,0.16 0.0533,0.266667 v 0.373333 l -0.10666,0.106666 0.0533,-0.159999 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 l -0.0533,-0.16 z m -2.13334,6.77333 0.0533,0.05333 v -0.05333 c 0.0533,0.106666 0.0533,0.213333 0.16,0.319999 -0.16,-0.586666 0.0533,-1.119999 0.0533,-1.653332 l 0.0533,-2.666665 c 0.10667,0.106667 0.10667,0.32 0.10667,0.48 0,0.16 -0.0533,0.373333 0,0.533333 0.16,-0.32 0.0533,-0.426667 0.0533,-0.746666 0.0533,0.426666 0.0533,0.906666 0.0533,1.333332 0,1.013333 -0.16,2.079999 -0.16,3.093332 l -0.0533,0.05333 h -0.0533 l -0.10667,-0.16 v 0.426666 l -0.0533,-0.266666 -0.0533,0.16 -0.0533,-0.106667 z m -0.42666,-12.10666 c -0.16,-1.493332 0,-3.306665 0,-4.799997 l -0.0533,0.05333 v -0.479999 c 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 l 0.10666,-0.373333 c 0,0.586667 -0.16,1.066666 0.0533,1.653333 0.0533,-0.16 0.0533,-0.266667 0.0533,-0.426667 l 0.0533,0.05333 c 0,0.159999 0,0.319999 0.0533,0.479999 -0.0533,0.106667 -0.0533,0.16 -0.0533,0.266667 l -0.0533,-0.213333 -0.0533,0.106666 c 0,0.106667 0,0.213333 0.0533,0.32 -0.10667,0.05333 0,0.426666 0,0.533333 0,-0.106667 0.0533,-0.32 0.10667,-0.426666 0,0.159999 0,0.373333 0.0533,0.533333 -0.0533,0.16 -0.0533,0.373333 -0.0533,0.533333 l -0.10667,-0.373333 c -0.0533,0.266666 -0.0533,0.586666 -0.0533,0.853332 v 0.05333 h 0.0533 l 0.0533,-0.106667 c 0,0.05333 0,0.16 0.0533,0.213333 -0.0533,0.16 -0.0533,0.266667 -0.0533,0.426667 l -0.0533,0.106666 -0.0533,-0.266666 c -0.10667,0.479999 -0.0533,0.959999 -0.0533,1.439999 l -0.10666,-0.373333 -0.0533,0.32 v -0.32 z m 12.58666,8.426662 c -0.21334,-0.906666 0,-2.079999 -0.26667,-2.879998 0.10667,-0.533333 0.0533,-1.013333 0.0533,-1.546666 0.16,0.959999 0,1.973332 0.16,2.933331 0,-0.106666 0.0533,-0.213333 0.0533,-0.319999 0,-0.48 -0.10667,-1.013333 -0.10667,-1.546666 0,-0.05333 0,-0.266667 0.0533,-0.266667 0.0533,0.746667 0.10667,1.493333 0.10667,2.239999 0,0.213333 -0.0533,0.48 -0.0533,0.693333 l -0.0533,-0.05333 c 0.0533,0.213333 0.0533,0.479999 0.0533,0.746666 z m -0.74667,2.986665 c -0.0533,-0.373333 -0.10667,-0.746667 -0.10667,-1.12 v -0.106666 l 0.0533,-0.05333 h -0.0533 V 61.89362 c 0,-0.373333 0,-0.693333 0.0533,-1.013333 0,0.853333 0.16,1.759999 0.16,2.613332 0,0.213333 0.0533,0.693333 -0.10667,0.853333 z m -1.22667,-15.253325 c 0.16,0.373334 0.21334,0.533333 0.21334,0.96 v 1.013333 c 0,0.16 0,0.319999 -0.0533,0.479999 -0.10666,-0.266666 -0.21333,-0.639999 -0.21333,-0.959999 v -0.8 l 0.0533,0.16 z m 0.53334,4.426664 c -0.0533,-0.266666 -0.16,-0.799999 -0.16,-1.066666 v -2.559998 c 0.0533,0.106667 0.16,0.373333 0.16,0.426666 z m 1.17333,4.959998 c 0,0.426666 -0.0533,0.853332 -0.0533,1.279999 -0.10667,-0.693333 -0.16,-1.386666 -0.16,-2.079999 0,-0.213333 0,-0.8 0.21333,-0.959999 -0.0533,0.586666 0,1.173332 0,1.759999 z m -0.74667,-6.879996 c 0,0.213333 -0.0533,0.479999 -0.16,0.693333 0.0533,0.159999 0.0533,0.319999 0.0533,0.479999 l -0.10667,0.16 c -0.0533,-0.799999 -0.10667,-1.546666 -0.10667,-2.346665 0.16,0.266666 0.16,0.64 0.26667,0.959999 z m -11.62666,-4.586664 c 0,-0.693333 0.10667,-1.333333 0.10667,-2.026666 l 0.10667,-0.05333 0.10666,0.16 c -0.0533,0.639999 -0.0533,1.653332 -0.32,2.239999 z m 2.13334,5.119997 -0.0533,-0.213334 c -0.0533,0.16 0,0.266667 0,0.426667 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493333 -0.16,-1.813332 0,-0.106667 0.0533,-0.213334 0.0533,-0.32 v 0.266666 c 0,-0.32 -0.0533,-1.439999 0.10666,-1.706665 0,0.853332 0.0533,1.653332 0.0533,2.506665 z m 0.53333,13.439992 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.64 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373333 0.10667,0.746667 0.10667,1.12 z m 9.75999,-9.226662 c 0,-0.213333 0,-0.479999 0.0533,-0.639999 0,0.48 0.10666,0.959999 0.10666,1.493332 0,0.16 0,0.32 0,0.48 0,-0.32 -0.10666,-0.64 -0.10666,-0.959999 l -0.0533,0.159999 v -0.213333 h 0.0533 l -0.0533,-0.266666 v 0.106666 c 0,-0.05333 0,-0.106666 0,-0.16 z m -11.78666,5.65333 c -0.0533,-0.319999 -0.0533,-0.639999 -0.0533,-0.959999 v -0.373333 c 0.0533,0.106666 0.10667,0.266666 0.10667,0.373333 0,0.32 -0.0533,0.64 -0.0533,0.959999 z m 0.53334,4.053331 c -0.0533,-0.106666 -0.0533,-0.266666 -0.0533,-0.373333 l -0.0533,0.106667 h -0.10667 c 0,-0.16 0.0533,-0.373333 0.10667,-0.533333 h 0.16 c -0.0533,0.32 0,0.586666 0,0.853333 z m 11.14666,-4.53333 c 0.0533,0.426666 0.16,0.853333 0.26666,1.226666 l -0.10666,0.373333 c -0.0533,-0.373333 -0.16,-0.853333 -0.16,-1.226666 z m -1.22667,4.799997 c -0.10667,0.05333 -0.10667,0.16 -0.16,0.266666 v -0.639999 l 0.10667,-0.266667 c 0,0.213333 0.0533,0.426667 0.0533,0.64 z m 0.10667,-1.333333 c 0,0.373333 0.0533,0.746667 0.0533,1.12 -0.0533,0 -0.0533,0.05333 -0.0533,0.106666 -0.10667,-0.32 -0.0533,-0.799999 -0.0533,-1.119999 z m -9.44,-15.146657 c 0.10667,0.479999 0.10667,0.959999 0,1.386666 z m 10.61333,5.01333 c 0.0533,0.213333 0.10667,0.373333 0.10667,0.586666 v 0.16 l -0.10667,0.05333 c 0,-0.266667 0,-0.586667 -0.0533,-0.853333 z m -0.0533,5.066664 c 0.0533,0.373333 0.10666,0.906666 0.10666,1.279999 l -0.0533,-0.106667 -0.0533,0.106667 v -0.533333 0.05333 z m -0.10667,1.119999 c -0.10667,-0.266667 -0.0533,-0.853333 -0.0533,-1.119999 0.0533,0.213333 0.10666,0.906666 0.0533,1.119999 z m -0.26667,3.893331 -0.10666,0.32 v -0.16 c 0,-0.16 0,-0.32 0.0533,-0.426666 z m -10.77332,1.866666 c 0,-0.05333 -0.10667,-0.213334 -0.10667,-0.266667 0,-0.106667 0.0533,-0.213333 0.10667,-0.32 z m -0.85334,-3.306665 c -0.0533,-0.266667 -0.0533,-0.533333 0,-0.8 z m 11.41333,-1.279999 v 0.693333 c -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 z m -9.65333,-9.653328 v 0.106667 l 0.10667,-0.05333 v 0.05333 l -0.0533,0.213333 -0.10667,-0.05333 c 0.0533,-0.106667 0,-0.16 0,-0.266667 z m 10.61333,5.279997 v -0.693333 c 0.0533,0.213333 0.0533,0.586666 0.0533,0.8 z m -2.98667,-10.559994 0.0533,-0.213333 0.0533,0.05333 v 0.373333 z m -9.49332,0.853333 c 0.10666,0.106667 0.0533,0.426666 0.0533,0.586666 -0.0533,-0.213333 -0.0533,-0.373333 -0.0533,-0.586666 z m 0,4.85333 c 0,-0.213333 0,-0.426666 0.0533,-0.586666 0,0.213333 0.0533,0.426667 -0.0533,0.586666 z m 0.69333,15.893324 c 0,0.16 -0.0533,0.32 0,0.48 0,-0.16 0.0533,-0.32 0,-0.48 z m -0.53333,-7.946662 c 0,0.213334 0,0.48 -0.0533,0.693333 0,-0.213333 0,-0.479999 0.0533,-0.693333 z m 0.58666,1.653333 c 0,-0.106667 -0.0533,-0.106667 -0.10666,-0.16 v 0.32 z m 1.49334,-0.48 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m -1.49334,6.186663 c -0.0533,0.213333 -0.0533,0.373333 -0.0533,0.586666 0.0533,-0.213333 0.0533,-0.373333 0.0533,-0.586666 z m 1.76,-0.586666 v -0.05333 l 0.0533,-0.106666 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213333 -0.0533,0.32 z m -1.44,-0.853333 c 0,-0.16 -0.0533,-0.266667 -0.0533,-0.426667 0.10667,0.106667 0.10667,0.266667 0.0533,0.426667 z m -0.37333,-4.639997 c 0.0533,0.05333 0.10667,0 0.0533,-0.05333 -0.0533,0.05333 -0.10666,0.16 -0.10666,0.213334 h 0.10666 z m 10.82666,-2.826665 c -0.0533,-0.106667 -0.0533,-0.213334 -0.0533,-0.32 l 0.0533,-0.106667 z m 0.53333,3.733331 c 0,-0.16 -0.0533,-0.266667 0,-0.426667 0,0.16 0.0533,0.266667 0,0.426667 z m -10.71999,7.146662 -0.0533,0.106667 0.0533,0.16 z m 10.18666,-8.959994 -0.0533,0.05333 v -0.213333 l 0.0533,-0.05333 z m 0.58667,-4.906664 c -0.0533,-0.106667 -0.0533,-0.213333 0,-0.32 0.0533,0.106667 0,0.213333 0,0.32 z m -11.52,9.599994 v 0.16 l -0.0533,0.05333 0.0533,-0.16 c -0.0533,0.05333 -0.10666,0 -0.10666,-0.05333 z m 0.64,2.026666 c 0.0533,-0.106667 0.0533,-0.213334 0,-0.32 z m 1.17334,-1.6 c 0,0 -0.0533,-0.05333 -0.0533,-0.106666 0,0 0.0533,-0.05333 0.0533,-0.106667 l 0.0533,0.106667 c 0,0.05333 0,0.05333 -0.0533,0.106666 z m 9.11999,-4.53333 v 0.16 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.16 z m 0,1.439999 h -0.0533 v -0.16 h 0.0533 z m -9.22666,1.013333 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.10667,-4.053331 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.53333,-7.093329 c 0,-0.05333 0,-0.16 0.0533,-0.213334 0,0.05333 0,0.16 -0.0533,0.213334 z m -1.22667,12.906659 c 0.0533,0.106666 0.0533,0.213333 0,0.266666 z m 11.57333,-11.359994 c 0,0.106667 0.0533,0.213334 0,0.32 z m -10.13333,15.039991 c 0,0.05333 0,0.106667 -0.0533,0.16 v -0.106666 z m 0.37334,-10.50666 0.0533,-0.05333 -0.0533,-0.106667 z m -1.12,8.533328 -0.0533,-0.106666 v 0.16 z m 1.06666,3.573332 v -0.16 l 0.0533,0.16 z m 9.06666,-17.813323 v 0.16 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.106667 z m -9.54666,-6.026663 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m -1.01333,15.573324 c 0,-0.05333 0,-0.106667 -0.0533,-0.16 0,0.05333 0,0.106666 0.0533,0.16 z m 1.01333,-14.986658 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 9.92,16.426657 0.0533,-0.106667 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.37333,-4.853331 c 0,0 -0.0533,0.05333 -0.10667,0.05333 l 0.0533,-0.05333 z m -11.67999,5.546664 c 0,0 -0.0533,0.05333 -0.0533,0.05333 0,0 0.0533,0.05333 0.0533,0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,-2.933332 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 0.90667,3.093332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -0.16,1.706665 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m 0.96,-16.47999 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -1.70667,13.333326 v -0.106667 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106667 z m 1.81333,6.186663 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.53334,-3.573332 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.90667,2.133332 c 0.0533,0 0.0533,0.05333 0.0533,0.106667 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106667 z m -1.28,-8.479995 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.69333,5.97333 h -0.0533 v -0.05333 z m -0.8,-16.906656 h -0.0533 v 0.05333 z m 2.08,11.359993 0.0533,-0.05333 v 0.05333 z m -0.0533,10.18666 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106666 z M 143.50098,55.76029 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m -1.97333,-3.999997 v 0.05333 l 0.0533,-0.05333 z m 2.13333,8.959994 0.0533,0.05333 h -0.0533 z m -0.90666,7.519996 -0.0533,0.05333 h 0.0533 z m 11.14666,-8.266662 c -0.0533,0.05333 -0.0533,0.05333 -0.0533,0.106667 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106667 z m -12.58666,-9.919994 h 0.0533 v -0.05333 z m 0.53333,10.133327 c 0,0 0.0533,0.05333 0.0533,0.05333 0,0 -0.0533,0.05333 -0.0533,0.05333 z m 1.12,8.213329 h -0.0533 v -0.05333 z m 10.45333,-8.959995 h -0.0533 l 0.0533,-0.05333 z m 0,0.106667 0.0533,0.05333 h -0.0533 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path926" />
+    <path
+       d="m 171.9068,62.533619 c 0,-3.199998 -2.82667,-4.586663 -5.38667,-5.439996 1.65334,-1.653333 2.82667,-3.999998 2.82667,-6.399996 0,-1.226666 0.0533,-2.719999 -0.58667,-3.839998 -0.90666,-1.546666 -2.08,-3.039998 -4.10666,-3.039998 -1.01333,0 -1.86667,0.319999 -2.61333,0.959999 -0.21334,-1.546666 0.10666,-3.039998 -2.08,-3.039998 -1.81333,0 -1.97333,0.906666 -1.97333,2.453332 0,1.813332 0.16,3.679998 0.16,5.546663 0,3.039998 0.16,6.079996 0.16,9.173328 0,1.919999 0.21333,3.839998 0.21333,5.759997 0,0.586666 0.16,1.119999 0.16,1.706665 0,0.16 -0.0533,0.266667 -0.10667,0.426667 -0.53333,0.159999 -0.74666,0.586666 -0.74666,1.119999 0,0.426666 0.16,1.173333 0.69333,1.226666 l 0.16,-0.05333 v 0.266666 c 0,1.013333 -0.37333,2.773332 0.8,3.253331 0.37333,0.16 0.85333,0.426667 1.22666,0.426667 0.8,0 3.2,-0.106667 3.2,-1.279999 -0.32,-0.693333 0,-1.546666 -0.21333,-2.239999 3.84,0 8.21333,-2.773332 8.21333,-6.986663 z m -7.62667,-4.426664 c 2.24,0 5.06667,1.546666 5.06667,4.053331 0,2.826665 -3.52,4.319998 -5.81333,4.639998 -0.10667,-0.8 -0.26667,-1.653333 -0.26667,-2.453332 0,-1.546666 -0.10666,-4.693331 -0.32,-6.186663 0.16,-0.05333 0.32,-0.05333 0.48,-0.05333 z M 164.8668,45.25363 c 1.33333,0 1.97333,1.173332 1.97333,2.453332 0,3.199998 -1.70666,6.559996 -3.94666,8.799994 -0.21333,-0.426666 -0.53333,-6.719996 -0.53333,-7.413329 0,-1.386665 0.37333,-2.133332 1.33333,-3.093331 0.21333,-0.213333 0.85333,-0.746666 1.17333,-0.746666 z m -3.62666,14.506658 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.32 -0.16,0.32 0,-0.266667 -0.0533,-0.533333 -0.0533,-0.8 0,-0.959999 0.26667,-1.973332 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.21333,-0.373333 0.10666,-0.853332 0.21333,-1.279999 0,0.16 0,0.32 0,0.48 l 0.0533,-0.106667 c 0.0533,0.48 0.0533,0.96 0.0533,1.439999 l 0.0533,0.05333 0.0533,-0.213334 c 0.10667,0.533333 0.16,1.013333 0.16,1.546666 -0.0533,0.213333 0.0533,0.693333 0.0533,0.906666 l -0.0533,-0.05333 c 0.0533,0.106667 0.0533,0.373333 0.0533,0.533333 -0.0533,-0.213333 -0.10667,-0.426666 -0.21334,-0.586666 0.10667,-0.32 0.16,-1.599999 -0.0533,-1.813333 0,0.373334 0,0.693333 -0.0533,1.066667 l 0.0533,-0.213334 c 0.0533,0.213334 0.0533,0.426667 0.0533,0.64 l -0.10666,0.106667 0.0533,-0.16 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 v 0.853332 l -0.0533,0.05333 z m -0.32,-8.693328 c -0.0533,0.479999 0.0533,0.906666 0.0533,1.386665 l 0.0533,0.106667 v 0.8 l -0.0533,0.05333 v -0.533333 c -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 l -0.0533,0.05333 c 0,-0.373333 -0.0533,-0.906666 0.0533,-1.279999 h -0.0533 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 l -0.0533,-0.05333 c -0.16,0.16 -0.10666,0.586667 -0.10666,0.8 l -0.0533,-0.213333 v 0.426666 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493332 -0.16,-1.813332 0,-0.48 0.10667,-0.96 0.10667,-1.439999 l 0.0533,0.106666 0.0533,-0.106666 c 0,0.479999 0.16,0.853332 0.16,1.279999 v 0.266666 l -0.0533,0.05333 z m -0.64,20.159988 c 0,-0.32 -0.0533,-0.586667 -0.0533,-0.906667 v -0.479999 l 0.0533,-0.106667 v 0.213333 l 0.0533,-0.106666 v -0.106667 l 0.0533,0.05333 v 0.853333 c 0.0533,-0.05333 0.0533,-0.16 0.0533,-0.213333 0.0533,0.32 0.0533,0.586666 0.0533,0.906666 z m 1.01333,-5.279997 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.639999 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373334 0.10667,0.746667 0.10667,1.12 z m -1.28,1.813332 c 0,0.16 0.26667,1.119999 0.32,1.173333 l -0.0533,0.106666 c 0,-0.213333 -0.10667,-0.533333 -0.21334,-0.693333 v -0.266666 0.266666 c -0.0533,-0.106666 -0.10666,-0.266666 -0.21333,-0.319999 l 0.10667,-0.213334 v -0.05333 z m 0.37333,-17.546656 c 0.10667,0.48 0.10667,0.959999 0,1.386666 z m 0.32,-1.386666 v 0.106667 h -0.0533 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0,-0.106667 0.10666,-0.16 0.16,-0.16 v 0.213333 z m -0.0533,4.373331 c 0.0533,-0.106667 0,-0.16 0,-0.266667 h 0.0533 v 0.106667 l 0.10667,-0.05333 -0.0533,0.266667 z m 0.69333,6.559996 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,0.05333 0.0533,0.159999 0.0533,0.213333 v 0.106666 l -0.10667,0.05333 z m 4.58667,-4.426664 c 0.0533,-0.213333 0.21333,-0.373333 0.32,-0.533333 v 0.106666 z m -4.85333,7.146662 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m 0.26666,5.546664 0.0533,-0.106667 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213334 -0.0533,0.32 l -0.0533,-0.05333 z m -0.32,-14.399992 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.213333 l 0.0533,0.05333 z m 0.26667,13.066659 c 0,0.05333 0,0.05333 -0.0533,0.106667 0,0 -0.0533,-0.05333 -0.0533,-0.106667 0,0 0.0533,-0.05333 0.0533,-0.106667 z m 0.10667,-6.45333 -0.0533,-0.213333 c 0.0533,0.05333 0.0533,0.106667 0.10667,0.16 z m -0.26667,-4.959997 c -0.0533,-0.106666 -0.0533,-0.213333 -0.0533,-0.319999 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 z m -0.48,-5.919996 c 0,-0.106667 0,-0.16 0.0533,-0.266667 0,0.106667 0,0.16 -0.0533,0.266667 z m 0.48,15.359991 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.64,-11.14666 c 0,-0.05333 0,-0.16 0.0533,-0.213333 0,0.05333 0,0.159999 -0.0533,0.213333 z m 0.53333,7.093329 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.0533,-6.45333 v -0.16 l 0.0533,0.05333 z m -0.26667,15.946658 c 0,0.05333 0,0.106666 -0.0533,0.159999 v -0.106666 z m 0.42667,-10.559994 -0.0533,-0.106667 v 0.16 z m -0.85333,8.639995 c 0,0 -0.0533,0 -0.0533,-0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.26666,-20.319988 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m 0,0.586666 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.16,22.026654 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0,-21.439988 v -0.106666 l 0.0533,0.16 z m 0.53334,17.866656 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.64,-15.946657 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,18.07999 c 0.0533,0 0.0533,0.05333 0.0533,0.106666 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106666 z m 0,0.373333 h -0.0533 v -0.106667 z m 0.53333,-13.333326 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m 0.21334,5.013331 h -0.0533 v -0.05333 z m 0.32,-1.813333 0.0533,0.05333 h -0.0533 z m -1.06667,9.439995 h -0.0533 v -0.05333 z m 0.74667,-7.733329 h -0.0533 l 0.0533,-0.05333 z m -0.10667,10.186661 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path928" />
+  </g>
+</svg>
diff --git a/common/static/img/logo_square.jpg b/common/static/img/logo_square.jpg
index 37ed7679cdf0eb96115865560d3386e3af8970c8..073cc571293d57ac64773cb0251cd4f38f4d2839 100644
GIT binary patch
literal 32105
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
ZLH)leC;mIeLH+-6<?dg(KGeUa{s*dci825H

literal 42490
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

diff --git a/common/static/img/logo_square.svg b/common/static/img/logo_square.svg
index ccc0ff8d..76251fed 100644
--- a/common/static/img/logo_square.svg
+++ b/common/static/img/logo_square.svg
@@ -1 +1,193 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><title>logo</title><path d="M2.46,18h.32l.55,1,.18.38h0a5.66,5.66,0,0,1,0-.6V18h.29v1.75H3.45l-.54-1-.18-.38h0c0,.19,0,.4,0,.59v.77h-.3Z" style="fill:#606c76"/><path d="M5.45,18h.31v1.75H5.45Z" style="fill:#606c76"/><path d="M7.34,18.88a.82.82,0,0,1,.8-.91.67.67,0,0,1,.5.22l-.16.2a.47.47,0,0,0-.34-.15c-.28,0-.48.24-.48.63s.18.64.48.64a.49.49,0,0,0,.37-.18l.17.2a.73.73,0,0,1-.56.25A.8.8,0,0,1,7.34,18.88Z" style="fill:#606c76"/><path d="M10.2,18h1.05v.26h-.74v.45h.63V19h-.63v.52h.77v.26H10.2Z" style="fill:#606c76"/><path d="M12.86,18h.47a.78.78,0,0,1,.85.87.78.78,0,0,1-.84.88h-.48Zm.45,1.5c.35,0,.55-.2.55-.63s-.2-.62-.55-.62h-.14V19.5Z" style="fill:#606c76"/><path d="M15.76,18h.56c.36,0,.62.11.62.43a.39.39,0,0,1-.24.38h0a.41.41,0,0,1,.34.42c0,.35-.29.51-.67.51h-.61Zm.53.72c.25,0,.35-.09.35-.24s-.11-.24-.34-.24h-.23v.48Zm0,.79c.26,0,.4-.09.4-.29s-.14-.26-.4-.26h-.26v.55Z" style="fill:#606c76"/><path d="M18.53,19.57a.2.2,0,1,1,.4,0,.2.2,0,1,1-.4,0Z" style="fill:#606c76"/><path d="M20.46,18.87c0-.57.31-.9.77-.9s.77.34.77.9-.32.91-.77.91S20.46,19.43,20.46,18.87Zm1.22,0c0-.39-.18-.63-.45-.63s-.45.24-.45.63.17.64.45.64S21.68,19.26,21.68,18.87Z" style="fill:#606c76"/><path d="M23.58,18h.59c.36,0,.64.13.64.52s-.28.55-.64.55h-.28v.68h-.31Zm.56.82c.24,0,.37-.1.37-.3s-.13-.27-.37-.27h-.25v.57Zm0,.16.23-.18.54.95h-.35Z" style="fill:#606c76"/><path d="M26.3,18.88a.83.83,0,0,1,.83-.91.72.72,0,0,1,.52.22l-.16.2a.5.5,0,0,0-.36-.15c-.31,0-.51.24-.51.63s.18.64.53.64a.44.44,0,0,0,.25-.07v-.38h-.33V18.8h.61v.77a.8.8,0,0,1-.56.21A.81.81,0,0,1,26.3,18.88Z" style="fill:#606c76"/><path d="M6.06,17H6l-3.61-.76a.18.18,0,0,1-.14-.18V10.78a.16.16,0,0,1,.07-.14.19.19,0,0,1,.15,0l3.6.76a.18.18,0,0,1,.14.18v5.27a.18.18,0,0,1-.06.14A.19.19,0,0,1,6.06,17ZM2.64,15.91l3.24.68V11.68L2.64,11Z" style="fill:#00a1cc"/><path d="M6.06,17A.15.15,0,0,1,6,17a.19.19,0,0,1-.07-.14V11.54A.18.18,0,0,1,6,11.36l3.6-.76a.2.2,0,0,1,.15,0,.19.19,0,0,1,.07.14v5.27a.18.18,0,0,1-.15.18L6.1,17Zm.18-5.31v4.91l3.24-.68V11Zm3.43,4.37Z" style="fill:#00a1cc"/><path d="M24.69,17a3.4,3.4,0,1,1,3.4-3.4A3.41,3.41,0,0,1,24.69,17Zm0-6.44a3,3,0,1,0,3,3A3,3,0,0,0,24.69,10.58Z" style="fill:#00a1cc"/><path d="M24.69,14.93A1.31,1.31,0,1,1,26,13.62,1.31,1.31,0,0,1,24.69,14.93Zm0-2.26a1,1,0,1,0,.95.95A.95.95,0,0,0,24.69,12.67Z" style="fill:#00a1cc"/><path d="M17.73,16H11.42a.18.18,0,0,1-.18-.18V11.39a.18.18,0,0,1,.18-.18h6.31a.18.18,0,0,1,.18.18v4.46A.18.18,0,0,1,17.73,16Zm-6.13-.36h6v-4.1h-6Z" style="fill:#00a1cc"/><path d="M19.94,16l-.08,0L17.65,14.9a.18.18,0,0,1-.1-.16V12.5a.18.18,0,0,1,.1-.16l2.21-1.11a.17.17,0,0,1,.17,0,.19.19,0,0,1,.09.15v4.46A.19.19,0,0,1,20,16Zm-2-1.4,1.85.92V11.69l-1.85.92Z" style="fill:#00a1cc"/></svg>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="220.10335"
+   height="220.69331"
+   viewBox="0 0 220.10334 220.69331"
+   data-svgdocument=""
+   id="_3tPmdtZQ0LGlljQGEkT-3"
+   class="fl-svgdocument"
+   x="0"
+   y="0"
+   version="1.1"
+   sodipodi:docname="logo-square.svg"
+   inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview21"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     units="px"
+     fit-margin-top="42"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="42"
+     inkscape:zoom="1.4707031"
+     inkscape:cx="282.51793"
+     inkscape:cy="94.172644"
+     inkscape:window-width="1792"
+     inkscape:window-height="1067"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="_3tPmdtZQ0LGlljQGEkT-3"
+     inkscape:document-units="mm"
+     inkscape:snap-global="false" />
+  <defs
+     id="_SabXoCmWUJmxvMwSINRGH"
+     transform="matrix(1.1728515134752755, 0, 0, 1.1728515134752755, -34.46172819025348, -40.976997984526726)">
+    <rect
+       x="86.353256"
+       y="25.158035"
+       width="217.58301"
+       height="60.515274"
+       id="rect4965" />
+  </defs>
+  <g
+     id="_WTrHMt3eZfxcQKrYJirRK"
+     transform="matrix(0.95294163,0,0,0.95294163,-45.741197,-148.14988)">
+    <path
+       id="_1MYZsbAYvxe5D-50H3pho"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.519653,192.79657)"
+       data-type="polygon"
+       d="M 82.1,74 75,81.1 v 0 L 82.1,74 74,65.9 v 0 z" />
+    <path
+       id="_oL3QJlwkbW1Nmdh3qAlDC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.45702,193.17499)"
+       data-type="rect"
+       data-x="64.5"
+       data-y="51.5"
+       data-width="0"
+       data-height="9.8"
+       d="M 64.5,51.5" />
+    <path
+       id="_sK1htog-8Op55rgRI5hDg"
+       d="m 61.1,52.9 v 0 0 l -4.9,4.9 c 2.5,1.2 4.3,3.8 4.3,6.9 0,4.2 -3.4,7.6 -7.6,7.6 -3,0 -5.6,-1.8 -6.9,-4.3 L 40.1,73.9 40,74 61,95 69.1,86.9 v 0 c -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 l 7.1,-7.1 -8.1,-8 v 0 l -0.6,-0.6 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 0,-1.5 -0.6,-2.8 -1.5,-3.9 -1,-0.9 -2.4,-1.5 -3.9,-1.5 -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 68,59.9 v 0 z"
+       fill="#083b66"
+       transform="matrix(1.2602785,0,0,1.2602785,27.51993,192.73364)"
+       style="fill:#ffb380" />
+    <path
+       id="_OMWHsyuaoclMfPE9tvM7V"
+       d="m 46.5,49.5 c -4.2,0 -7.6,-3.4 -7.6,-7.6 0,-3 1.7,-5.6 4.2,-6.8 l -5.9,-5.9 v 0 l -21,21 22.2,22.2 9.5,-9.5 c -0.3,0.6 -0.4,1.3 -0.4,2 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-2.9 -2.4,-5.3 -5.3,-5.3 -0.7,0 -1.4,0.1 -2,0.4 l 8.6,-8.6 -6,-6 c -1.3,2.3 -3.9,4.1 -6.9,4.1 z"
+       fill="#8dcaff"
+       transform="matrix(1.2602785,0,0,1.2602785,27.583487,192.79709)" />
+    <path
+       id="_jAk8zpl6O80_21e1JT4hk"
+       d="m 83.3,27.4 -8.7,-8.7 c 0.1,0 0.3,0 0.4,0 3.2,0 5.9,-2.6 5.9,-5.9 C 80.9,9.6 78.3,7 75,7 c -3.2,0 -5.9,2.6 -5.9,5.9 0,0.1 0,0.3 0,0.4 L 61,5.2 l -21,21 -0.7,0.8 -0.4,0.4 9.5,9.5 c -0.6,-0.2 -1.2,-0.3 -1.8,-0.3 -2.9,0 -5.3,2.4 -5.3,5.3 0,2.9 2.4,5.3 5.3,5.3 2.9,0 5.3,-2.4 5.3,-5.3 0,-0.5 -0.1,-0.9 -0.2,-1.4 0,-0.2 -0.1,-0.3 -0.1,-0.5 l 0.5,0.5 9.2,9.2 0.4,-0.4 0.8,-0.8 6.8,-6.8 c 0,0 0,0 0,0 -3.2,0 -5.9,-2.6 -5.9,-5.9 0,-3.3 2.6,-5.9 5.9,-5.9 3.2,0 5.9,2.6 5.9,5.9 0,0 0,0 0,0 z"
+       fill="#c0d9b4"
+       transform="matrix(1.2602785,0,0,1.2602785,27.709267,192.98645)" />
+    <path
+       id="_GDSYpBtYoW2GJpRZoPCot"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_NldltLD5fZQlQrgFsakaC"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.456932,192.67065)"
+       data-type="rect"
+       data-x="61.1"
+       data-y="52.9"
+       data-width="0"
+       data-height="0"
+       d="M 61.1,52.9" />
+    <path
+       id="_TbX_b0jxU8J0Cb3YWVDy6"
+       d="m 51.6,40.5 h 0.3 L 51.5,40 c 0,0.1 0,0.3 0.1,0.5 z"
+       fill="#000000"
+       transform="matrix(1.2602785,0,0,1.2602785,27.708767,192.98587)" />
+  </g>
+  <g
+     aria-label="NeoDB"
+     transform="matrix(1.5308318,0,0,1.7424549,-43.057047,-3.2641996)"
+     id="text4963"
+     style="font-size:40px;line-height:1.25;white-space:pre;shape-inside:url(#rect4965);fill:#083b66">
+    <path
+       d="m 107.58017,70.426948 c 0,-1.866665 -0.85333,-6.026663 -1.28,-7.786662 -0.10667,-0.426666 0,-0.853333 -0.10667,-1.279999 -0.90666,-3.786665 -1.44,-7.733329 -1.92,-11.573327 -0.15999,-1.546665 -0.58666,-3.039998 -0.58666,-4.586664 0,-0.266666 -0.10667,-1.919998 0.42667,-1.919998 0.74666,0 0.90666,1.226666 1.59999,1.226666 0.26667,0 0.64,-0.106667 0.64,-0.426667 0,-1.013333 -1.92,-1.866665 -2.82666,-1.866665 -3.09333,0 -2.93333,3.626664 -2.93333,5.919996 0,2.079999 0.32,4.373331 0.58666,6.45333 0.16,1.279999 0.10667,2.559998 0.32,3.839997 0.16,1.12 0.64,2.719999 0.64,3.839998 0,0.906666 0.16,1.706666 0.26667,2.559998 -0.48,-0.746666 -0.69333,-1.493332 -1.12,-2.239998 -0.64,-1.066666 -1.28,-2.186666 -1.813332,-3.306665 -0.373333,-0.746666 -0.8,-1.439999 -1.226666,-2.186665 -0.426667,-0.746667 -0.693333,-1.546666 -1.119999,-2.293332 -1.919999,-3.253332 -3.626665,-6.666663 -5.493331,-9.919994 -0.213333,-0.373333 -0.693332,-0.426667 -1.066666,-0.426667 -0.693332,0 -2.933331,0.533333 -3.039998,1.333333 -0.16,1.333332 -0.16,2.719998 -0.16,4.053331 v 4.53333 c 0,1.013333 0.106667,1.973332 0.106667,2.933332 0,1.386666 0.16,2.719998 0.16,4.053331 0,0.799999 0.16,1.546666 0.16,2.346665 0,1.333333 0.05333,2.826665 0.266666,4.159998 0.106667,0.533333 0.213333,1.279999 0.213333,1.866665 0,2.399999 -0.266666,4.053331 2.826665,4.053331 0.8,0 3.093332,-0.426666 3.093332,-1.439999 0,-0.48 -0.16,-0.959999 -0.16,-1.493333 0,-2.666665 -0.213333,-5.759996 -0.64,-8.426661 -0.159999,-1.119999 -0.426666,-2.559999 -0.426666,-3.679998 0,-2.559998 -0.693333,-4.746664 -0.693333,-7.413329 1.279999,2.933332 9.599998,20.853321 11.786658,20.853321 0.8,0 3.52,-0.799999 3.52,-1.759999 z M 90.246847,53.573625 c 0.266666,0.106666 0.266666,1.333332 0.266666,1.546666 0,1.119999 -0.16,2.239998 -0.16,3.359998 -0.16,-1.493333 -0.16,-2.986665 -0.16,-4.479998 0,-0.16 0,-0.266666 0.05333,-0.426666 z m 0.266666,18.453322 0.05333,-0.106666 c 0,-0.373334 -0.106666,-0.746667 -0.106666,-1.12 v -0.48 c 0.05333,0.266667 0.106666,0.906667 0.32,1.12 0,-0.64 -0.16,-1.226666 -0.16,-1.866666 0,-0.213333 0,-0.639999 0.213333,-0.799999 0,1.119999 0.05333,2.293332 0.05333,3.413331 l -0.16,-0.16 v 0.106667 h -0.213333 z m -0.266666,-7.199996 c -0.05333,-0.266666 -0.05333,-0.479999 -0.05333,-0.746666 v -0.693333 c 0.213334,-0.266666 0.16,-2.453332 0.16,-2.719998 0.106667,0.426666 0.16,1.813332 0.16,2.346665 0,0.266667 0,1.653333 -0.266666,1.813332 z m -1.28,-15.83999 c 0.106667,-0.64 0.05333,-1.333333 0.05333,-1.973332 0.213333,1.706665 0,3.413331 0.426666,5.066663 l -0.106666,0.05333 -0.213334,-1.066666 c 0,0.533333 0.05333,1.119999 0.05333,1.653332 l -0.05333,-0.05333 v -0.533333 c -0.05333,-0.106667 -0.16,-0.32 -0.16,-0.426667 0,-0.32 0.05333,-0.639999 0.05333,-0.959999 l 0.106666,0.32 c -0.05333,-0.693333 0,-1.386666 -0.16,-2.079999 z m 0.266667,12.373326 c 0.05333,-0.373333 -0.05333,-2.239999 -0.16,-2.559999 0.16,-0.266666 0.16,-1.493332 0.16,-1.813332 0.106667,0.213333 0.106667,1.813332 0.106667,2.133332 0,0.426667 0,1.866666 -0.106667,2.239999 z m 14.613326,-0.373333 c -0.0533,0 -0.10667,-0.693333 -0.10667,-0.746667 -0.0533,0.106667 -0.0533,0.266667 -0.0533,0.373334 0,0.106666 0.0533,0.213333 0.0533,0.319999 l -0.0533,0.05333 h 0.0533 c 0,0.32 0.0533,0.639999 -0.0533,0.959999 0,-0.533333 -0.0533,-1.066666 -0.0533,-1.599999 v -0.64 0.266667 c 0.0533,-0.213333 0.0533,-0.426666 0.0533,-0.64 0.16,0.05333 0.16,1.173333 0.16,1.386666 z m -0.10667,-3.466665 c 0,0.213333 -0.0533,0.373333 0,0.586666 v -0.319999 c 0.0533,0.16 0.10667,0.373333 0.10667,0.586666 0,0.213333 -0.0533,0.48 -0.16,0.693333 0,-0.213333 0,-0.426667 -0.0533,-0.64 v 0.266667 -1.973332 c 0,0.05333 0.10666,0.213333 0.10666,0.266666 z m -13.546657,1.493333 c -0.106666,-0.373334 -0.16,-0.853333 -0.106666,-1.226666 l -0.05333,0.16 c 0.106667,-0.586667 -0.05333,-1.12 -0.05333,-1.706666 0,-0.213333 0,-0.426667 0.106667,-0.64 0.05333,1.12 0.05333,2.293332 0.106666,3.413332 z m 0.16,12.586659 c -0.05333,0.16 -0.05333,0.32 -0.05333,0.479999 l -0.16,-0.05333 c 0,-0.266666 -0.05333,-1.119999 0.16,-1.333332 0,0.159999 -0.05333,0.426666 0.05333,0.533333 0.05333,0.266666 0.106667,0.586666 0.106667,0.853332 v 0.05333 L 90.353513,72.08028 Z M 89.926847,57.733622 c 0,0.16 0,0.266667 0.05333,0.426667 -0.16,0.799999 -0.05333,1.759999 -0.05333,2.506665 -0.106667,-0.48 -0.106667,-0.96 -0.106667,-1.493333 0,-0.479999 0,-0.959999 0.106667,-1.439999 z m 0.05333,4.053331 c 0,-0.426666 0.106667,-0.799999 0,-1.279999 l 0.16,0.16 v 0.959999 c 0,0.373333 0,0.8 -0.05333,1.173333 -0.05333,-0.32 -0.106667,-0.693333 -0.106667,-1.013333 z m 0.586666,-11.093326 c 0,0.266666 -0.05333,0.479999 -0.106666,0.746666 -0.05333,-0.05333 -0.05333,-0.16 -0.05333,-0.266667 V 50.58696 c 0,-0.266667 0,-0.48 0.05333,-0.746666 0.05333,0.213333 0.05333,0.479999 0.05333,0.693333 h 0.05333 z m -0.05333,17.173323 c 0,0.213333 0,0.426666 -0.05333,0.639999 -0.05333,-0.213333 -0.05333,-0.479999 -0.05333,-0.693333 V 67.22695 c 0,-0.106667 0,-0.213333 0.05333,-0.32 0.05333,0.32 0.106666,0.64 0.106666,0.96 z m 13.706657,-3.679998 0.16,0.32 c 0,0.213333 -0.10667,0.373333 -0.21333,0.533333 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.16 0,-0.266667 0.0533,-0.426667 z m -0.32,-0.16 c 0,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 l 0.0533,-0.106667 -0.0533,0.05333 c 0,-0.106666 -0.0533,-0.159999 -0.0533,-0.266666 v -0.64 l 0.10666,-0.05333 c 0,0.48 0,0.906666 0,1.386666 z M 89.180181,54.213624 c -0.05333,-0.16 -0.106667,-0.319999 -0.106667,-0.533333 0,-0.266666 0.05333,-0.586666 0.05333,-0.906666 0.05333,0.16 0.05333,0.373333 0.05333,0.533333 z M 102.78017,51.22696 c -0.0533,0.373333 0.21334,0.853332 0.21334,1.226665 v 0.05333 c -0.16,-0.373333 -0.21334,-0.8 -0.37334,-1.226666 z m -11.359991,8.799994 c -0.05333,-0.32 -0.05333,-0.639999 -0.05333,-0.959999 v -0.373333 c 0.05333,0.213333 0.106667,0.586666 0.106667,0.799999 0,0.16 -0.05333,0.373333 -0.05333,0.533333 z m -2.239998,-4.319997 -0.106667,0.266666 v -0.266666 c 0,-0.213333 0,-0.373333 0.05333,-0.533333 0.05333,0.106666 0.05333,0.213333 0.05333,0.32 l 0.106666,-0.106667 v 0.426666 l -0.05333,-0.05333 -0.05333,0.213333 z m 0.213333,-1.226666 c 0,-0.05333 -0.106667,-0.586666 0,-0.48 -0.05333,-0.266666 0,-0.693333 -0.106667,-0.906666 l 0.106667,0.05333 c 0,0.32 0.05333,0.586667 0.05333,0.906666 0,0.16 0,0.32 -0.05333,0.426667 z m 0.906666,-6.399996 c 0,0.32 0,0.639999 0,0.959999 l -0.106667,-0.48 c 0,-0.106666 0.05333,-0.319999 0.106667,-0.479999 z m 12.74666,5.599996 c 0,0.213334 -0.0533,0.373333 -0.0533,0.586667 -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 v -0.32 c 0.0533,0.106667 0.10667,0.266666 0.10667,0.426666 z m 1.06667,8.693329 0.0533,-0.05333 v -0.26667 l 0.10666,0.32 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 l -0.10666,-0.213334 v 0.32 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0,-0.16 0,-0.373333 0.0533,-0.533333 z m -13.81333,6.613329 c 0,0.266666 -0.05333,0.906666 0.05333,1.226666 l -0.05333,0.106666 c -0.106667,-0.106666 -0.05333,-1.226665 0,-1.333332 z m 0.32,-2.399999 c -0.106667,0 -0.16,0.106667 -0.16,0.213334 0.05333,-0.266667 -0.05333,-0.586667 0,-0.746667 l 0.106666,0.05333 v 0.213333 h 0.05333 z m 12.53333,-8.746661 c 0.0533,0.266666 0.0533,0.586666 0,0.853333 -0.0533,-0.32 -0.0533,-0.533333 0,-0.853333 z m -0.53334,-7.253329 c 0.0533,-0.213333 0.0533,-0.426667 0.0533,-0.64 l 0.0533,0.05333 v 0.693333 z m -13.386656,6.346663 c 0,-0.16 0,-0.373333 -0.106667,-0.533333 l 0.106667,-0.266667 c 0,0.16 0,0.373333 0,0.533333 z m 1.333332,-4.853331 c -0.05333,-0.16 -0.106666,-0.319999 -0.106666,-0.479999 0,0.106666 0.05333,0.16 0.16,0.16 0,0.106666 -0.05333,0.159999 -0.05333,0.319999 0,0 0.05333,-0.05333 0.05333,-0.05333 v 0.05333 z m -0.319999,-4.479997 c -0.106667,-0.32 -0.106667,-0.693333 -0.106667,-1.066666 z m 0.16,11.839993 c 0.159999,0.05333 0.05333,0.373333 0,0.48 z m 0.213333,-6.45333 c 0,0.106667 -0.05333,0.266667 -0.05333,0.373334 v -0.48 z m -0.213333,5.493331 c 0,0.05333 0.159999,0.426666 0,0.426666 z m 0.159999,6.666662 c 0.05333,0.05333 0.05333,0.16 0.05333,0.266667 l -0.05333,0.05333 z m 13.866654,-2.773331 0.0533,0.05333 c 0,0.213333 0,0.426666 -0.0533,0.639999 z M 90.353513,53.200292 c 0,0.106666 0,0.16 -0.05333,0.266666 0,-0.106666 -0.05333,-0.266666 -0.05333,-0.373333 z m 0.16,2.879998 v -0.586666 c 0.05333,0.213333 0.05333,0.373333 0.05333,0.586666 z M 104.9135,70.160282 c 0,0.05333 0.0533,0.106666 0.10667,0.159999 l -0.10667,0.05333 h -0.0533 l -0.0533,-0.05333 z m -0.69333,-9.066662 c 0,0.16 0.10667,0.32 -0.0533,0.48 0,-0.16 0,-0.32 0.0533,-0.48 z m -13.973323,4.159998 c 0,-0.106667 0,-0.213333 0.05333,-0.32 l 0.05333,0.16 c 0,0.106667 -0.05333,0.16 -0.106666,0.16 z m 0.319999,-2.933332 c 0,0.16 0,0.373333 -0.05333,0.533333 v -0.533333 z m 0,-10.18666 v 0.106666 l 0.05333,0.106667 -0.05333,0.05333 v -0.16 l -0.106666,0.05333 v -0.05333 z m 14.079994,11.466659 c 0,-0.106666 0,-0.159999 0.0533,-0.266666 v 0.32 z m -2.13333,-11.893326 c 0,-0.106666 0,-0.16 0.0533,-0.266666 0,0.106666 0,0.16 0.0533,0.266666 z M 90.46018,69.840282 c 0,-0.106667 0,-0.213333 0.05333,-0.32 v 0.213333 z m 0.32,-1.333333 c 0,0.05333 0.05333,0.106667 0.05333,0.106667 0,0.05333 -0.05333,0.106666 -0.05333,0.106666 l -0.05333,-0.05333 c 0,-0.05333 0,-0.106666 0.05333,-0.16 z m -1.386666,-3.466664 c -0.05333,-0.106667 -0.05333,-0.266667 -0.05333,-0.373333 0.05333,0.106666 0.05333,0.266666 0.05333,0.373333 z m 14.933326,0.586666 0.0533,-0.106667 v 0.266667 z m -0.21333,-5.119997 c -0.0533,-0.16 -0.0533,-0.266667 0,-0.373333 z m 0,4.746664 h 0.0533 v 0.16 l -0.0533,-0.05333 z m -13.386664,6.293329 c 0,0.05333 0,0.16 0.05333,0.213334 0,-0.05333 0,-0.16 -0.05333,-0.213334 z m -0.213333,-14.773324 0.05333,0.106666 c 0,0.05333 0,0.05333 -0.05333,0.106667 z m -1.333332,-2.186665 c -0.05333,-0.05333 -0.05333,-0.16 0,-0.266667 z m 15.199989,11.946659 c -0.0533,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.0533,-2.613332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 0,-0.05333 0.0533,-0.05333 0.0533,-0.05333 z m -0.21333,-3.199998 -0.0533,-0.05333 0.0533,-0.106667 z m 0.16,5.33333 c -0.0533,-0.05333 -0.0533,-0.16 0,-0.213333 z m -1.44,-13.173325 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0.0533,0.05333 0.0533,0.106667 0.0533,0.16 z m -13.599989,0 c 0.05333,0.05333 0.05333,0.106666 0.05333,0.16 -0.05333,-0.05333 -0.05333,-0.106667 -0.05333,-0.16 z m 14.773329,8.373328 -0.0533,-0.106666 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m -13.599997,7.679996 v -0.106667 h 0.05333 z m -1.119999,-4.906664 v -0.106667 z m 1.173332,1.973332 v 0.106667 z m 13.119994,-8.533328 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -14.613326,-0.16 v -0.05333 h 0.05333 v 0.05333 z m 1.493332,-0.693333 c 0,0 -0.05333,-0.05333 -0.05333,-0.05333 0,0 0.05333,-0.05333 0.05333,-0.05333 z m 13.599994,7.413329 v -0.05333 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106666 z M 89.446847,63.653619 h -0.05333 v -0.05333 z m 15.253323,-0.533333 h -0.0533 l 0.0533,-0.05333 z m -13.91999,4.959997 0.05333,0.05333 h -0.05333 z m 13.38666,-7.146663 0.0533,-0.05333 v 0.05333 z M 91.366846,71.653614 h 0.05333 l -0.05333,0.05333 z M 104.06017,61.57362 v -0.05333 h 0.0533 z m 0.0533,-1.546666 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -14.719996,-5.33333 0.05333,0.05333 h -0.05333 z m 15.626656,15.626657 v -0.05333 l 0.0533,0.05333 z m -2.02666,-15.306657 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z m -12.479997,6.826663 0.05333,-0.05333 v 0.05333 z M 103.63351,60.93362 c 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 1.33333,8.906662 c 0,-0.05333 0,-0.05333 0.0533,-0.106667 0,0.05333 0,0.05333 -0.0533,0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path920" />
+    <path
+       d="m 126.07266,60.666954 c 0,-0.266667 -0.16,-0.586666 -0.48,-0.586666 -0.58667,0 -0.64,0.693332 -0.69333,1.119999 -0.42667,2.506665 -2.77334,7.306662 -5.70667,7.306662 -1.76,0 -1.97333,-0.479999 -3.14666,-1.706665 -0.32,-0.96 -0.96,-1.919999 -0.96,-2.986665 v -0.106667 c 0.37333,-0.106667 0.69333,-0.373333 1.01333,-0.586666 1.97333,-1.12 3.94667,-2.613332 3.94667,-5.119997 0,-1.706666 -1.44,-3.413331 -3.2,-3.413331 -4.16,0 -5.6,2.773331 -6.29333,6.293329 -0.10667,0.533333 -0.32,1.173333 -0.32,1.706666 0,1.706665 0.53333,3.253331 1.01333,4.85333 0.16,0.64 2.08,2.506665 2.66667,2.879998 0.8,0.533333 2.02666,0.8 2.98666,0.8 0.64,0 2.82667,-0.266667 3.30667,-0.746666 3.30666,-1.066666 5.86666,-6.45333 5.86666,-9.706661 z m -9.28,-4.213331 c 0.74667,0 1.33334,0.746666 1.33334,1.439999 0,1.973332 -1.65334,3.306665 -3.25333,4.159998 -0.0533,-0.213333 -0.10667,-0.96 -0.10667,-1.173333 v -0.64 c 0,-1.226665 0.42667,-3.786664 2.02666,-3.786664 z m -3.67999,4.266664 c 0,-0.16 0,-0.32 -0.0533,-0.48 v 0.48 l 0.0533,0.05333 v 0.266667 c -0.0533,-0.106667 -0.0533,-0.213333 -0.0533,-0.32 v 0.586667 l 0.0533,-0.106667 c 0.10666,0.213333 0.16,0.426666 0.16,0.64 0,0.159999 -0.0533,0.319999 -0.0533,0.426666 -0.0533,-0.106667 -0.0533,-0.266667 -0.0533,-0.373333 0,-0.106667 0.0533,-0.426667 -0.10667,-0.48 -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 -0.10667,-0.586666 -0.16,-1.279999 -0.21333,-1.973332 -0.0533,0.426666 -0.0533,0.959999 -0.10667,1.013332 0,-0.16 -0.0533,-0.319999 -0.10667,-0.479999 l 0.0533,-0.106667 c -0.0533,0 -0.10667,-0.05333 -0.10667,-0.16 v -0.05333 h 0.16 v -0.213333 c 0,-0.106667 0,-0.266667 -0.0533,-0.373333 l -0.0533,0.213333 v -0.266667 l -0.0533,-0.106666 v -0.05333 l 0.16,-0.05333 c 0,-0.106667 0,-0.213333 -0.0533,-0.32 l 0.0533,-0.106666 c 0,0 0.0533,0.106666 0.0533,0.213333 0,-0.32 0.0533,-0.64 0.32,-0.906666 v 0.479999 l 0.0533,-0.05333 v 0.213333 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0.0533,0.213333 0.10666,0.426666 0.10666,0.639999 0,0.32 0,0.906666 -0.10666,1.226666 z m 1.54666,7.626662 c -0.0533,0.16 -0.0533,0.32 -0.10666,0.48 l -0.16,-0.16 c 0,-0.373333 0.0533,-0.853333 0.16,-1.173333 0.0533,0.213334 0.10666,0.586667 0.10666,0.8 L 114.766,68.02695 v -0.213334 c 0.16,0.16 0.32,0.32 0.42667,0.48 -0.0533,0.16 -0.0533,0.373333 -0.21334,0.533333 0,0.106667 0.0533,0.213333 0.0533,0.266667 l -0.10667,-0.05333 v -0.479999 c -0.0533,0.159999 -0.0533,0.319999 -0.0533,0.479999 l -0.10666,-0.10667 c 0.0533,-0.106667 0.0533,-0.16 0.0533,-0.266667 l -0.0533,-0.05333 -0.0533,0.16 c 0,-0.16 0,-0.32 -0.0533,-0.426667 z m -1.76,-1.439999 c -0.0533,-0.32 -0.0533,-0.693333 -0.0533,-1.013333 v -1.866665 l -0.0533,0.106666 v -0.106666 l -0.0533,-0.106667 h 0.10667 v -0.639999 l 0.0533,-0.106667 c 0,0.64 0.0533,1.226666 0.0533,1.866666 v 1.013332 c 0,0.266667 0,0.586667 -0.0533,0.853333 z m 1.17334,1.066666 c 0,-0.106666 0,-0.16 0.0533,-0.266666 v -0.48 l -0.0533,0.05333 c 0,-0.266666 0.0533,-0.479999 0.0533,-0.746666 0.0533,0.05333 0.16,0.32 0.16,0.373333 v 0.64 c 0,0.213333 -0.0533,0.48 -0.10667,0.693333 z M 113.006,59.546955 c 0.0533,-0.16 0,-0.32 -0.10667,-0.48 v 0.213333 c 0,0.32 0,0.64 0.0533,0.959999 0.0533,-0.106666 0.10666,-0.159999 0.10666,-0.266666 l 0.0533,0.16 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,-0.16 -0.10667,-0.373333 -0.10667,-0.533333 z m -0.32,3.519997 c -0.0533,-0.159999 -0.10667,-1.013332 0,-1.173332 z m 0.90667,-5.49333 c 0,0.16 0,0.32 -0.0533,0.426667 l -0.0533,0.05333 v -0.373333 z m -1.49334,6.346663 c 0.0533,0.32 0.0533,0.586667 0.0533,0.906666 l -0.0533,-0.106666 z m 0.69334,-3.999997 c 0,-0.16 0,-0.32 0,-0.426667 0,-0.05333 0,-0.266666 -0.0533,-0.426666 0,0.266666 0,0.533333 0.0533,0.853333 z M 112.206,61.41362 c -0.0533,-0.16 -0.0533,-0.32 -0.0533,-0.48 l 0.0533,-0.106666 z m 0.32,-0.64 c 0.0533,0.106667 0.0533,0.266667 0,0.373334 l -0.0533,-0.213334 z m 0.10667,1.12 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 v 0.32 z m -0.74667,0.426666 c 0,0.106667 0.0533,0.213333 0,0.32 z m 2.08,4.586664 -0.0533,-0.106666 0.0533,-0.05333 z m 0,0.373333 -0.0533,-0.106666 0.0533,-0.05333 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path922" />
+    <path
+       d="m 134.04765,67.86695 c 1.17334,-0.64 1.92,-1.866666 2.4,-3.093332 1.38667,-0.533333 4.69333,-2.399998 4.69333,-4.106664 0,-0.213333 -0.21333,-0.426667 -0.42666,-0.426667 -0.32,0 -0.58667,0.16 -0.74667,0.426667 -0.8,1.226666 -1.92,2.239999 -3.30667,2.773332 0,-2.079999 -0.63999,-4.159998 -2.13333,-5.65333 -0.48,-0.48 -2.50666,-1.706666 -3.2,-1.706666 -0.26666,0 -0.58666,0 -0.85333,0 -0.96,0 -1.97333,0.05333 -2.77333,0.586666 -0.42667,0.266667 -0.53333,0.586667 -0.85333,0.906666 -1.44,1.599999 -2.18667,2.133332 -2.18667,4.533331 0,3.946664 3.09333,6.45333 6.82666,6.45333 0.32,0 2.24,-0.533333 2.56,-0.693333 z m -4.95999,-9.386661 c 0,-0.373334 0.26666,-1.44 0.8,-1.44 2.29333,0 2.93333,4.906664 3.14666,6.506663 -1.97333,-0.746666 -2.88,-1.706665 -3.52,-3.733331 -0.10666,-0.426666 -0.42666,-0.906666 -0.42666,-1.333332 z m 2.55999,8.159995 c -1.65333,0 -1.91999,-2.826665 -2.34666,-3.946665 1.22667,1.546666 1.86666,1.706666 3.68,2.293332 -0.21334,0.746667 -0.32,1.653333 -1.33334,1.653333 z m 2.98667,-2.986665 c 0,-0.426667 -0.10667,-0.8 -0.10667,-1.226666 v -0.106667 c 0.10667,-0.106666 0.0533,-0.799999 0,-0.906666 h -0.0533 v 0.05333 l -0.0533,0.05333 c 0.0533,-0.16 0.10666,-0.373333 0.16,-0.533333 h 0.10666 c 0.0533,0.373333 0.10667,0.746666 0.10667,1.066666 0,0.213333 -0.0533,0.426666 -0.10667,0.586666 0,0.373333 0.10667,0.693333 -0.0533,1.013333 z m -7.2,0.106666 c 0.21334,0.213334 0.16,0.64 0.16,0.906667 v 0.853332 c -0.10666,-0.373333 -0.16,-0.799999 -0.16,-1.226666 0.10667,-0.106666 -0.0533,-0.319999 -0.0533,-0.426666 0,-0.05333 0.0533,-0.106667 0.0533,-0.106667 z m 7.25333,1.066666 c 0,-0.213333 0.0533,-1.333332 0.10667,-1.439999 0,0.05333 0.0533,0.106667 0.0533,0.16 0,-0.05333 0,-0.05333 0.0533,-0.106666 v 0.213333 c 0,0.373333 -0.0533,0.693333 -0.10667,1.013333 z m 0.26667,-0.693333 c 0,-0.266666 0.0533,-0.533333 -0.0533,-0.799999 0.10666,-0.426666 0.0533,-0.959999 0.0533,-1.386666 0.0533,0.213333 0.10667,0.426667 0.10667,0.64 0,0.373333 0,1.386666 -0.10667,1.546665 z m -0.16,-0.799999 v -0.05333 l 0.0533,0.05333 z m -0.21333,-2.079999 0.0533,0.05333 v -0.05333 z m 0.0533,1.439999 v -0.106666 z m -7.14666,-0.639999 v -0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path924" />
+    <path
+       d="m 155.71431,60.400287 c 0,-1.653332 -0.21334,-5.386663 -0.8,-6.879996 -0.16,-0.373333 0,-0.853332 -0.16,-1.226665 -0.26667,-0.8 -0.42667,-1.599999 -0.74667,-2.399999 -1.17333,-2.986665 -3.46666,-8.159995 -7.30666,-8.159995 -1.44,0 -3.30666,0.746666 -4,2.133332 -0.26666,-0.266667 -0.69333,-0.533333 -1.06666,-0.533333 -1.97334,0 -1.86667,0.906666 -1.86667,2.346665 0,0.426667 -0.10667,0.853333 -0.10667,1.279999 v 11.679993 c 0,0.853333 0.16,1.706666 0.21334,2.559999 0.16,2.613332 0.26666,5.119997 0.58666,7.733329 -0.26666,0.16 -0.48,0.426666 -0.48,0.746666 0,0.48 0.37334,0.533333 0.69334,0.799999 0.42666,1.333333 -0.16,1.653333 1.33333,2.239999 0.37333,0.16 0.8,0.32 1.22667,0.32 1.43999,0 2.61333,-0.32 2.61333,-2.026666 6.34666,-0.799999 9.86666,-4.21333 9.86666,-10.613327 z m -9.81333,9.013328 c 0,-2.079998 0.0533,-4.21333 -0.0533,-6.293329 -0.0533,-0.586667 -0.16,-1.12 -0.16,-1.706666 0,-2.933331 -0.64,-5.919996 -0.64,-8.853328 0,-1.546666 -0.21334,-3.093331 -0.21334,-4.639997 0,-1.226666 -0.0533,-4.853331 1.70667,-4.853331 0.64,0 1.12,0.64 1.44,1.173333 2.24,3.679998 3.25333,10.399994 3.30666,14.613325 0,0.426666 0.16,0.906666 0.16,1.333332 0,3.039998 -0.53333,6.45333 -3.14666,8.266662 -0.53333,0.373333 -0.53333,0.586666 -1.17333,0.746666 -0.42667,0.106667 -0.85334,0.05333 -1.22667,0.213333 z m -4.42666,-6.879996 c 0,-1.333332 -0.21334,-2.613331 -0.21334,-3.946664 v -4.85333 c 0.37334,0.639999 0.37334,3.679997 0.37334,4.479997 0,1.226666 -0.0533,2.506665 -0.10667,3.733331 h 0.0533 v 0.48 z m 2.4,-4.266664 v 1.066666 l -0.0533,0.05333 h 0.0533 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.319999 -0.16,0.319999 0,-0.266666 -0.0533,-0.533333 -0.0533,-0.799999 0,-0.853333 0.26667,-2.079999 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.10666,-0.213333 0.10666,-0.479999 0.16,-0.693333 v 0.106667 l 0.0533,0.106667 c 0,0.693332 0.16,1.493332 0.16,2.186665 l -0.0533,0.05333 h 0.0533 c 0,0.106667 0.0533,0.16 0.0533,0.266667 v 0.373333 l -0.10666,0.106666 0.0533,-0.159999 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 l -0.0533,-0.16 z m -2.13334,6.77333 0.0533,0.05333 v -0.05333 c 0.0533,0.106666 0.0533,0.213333 0.16,0.319999 -0.16,-0.586666 0.0533,-1.119999 0.0533,-1.653332 l 0.0533,-2.666665 c 0.10667,0.106667 0.10667,0.32 0.10667,0.48 0,0.16 -0.0533,0.373333 0,0.533333 0.16,-0.32 0.0533,-0.426667 0.0533,-0.746666 0.0533,0.426666 0.0533,0.906666 0.0533,1.333332 0,1.013333 -0.16,2.079999 -0.16,3.093332 l -0.0533,0.05333 h -0.0533 l -0.10667,-0.16 v 0.426666 l -0.0533,-0.266666 -0.0533,0.16 -0.0533,-0.106667 z m -0.42666,-12.10666 c -0.16,-1.493332 0,-3.306665 0,-4.799997 l -0.0533,0.05333 v -0.479999 c 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 l 0.10666,-0.373333 c 0,0.586667 -0.16,1.066666 0.0533,1.653333 0.0533,-0.16 0.0533,-0.266667 0.0533,-0.426667 l 0.0533,0.05333 c 0,0.159999 0,0.319999 0.0533,0.479999 -0.0533,0.106667 -0.0533,0.16 -0.0533,0.266667 l -0.0533,-0.213333 -0.0533,0.106666 c 0,0.106667 0,0.213333 0.0533,0.32 -0.10667,0.05333 0,0.426666 0,0.533333 0,-0.106667 0.0533,-0.32 0.10667,-0.426666 0,0.159999 0,0.373333 0.0533,0.533333 -0.0533,0.16 -0.0533,0.373333 -0.0533,0.533333 l -0.10667,-0.373333 c -0.0533,0.266666 -0.0533,0.586666 -0.0533,0.853332 v 0.05333 h 0.0533 l 0.0533,-0.106667 c 0,0.05333 0,0.16 0.0533,0.213333 -0.0533,0.16 -0.0533,0.266667 -0.0533,0.426667 l -0.0533,0.106666 -0.0533,-0.266666 c -0.10667,0.479999 -0.0533,0.959999 -0.0533,1.439999 l -0.10666,-0.373333 -0.0533,0.32 v -0.32 z m 12.58666,8.426662 c -0.21334,-0.906666 0,-2.079999 -0.26667,-2.879998 0.10667,-0.533333 0.0533,-1.013333 0.0533,-1.546666 0.16,0.959999 0,1.973332 0.16,2.933331 0,-0.106666 0.0533,-0.213333 0.0533,-0.319999 0,-0.48 -0.10667,-1.013333 -0.10667,-1.546666 0,-0.05333 0,-0.266667 0.0533,-0.266667 0.0533,0.746667 0.10667,1.493333 0.10667,2.239999 0,0.213333 -0.0533,0.48 -0.0533,0.693333 l -0.0533,-0.05333 c 0.0533,0.213333 0.0533,0.479999 0.0533,0.746666 z m -0.74667,2.986665 c -0.0533,-0.373333 -0.10667,-0.746667 -0.10667,-1.12 v -0.106666 l 0.0533,-0.05333 h -0.0533 V 61.89362 c 0,-0.373333 0,-0.693333 0.0533,-1.013333 0,0.853333 0.16,1.759999 0.16,2.613332 0,0.213333 0.0533,0.693333 -0.10667,0.853333 z m -1.22667,-15.253325 c 0.16,0.373334 0.21334,0.533333 0.21334,0.96 v 1.013333 c 0,0.16 0,0.319999 -0.0533,0.479999 -0.10666,-0.266666 -0.21333,-0.639999 -0.21333,-0.959999 v -0.8 l 0.0533,0.16 z m 0.53334,4.426664 c -0.0533,-0.266666 -0.16,-0.799999 -0.16,-1.066666 v -2.559998 c 0.0533,0.106667 0.16,0.373333 0.16,0.426666 z m 1.17333,4.959998 c 0,0.426666 -0.0533,0.853332 -0.0533,1.279999 -0.10667,-0.693333 -0.16,-1.386666 -0.16,-2.079999 0,-0.213333 0,-0.8 0.21333,-0.959999 -0.0533,0.586666 0,1.173332 0,1.759999 z m -0.74667,-6.879996 c 0,0.213333 -0.0533,0.479999 -0.16,0.693333 0.0533,0.159999 0.0533,0.319999 0.0533,0.479999 l -0.10667,0.16 c -0.0533,-0.799999 -0.10667,-1.546666 -0.10667,-2.346665 0.16,0.266666 0.16,0.64 0.26667,0.959999 z m -11.62666,-4.586664 c 0,-0.693333 0.10667,-1.333333 0.10667,-2.026666 l 0.10667,-0.05333 0.10666,0.16 c -0.0533,0.639999 -0.0533,1.653332 -0.32,2.239999 z m 2.13334,5.119997 -0.0533,-0.213334 c -0.0533,0.16 0,0.266667 0,0.426667 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493333 -0.16,-1.813332 0,-0.106667 0.0533,-0.213334 0.0533,-0.32 v 0.266666 c 0,-0.32 -0.0533,-1.439999 0.10666,-1.706665 0,0.853332 0.0533,1.653332 0.0533,2.506665 z m 0.53333,13.439992 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.64 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373333 0.10667,0.746667 0.10667,1.12 z m 9.75999,-9.226662 c 0,-0.213333 0,-0.479999 0.0533,-0.639999 0,0.48 0.10666,0.959999 0.10666,1.493332 0,0.16 0,0.32 0,0.48 0,-0.32 -0.10666,-0.64 -0.10666,-0.959999 l -0.0533,0.159999 v -0.213333 h 0.0533 l -0.0533,-0.266666 v 0.106666 c 0,-0.05333 0,-0.106666 0,-0.16 z m -11.78666,5.65333 c -0.0533,-0.319999 -0.0533,-0.639999 -0.0533,-0.959999 v -0.373333 c 0.0533,0.106666 0.10667,0.266666 0.10667,0.373333 0,0.32 -0.0533,0.64 -0.0533,0.959999 z m 0.53334,4.053331 c -0.0533,-0.106666 -0.0533,-0.266666 -0.0533,-0.373333 l -0.0533,0.106667 h -0.10667 c 0,-0.16 0.0533,-0.373333 0.10667,-0.533333 h 0.16 c -0.0533,0.32 0,0.586666 0,0.853333 z m 11.14666,-4.53333 c 0.0533,0.426666 0.16,0.853333 0.26666,1.226666 l -0.10666,0.373333 c -0.0533,-0.373333 -0.16,-0.853333 -0.16,-1.226666 z m -1.22667,4.799997 c -0.10667,0.05333 -0.10667,0.16 -0.16,0.266666 v -0.639999 l 0.10667,-0.266667 c 0,0.213333 0.0533,0.426667 0.0533,0.64 z m 0.10667,-1.333333 c 0,0.373333 0.0533,0.746667 0.0533,1.12 -0.0533,0 -0.0533,0.05333 -0.0533,0.106666 -0.10667,-0.32 -0.0533,-0.799999 -0.0533,-1.119999 z m -9.44,-15.146657 c 0.10667,0.479999 0.10667,0.959999 0,1.386666 z m 10.61333,5.01333 c 0.0533,0.213333 0.10667,0.373333 0.10667,0.586666 v 0.16 l -0.10667,0.05333 c 0,-0.266667 0,-0.586667 -0.0533,-0.853333 z m -0.0533,5.066664 c 0.0533,0.373333 0.10666,0.906666 0.10666,1.279999 l -0.0533,-0.106667 -0.0533,0.106667 v -0.533333 0.05333 z m -0.10667,1.119999 c -0.10667,-0.266667 -0.0533,-0.853333 -0.0533,-1.119999 0.0533,0.213333 0.10666,0.906666 0.0533,1.119999 z m -0.26667,3.893331 -0.10666,0.32 v -0.16 c 0,-0.16 0,-0.32 0.0533,-0.426666 z m -10.77332,1.866666 c 0,-0.05333 -0.10667,-0.213334 -0.10667,-0.266667 0,-0.106667 0.0533,-0.213333 0.10667,-0.32 z m -0.85334,-3.306665 c -0.0533,-0.266667 -0.0533,-0.533333 0,-0.8 z m 11.41333,-1.279999 v 0.693333 c -0.0533,-0.213334 -0.0533,-0.48 -0.0533,-0.693333 z m -9.65333,-9.653328 v 0.106667 l 0.10667,-0.05333 v 0.05333 l -0.0533,0.213333 -0.10667,-0.05333 c 0.0533,-0.106667 0,-0.16 0,-0.266667 z m 10.61333,5.279997 v -0.693333 c 0.0533,0.213333 0.0533,0.586666 0.0533,0.8 z m -2.98667,-10.559994 0.0533,-0.213333 0.0533,0.05333 v 0.373333 z m -9.49332,0.853333 c 0.10666,0.106667 0.0533,0.426666 0.0533,0.586666 -0.0533,-0.213333 -0.0533,-0.373333 -0.0533,-0.586666 z m 0,4.85333 c 0,-0.213333 0,-0.426666 0.0533,-0.586666 0,0.213333 0.0533,0.426667 -0.0533,0.586666 z m 0.69333,15.893324 c 0,0.16 -0.0533,0.32 0,0.48 0,-0.16 0.0533,-0.32 0,-0.48 z m -0.53333,-7.946662 c 0,0.213334 0,0.48 -0.0533,0.693333 0,-0.213333 0,-0.479999 0.0533,-0.693333 z m 0.58666,1.653333 c 0,-0.106667 -0.0533,-0.106667 -0.10666,-0.16 v 0.32 z m 1.49334,-0.48 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m -1.49334,6.186663 c -0.0533,0.213333 -0.0533,0.373333 -0.0533,0.586666 0.0533,-0.213333 0.0533,-0.373333 0.0533,-0.586666 z m 1.76,-0.586666 v -0.05333 l 0.0533,-0.106666 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213333 -0.0533,0.32 z m -1.44,-0.853333 c 0,-0.16 -0.0533,-0.266667 -0.0533,-0.426667 0.10667,0.106667 0.10667,0.266667 0.0533,0.426667 z m -0.37333,-4.639997 c 0.0533,0.05333 0.10667,0 0.0533,-0.05333 -0.0533,0.05333 -0.10666,0.16 -0.10666,0.213334 h 0.10666 z m 10.82666,-2.826665 c -0.0533,-0.106667 -0.0533,-0.213334 -0.0533,-0.32 l 0.0533,-0.106667 z m 0.53333,3.733331 c 0,-0.16 -0.0533,-0.266667 0,-0.426667 0,0.16 0.0533,0.266667 0,0.426667 z m -10.71999,7.146662 -0.0533,0.106667 0.0533,0.16 z m 10.18666,-8.959994 -0.0533,0.05333 v -0.213333 l 0.0533,-0.05333 z m 0.58667,-4.906664 c -0.0533,-0.106667 -0.0533,-0.213333 0,-0.32 0.0533,0.106667 0,0.213333 0,0.32 z m -11.52,9.599994 v 0.16 l -0.0533,0.05333 0.0533,-0.16 c -0.0533,0.05333 -0.10666,0 -0.10666,-0.05333 z m 0.64,2.026666 c 0.0533,-0.106667 0.0533,-0.213334 0,-0.32 z m 1.17334,-1.6 c 0,0 -0.0533,-0.05333 -0.0533,-0.106666 0,0 0.0533,-0.05333 0.0533,-0.106667 l 0.0533,0.106667 c 0,0.05333 0,0.05333 -0.0533,0.106666 z m 9.11999,-4.53333 v 0.16 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.16 z m 0,1.439999 h -0.0533 v -0.16 h 0.0533 z m -9.22666,1.013333 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.10667,-4.053331 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.53333,-7.093329 c 0,-0.05333 0,-0.16 0.0533,-0.213334 0,0.05333 0,0.16 -0.0533,0.213334 z m -1.22667,12.906659 c 0.0533,0.106666 0.0533,0.213333 0,0.266666 z m 11.57333,-11.359994 c 0,0.106667 0.0533,0.213334 0,0.32 z m -10.13333,15.039991 c 0,0.05333 0,0.106667 -0.0533,0.16 v -0.106666 z m 0.37334,-10.50666 0.0533,-0.05333 -0.0533,-0.106667 z m -1.12,8.533328 -0.0533,-0.106666 v 0.16 z m 1.06666,3.573332 v -0.16 l 0.0533,0.16 z m 9.06666,-17.813323 v 0.16 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.106667 z m -9.54666,-6.026663 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m -1.01333,15.573324 c 0,-0.05333 0,-0.106667 -0.0533,-0.16 0,0.05333 0,0.106666 0.0533,0.16 z m 1.01333,-14.986658 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 9.92,16.426657 0.0533,-0.106667 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.37333,-4.853331 c 0,0 -0.0533,0.05333 -0.10667,0.05333 l 0.0533,-0.05333 z m -11.67999,5.546664 c 0,0 -0.0533,0.05333 -0.0533,0.05333 0,0 0.0533,0.05333 0.0533,0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,-2.933332 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m 0.90667,3.093332 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 v 0.16 z m -0.16,1.706665 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m 0.96,-16.47999 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -1.70667,13.333326 v -0.106667 c 0.0533,0.05333 0.0533,0.05333 0.0533,0.106667 z m 1.81333,6.186663 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.53334,-3.573332 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.90667,2.133332 c 0.0533,0 0.0533,0.05333 0.0533,0.106667 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106667 z m -1.28,-8.479995 c 0,0.05333 0,0.106667 -0.0533,0.16 z m 0.69333,5.97333 h -0.0533 v -0.05333 z m -0.8,-16.906656 h -0.0533 v 0.05333 z m 2.08,11.359993 0.0533,-0.05333 v 0.05333 z m -0.0533,10.18666 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106666 z M 143.50098,55.76029 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m -1.97333,-3.999997 v 0.05333 l 0.0533,-0.05333 z m 2.13333,8.959994 0.0533,0.05333 h -0.0533 z m -0.90666,7.519996 -0.0533,0.05333 h 0.0533 z m 11.14666,-8.266662 c -0.0533,0.05333 -0.0533,0.05333 -0.0533,0.106667 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106667 z m -12.58666,-9.919994 h 0.0533 v -0.05333 z m 0.53333,10.133327 c 0,0 0.0533,0.05333 0.0533,0.05333 0,0 -0.0533,0.05333 -0.0533,0.05333 z m 1.12,8.213329 h -0.0533 v -0.05333 z m 10.45333,-8.959995 h -0.0533 l 0.0533,-0.05333 z m 0,0.106667 0.0533,0.05333 h -0.0533 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path926" />
+    <path
+       d="m 171.9068,62.533619 c 0,-3.199998 -2.82667,-4.586663 -5.38667,-5.439996 1.65334,-1.653333 2.82667,-3.999998 2.82667,-6.399996 0,-1.226666 0.0533,-2.719999 -0.58667,-3.839998 -0.90666,-1.546666 -2.08,-3.039998 -4.10666,-3.039998 -1.01333,0 -1.86667,0.319999 -2.61333,0.959999 -0.21334,-1.546666 0.10666,-3.039998 -2.08,-3.039998 -1.81333,0 -1.97333,0.906666 -1.97333,2.453332 0,1.813332 0.16,3.679998 0.16,5.546663 0,3.039998 0.16,6.079996 0.16,9.173328 0,1.919999 0.21333,3.839998 0.21333,5.759997 0,0.586666 0.16,1.119999 0.16,1.706665 0,0.16 -0.0533,0.266667 -0.10667,0.426667 -0.53333,0.159999 -0.74666,0.586666 -0.74666,1.119999 0,0.426666 0.16,1.173333 0.69333,1.226666 l 0.16,-0.05333 v 0.266666 c 0,1.013333 -0.37333,2.773332 0.8,3.253331 0.37333,0.16 0.85333,0.426667 1.22666,0.426667 0.8,0 3.2,-0.106667 3.2,-1.279999 -0.32,-0.693333 0,-1.546666 -0.21333,-2.239999 3.84,0 8.21333,-2.773332 8.21333,-6.986663 z m -7.62667,-4.426664 c 2.24,0 5.06667,1.546666 5.06667,4.053331 0,2.826665 -3.52,4.319998 -5.81333,4.639998 -0.10667,-0.8 -0.26667,-1.653333 -0.26667,-2.453332 0,-1.546666 -0.10666,-4.693331 -0.32,-6.186663 0.16,-0.05333 0.32,-0.05333 0.48,-0.05333 z M 164.8668,45.25363 c 1.33333,0 1.97333,1.173332 1.97333,2.453332 0,3.199998 -1.70666,6.559996 -3.94666,8.799994 -0.21333,-0.426666 -0.53333,-6.719996 -0.53333,-7.413329 0,-1.386665 0.37333,-2.133332 1.33333,-3.093331 0.21333,-0.213333 0.85333,-0.746666 1.17333,-0.746666 z m -3.62666,14.506658 c 0,0.586666 0,1.173332 0.10666,1.759999 -0.0533,0.106666 -0.10666,0.213333 -0.10666,0.373333 0,0.426666 0.10666,0.799999 0.10666,1.226666 0,0.106666 0,0.32 -0.16,0.32 0,-0.266667 -0.0533,-0.533333 -0.0533,-0.8 0,-0.959999 0.26667,-1.973332 -0.0533,-2.933332 v 0.746667 l -0.0533,-0.106667 v -0.32 c -0.26666,-0.426666 -0.26666,-1.173332 -0.26666,-1.653332 0,-0.693333 0.10666,-1.439999 0.10666,-2.133332 l -0.10666,-0.213333 v -0.05333 c 0.21333,-0.373333 0.10666,-0.853332 0.21333,-1.279999 0,0.16 0,0.32 0,0.48 l 0.0533,-0.106667 c 0.0533,0.48 0.0533,0.96 0.0533,1.439999 l 0.0533,0.05333 0.0533,-0.213334 c 0.10667,0.533333 0.16,1.013333 0.16,1.546666 -0.0533,0.213333 0.0533,0.693333 0.0533,0.906666 l -0.0533,-0.05333 c 0.0533,0.106667 0.0533,0.373333 0.0533,0.533333 -0.0533,-0.213333 -0.10667,-0.426666 -0.21334,-0.586666 0.10667,-0.32 0.16,-1.599999 -0.0533,-1.813333 0,0.373334 0,0.693333 -0.0533,1.066667 l 0.0533,-0.213334 c 0.0533,0.213334 0.0533,0.426667 0.0533,0.64 l -0.10666,0.106667 0.0533,-0.16 c -0.0533,0.106666 -0.0533,0.266666 -0.0533,0.373333 h 0.0533 v 0.106666 l -0.0533,-0.05333 v 0.16 l 0.0533,-0.05333 c 0,0.106666 0,0.213333 0.0533,0.319999 0,-0.106666 0,-0.213333 0.0533,-0.319999 v 0.853332 l -0.0533,0.05333 z m -0.32,-8.693328 c -0.0533,0.479999 0.0533,0.906666 0.0533,1.386665 l 0.0533,0.106667 v 0.8 l -0.0533,0.05333 v -0.533333 c -0.0533,0.16 -0.0533,0.32 -0.0533,0.48 l -0.0533,0.05333 c 0,-0.373333 -0.0533,-0.906666 0.0533,-1.279999 h -0.0533 c 0,-0.106667 0,-0.266667 0.0533,-0.373333 l -0.0533,-0.05333 c -0.16,0.16 -0.10666,0.586667 -0.10666,0.8 l -0.0533,-0.213333 v 0.426666 h 0.0533 l -0.0533,0.16 v -0.16 c -0.10666,-0.32 -0.16,-1.493332 -0.16,-1.813332 0,-0.48 0.10667,-0.96 0.10667,-1.439999 l 0.0533,0.106666 0.0533,-0.106666 c 0,0.479999 0.16,0.853332 0.16,1.279999 v 0.266666 l -0.0533,0.05333 z m -0.64,20.159988 c 0,-0.32 -0.0533,-0.586667 -0.0533,-0.906667 v -0.479999 l 0.0533,-0.106667 v 0.213333 l 0.0533,-0.106666 v -0.106667 l 0.0533,0.05333 v 0.853333 c 0.0533,-0.05333 0.0533,-0.16 0.0533,-0.213333 0.0533,0.32 0.0533,0.586666 0.0533,0.906666 z m 1.01333,-5.279997 c 0,0.05333 -0.0533,0.426666 -0.0533,0.426666 0,-0.32 -0.16,-0.639999 -0.16,-0.959999 0,-0.16 0,-0.48 0.10666,-0.586667 0.10667,0.373334 0.10667,0.746667 0.10667,1.12 z m -1.28,1.813332 c 0,0.16 0.26667,1.119999 0.32,1.173333 l -0.0533,0.106666 c 0,-0.213333 -0.10667,-0.533333 -0.21334,-0.693333 v -0.266666 0.266666 c -0.0533,-0.106666 -0.10666,-0.266666 -0.21333,-0.319999 l 0.10667,-0.213334 v -0.05333 z m 0.37333,-17.546656 c 0.10667,0.48 0.10667,0.959999 0,1.386666 z m 0.32,-1.386666 v 0.106667 h -0.0533 c 0,-0.05333 -0.0533,-0.106667 -0.0533,-0.16 0,-0.106667 0.10666,-0.16 0.16,-0.16 v 0.213333 z m -0.0533,4.373331 c 0.0533,-0.106667 0,-0.16 0,-0.266667 h 0.0533 v 0.106667 l 0.10667,-0.05333 -0.0533,0.266667 z m 0.69333,6.559996 c 0,-0.05333 0.0533,-0.16 0.0533,-0.213333 0,0.05333 0.0533,0.159999 0.0533,0.213333 v 0.106666 l -0.10667,0.05333 z m 4.58667,-4.426664 c 0.0533,-0.213333 0.21333,-0.373333 0.32,-0.533333 v 0.106666 z m -4.85333,7.146662 c -0.0533,0.213333 -0.0533,0.48 -0.0533,0.746666 -0.0533,-0.266666 -0.0533,-0.533333 0,-0.799999 z m 0.26666,5.546664 0.0533,-0.106667 c 0,-0.05333 0,-0.106667 0.0533,-0.106667 0,0.106667 0,0.213334 -0.0533,0.32 l -0.0533,-0.05333 z m -0.32,-14.399992 c -0.0533,-0.05333 -0.0533,-0.106667 -0.0533,-0.213333 l 0.0533,0.05333 z m 0.26667,13.066659 c 0,0.05333 0,0.05333 -0.0533,0.106667 0,0 -0.0533,-0.05333 -0.0533,-0.106667 0,0 0.0533,-0.05333 0.0533,-0.106667 z m 0.10667,-6.45333 -0.0533,-0.213333 c 0.0533,0.05333 0.0533,0.106667 0.10667,0.16 z M 161.1335,55.28029 c -0.0533,-0.106666 -0.0533,-0.213333 -0.0533,-0.319999 0.0533,0.106666 0.0533,0.213333 0.0533,0.319999 z m -0.48,-5.919996 c 0,-0.106667 0,-0.16 0.0533,-0.266667 0,0.106667 0,0.16 -0.0533,0.266667 z m 0.48,15.359991 0.0533,0.106666 c -0.0533,0 -0.10666,-0.05333 -0.10666,-0.106666 z m -0.64,-11.14666 c 0,-0.05333 0,-0.16 0.0533,-0.213333 0,0.05333 0,0.159999 -0.0533,0.213333 z m 0.53333,7.093329 -0.0533,-0.106667 c 0,-0.05333 0,-0.05333 0.0533,-0.106666 z m -0.0533,-6.45333 v -0.16 l 0.0533,0.05333 z m -0.26667,15.946658 c 0,0.05333 0,0.106666 -0.0533,0.159999 v -0.106666 z m 0.42667,-10.559994 -0.0533,-0.106667 v 0.16 z m -0.85333,8.639995 c 0,0 -0.0533,0 -0.0533,-0.05333 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0.26666,-20.319988 c -0.0533,-0.106667 -0.0533,-0.16 0,-0.213333 z m 0,0.586666 c 0,-0.05333 -0.0533,-0.16 -0.0533,-0.213333 0.0533,0.05333 0.0533,0.16 0.0533,0.213333 z m 0.16,22.026654 -0.0533,-0.106667 c 0,0 0.0533,-0.05333 0.0533,-0.05333 z m 0,-21.439988 v -0.106666 l 0.0533,0.16 z m 0.53334,17.866656 0.0533,0.05333 c 0,0.05333 -0.0533,0.106666 -0.0533,0.106666 z m -0.64,-15.946657 0.0533,-0.106666 c 0,0 -0.0533,-0.05333 -0.0533,-0.05333 z m -0.26667,18.07999 c 0.0533,0 0.0533,0.05333 0.0533,0.106666 -0.0533,0 -0.0533,-0.05333 -0.0533,-0.106666 z m 0,0.373333 h -0.0533 v -0.106667 z m 0.53333,-13.333326 c 0.0533,-0.05333 0.0533,-0.05333 0.0533,-0.106666 v 0.05333 z m 0.21334,5.013331 h -0.0533 v -0.05333 z m 0.32,-1.813333 0.0533,0.05333 h -0.0533 z m -1.06667,9.439995 h -0.0533 v -0.05333 z m 0.74667,-7.733329 h -0.0533 l 0.0533,-0.05333 z m -0.10667,10.186661 c -0.0533,-0.05333 -0.0533,-0.05333 -0.0533,-0.106667 z"
+       style="font-size:53.3333px;font-family:'Wildemount Rough';-inkscape-font-specification:'Wildemount Rough, Normal'"
+       id="path928" />
+  </g>
+  <g
+     aria-label="neodb.social"
+     id="text2391"
+     style="font-size:40px;line-height:1.25;fill:#b3b3b3">
+    <path
+       d="m 32.766354,166.81331 q 2.856,0 4.248,1.488 1.2,1.272 1.2,3.48 v 6.888 h -1.824 v -7.152 q 0,-1.536 -1.008,-2.376 -0.984,-0.864 -2.616,-0.864 h -4.776 q -1.632,0 -2.664,0.888 -1.008,0.888 -1.008,2.352 v 7.152 h -1.8 v -11.856 h 1.8 v 1.92 q 0.456,-0.96 1.32,-1.44 0.888,-0.48 2.352,-0.48 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5924" />
+    <path
+       d="m 46.123104,178.66931 q -5.328,0 -5.328,-4.44 v -2.832 q 0,-1.92 1.344,-3.216 1.464,-1.416 3.936,-1.416 h 5.232 q 2.568,0 4.008,1.248 1.344,1.152 1.344,2.928 v 1.728 h -14.04 v 1.344 q 0,1.704 0.984,2.496 0.864,0.696 2.472,0.696 h 5.928 q 1.416,0 2.112,-0.576 0.72,-0.6 0.72,-1.512 v -0.24 h 1.824 v 0.24 q 0,1.368 -0.912,2.328 -1.176,1.224 -3.528,1.224 z m 5.376,-10.416 h -5.712 q -1.392,0 -2.28,0.816 -0.888,0.816 -0.888,2.136 h 12.24 q 0,-1.296 -0.912,-2.112 -0.888,-0.84 -2.448,-0.84 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5926" />
+    <path
+       d="m 64.309479,166.76531 h 5.016 q 2.88,0 4.248,1.416 1.176,1.2 1.176,3.264 v 2.592 q 0,2.064 -1.2,3.264 -1.392,1.392 -4.224,1.392 h -5.016 q -5.448,0 -5.448,-4.656 v -2.592 q 0,-2.16 1.248,-3.36 1.368,-1.32 4.2,-1.32 z m -3.6,7.272 q 0,3.192 3.768,3.192 h 4.68 q 1.8,0 2.736,-0.72 1.032,-0.792 1.032,-2.472 v -2.616 q 0,-1.536 -0.984,-2.352 -0.96,-0.816 -2.784,-0.816 h -4.68 q -1.92,0 -2.856,0.744 -0.912,0.744 -0.912,2.448 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5928" />
+    <path
+       d="m 76.910604,174.03731 v -2.592 q 0,-2.136 1.272,-3.36 1.392,-1.32 4.032,-1.32 h 4.896 q 1.776,0 2.616,0.408 0.504,0.24 0.984,0.936 v -6.096 h 1.824 v 16.656 h -1.824 v -1.152 q -0.48,0.6 -1.08,0.84 -0.816,0.336 -2.52,0.336 h -4.896 q -5.304,0 -5.304,-4.656 z m 9.96,-5.784 h -4.032 q -2.136,0 -3.12,0.816 -0.984,0.792 -0.984,2.376 v 2.592 q 0,3.192 4.104,3.192 h 4.032 q 1.92,0 2.904,-0.888 1.008,-0.888 1.008,-2.304 v -2.592 q 0,-1.44 -0.936,-2.28 -1.032,-0.912 -2.976,-0.912 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5930" />
+    <path
+       d="m 96.877479,177.37331 v 1.296 h -1.8 v -16.656 h 1.8 v 6.096 q 0.432,-0.624 1.152,-0.936 0.936,-0.408 2.472001,-0.408 h 4.896 q 2.592,0 3.936,1.248 1.368,1.248 1.368,3.432 v 2.592 q 0,2.256 -1.296,3.456 -1.296,1.2 -4.008,1.2 h -4.896 q -1.464001,0 -2.328001,-0.384 -0.576,-0.264 -1.296,-0.936 z m 3.864001,-0.144 h 4.032 q 2.16,0 3.12,-0.744 0.984,-0.768 0.984,-2.448 v -2.592 q 0,-1.56 -1.008,-2.376 -1.008,-0.816 -3.096,-0.816 h -4.032 q -1.920001,0 -2.928001,0.888 -0.984,0.864 -0.984,2.304 v 2.592 q 0,1.44 0.96,2.328 0.984,0.864 2.952001,0.864 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5932" />
+    <path
+       d="m 113.31635,176.79731 h 1.968 v 1.872 h -1.968 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5934" />
+    <path
+       d="m 117.6221,175.74131 v -0.84 h 1.824 v 0.624 q 0,0.864 0.552,1.296 0.552,0.408 2.04,0.408 h 6.792 q 2.712,0 2.712,-1.632 v -0.504 q 0,-0.768 -0.648,-1.224 -0.624,-0.456 -1.728,-0.456 l -7.224,-0.144 q -2.136,0 -3.144,-0.624 -1.152,-0.72 -1.152,-2.304 v -0.552 q 0,-1.368 1.08,-2.184 1.104,-0.84 3.264,-0.84 h 6.648 q 2.664,0 3.864,0.984 0.912,0.744 0.912,1.968 v 0.816 h -1.824 v -0.6 q 0,-0.792 -0.432,-1.152 -0.624,-0.528 -2.256,-0.528 h -7.08 q -2.376,0 -2.376,1.584 v 0.504 q 0,0.768 0.6,1.128 0.6,0.336 1.776,0.336 l 7.248,0.144 q 2.088,0 3.168,0.768 1.104,0.768 1.104,2.376 v 0.672 q 0,1.344 -1.104,2.136 -1.104,0.768 -3.36,0.768 h -7.08 q -2.328,0 -3.336,-0.888 -0.84,-0.744 -0.84,-2.04 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5936" />
+    <path
+       d="m 141.09073,166.76531 h 5.016 q 2.88,0 4.248,1.416 1.176,1.2 1.176,3.264 v 2.592 q 0,2.064 -1.2,3.264 -1.392,1.392 -4.224,1.392 h -5.016 q -5.448,0 -5.448,-4.656 v -2.592 q 0,-2.16 1.248,-3.36 1.368,-1.32 4.2,-1.32 z m -3.6,7.272 q 0,3.192 3.768,3.192 h 4.68 q 1.8,0 2.736,-0.72 1.032,-0.792 1.032,-2.472 v -2.616 q 0,-1.536 -0.984,-2.352 -0.96,-0.816 -2.784,-0.816 h -4.68 q -1.92,0 -2.856,0.744 -0.912,0.744 -0.912,2.448 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5938" />
+    <path
+       d="m 167.63585,174.46931 h 1.824 q 0,1.872 -1.08,2.928 -1.32,1.272 -4.2,1.272 h -5.592 q -2.52,0 -3.768,-1.152 -1.248,-1.152 -1.248,-3.24 v -2.952 q 0,-2.04 1.248,-3.24 1.368,-1.32 3.912,-1.32 h 5.448 q 2.592,0 3.936,1.104 1.344,1.08 1.344,3.096 h -1.824 q 0,-1.296 -0.768,-1.968 -0.84,-0.744 -2.688,-0.744 h -5.448 q -1.608,0 -2.472,0.768 -0.864,0.744 -0.864,2.304 v 2.952 q 0,1.368 0.96,2.184 0.888,0.744 2.232,0.744 h 5.592 q 3.456,0 3.456,-2.736 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5940" />
+    <path
+       d="m 174.07235,166.81331 v 11.856 h -1.8 v -11.856 z m 0,-4.8 v 1.728 h -1.8 v -1.728 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5942" />
+    <path
+       d="m 180.75448,171.42131 h 7.752 q 0.984,0 1.56,0.288 0.6,0.264 0.744,0.696 v -1.68 q 0,-1.104 -0.696,-1.728 -0.792,-0.72 -2.376,-0.72 h -5.28 q -1.536,0 -2.328,0.456 -0.768,0.456 -0.768,1.224 v 0.288 h -1.872 v -0.288 q 0,-1.464 1.176,-2.304 1.176,-0.84 3.312,-0.84 h 5.952 q 2.448,0 3.624,1.128 1.08,1.032 1.08,3.048 v 7.68 h -1.8 v -0.936 q -0.384,0.432 -0.912,0.696 -0.528,0.24 -1.344,0.24 h -7.92 q -3.816,0 -3.816,-3.024 v -1.44 q 0,-1.272 0.912,-1.992 1.008,-0.792 3,-0.792 z m 7.272,1.464 h -6.648 q -1.608,0 -2.184,0.408 -0.552,0.408 -0.552,1.32 v 0.864 q 0,0.84 0.6,1.296 0.624,0.432 2.136,0.432 h 6.648 q 1.464,0 2.088,-0.504 0.648,-0.504 0.648,-1.656 0,-2.16 -2.736,-2.16 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5944" />
+    <path
+       d="m 198.05623,162.01331 v 16.656 h -1.824 v -16.656 z"
+       style="font-size:24px;font-family:Federation;-inkscape-font-specification:'Federation, Normal'"
+       id="path5946" />
+  </g>
+</svg>
diff --git a/common/static/js/create_update_review.js b/common/static/js/create_update_review.js
index 9dad74dc..60ab409d 100644
--- a/common/static/js/create_update_review.js
+++ b/common/static/js/create_update_review.js
@@ -1,9 +1,9 @@
 $(document).ready( function() {
     
     $(".markdownx-preview").hide();
-    $(".markdownx textarea").attr("placeholder", "拖拽图片至编辑框即可插入哦~");
+    $(".markdownx textarea").attr("placeholder", "从剪贴板粘贴或者拖拽文件至编辑框即可插入图片");
 
-    $(".review-form__preview-button").click(function() {
+    $(".review-form__preview-button").on('click', function() {
         if ($(".markdownx-preview").is(":visible")) {
             $(".review-form__preview-button").text("预览");
             $(".markdownx-preview").hide();
diff --git a/common/static/js/detail.js b/common/static/js/detail.js
index 01feac46..fe61e7c9 100644
--- a/common/static/js/detail.js
+++ b/common/static/js/detail.js
@@ -7,7 +7,7 @@ $(document).ready( function() {
 
     // pop up new rating modal
     $("#addMarkPanel button").each(function() {
-        $(this).click(function(e) {
+        $(this).on('click', function(e) {
             e.preventDefault();
             let title = $(this).text().trim();
             $(".mark-modal__title").text(title);
@@ -29,7 +29,7 @@ $(document).ready( function() {
     })
 
     // pop up modify mark modal
-    $(".mark-panel a.edit").click(function(e) {
+    $(".mark-panel a.edit").on('click', function(e) {
         e.preventDefault();
         let title = $(".mark-panel__status").text().trim();
         $(".mark-modal__title").text(title);
@@ -79,7 +79,7 @@ $(document).ready( function() {
     if ($("#statusSelection input[type='radio']:checked").val() == WISH_CODE) {
         $(".mark-modal .rating-star-edit").hide();
     }
-    $("#statusSelection input[type='radio']").click(function() {
+    $("#statusSelection input[type='radio']").on('click', function() {
         if ($(this).val() == WISH_CODE) {
             $(".mark-modal .rating-star-edit").hide();
         } else {
@@ -89,14 +89,14 @@ $(document).ready( function() {
     });
 
     // show confirm modal
-    $(".mark-panel a.delete").click(function(e) {
+    $(".mark-panel a.delete").on('click', function(e) {
         e.preventDefault();
         $(".confirm-modal").show();
         $(".bg-mask").show();
     });
 
     // confirm modal
-    $(".confirm-modal input[type='submit']").click(function(e) {
+    $(".confirm-modal input[type='submit']").on('click', function(e) {
         e.preventDefault();
         $(".mark-panel form").submit();
     });
@@ -116,20 +116,20 @@ $(document).ready( function() {
     });
 
     // expand hidden long text
-    $(".entity-desc__unfold-button a").click(function() {
+    $(".entity-desc__unfold-button a").on('click', function() {
         $(this).parent().siblings(".entity-desc__content").removeClass('entity-desc__content--folded');
         $(this).parent(".entity-desc__unfold-button").remove();
     });
     
     // disable delete mark button after click
     const confirmDeleteMarkButton = $('.confirm-modal__confirm-button > input');
-    confirmDeleteMarkButton.click(function() {
+    confirmDeleteMarkButton.on('click', function() {
         confirmDeleteMarkButton.prop("disabled", true);
     });
 
     // disable sumbit button after click
     const confirmSumbitMarkButton = $('.mark-modal__confirm-button > input');
-    confirmSumbitMarkButton.click(function() {
+    confirmSumbitMarkButton.on('click', function() {
         confirmSumbitMarkButton.prop("disabled", true);
         confirmSumbitMarkButton.closest('form')[0].submit();
     });
diff --git a/common/static/js/home.js b/common/static/js/home.js
index 456c637b..ca1cb32b 100644
--- a/common/static/js/home.js
+++ b/common/static/js/home.js
@@ -1,114 +1,98 @@
-
 $(document).ready( function() {
+    $("#userInfoCard .mast-brief").text($("<div>"+$("#userInfoCard .mast-brief").text().replace(/\<br/g,'\n<br').replace(/\<p/g,'\n<p')+"</div>").text());
+    $("#userInfoCard .mast-brief").html($("#userInfoCard .mast-brief").html().replace(/\n/g,'<br/>'));
     
     let token = $("#oauth2Token").text();
-    let mast_uri = $("#mastodonURI").text();
-    let mast_domain = new URL(mast_uri);
-    mast_domain = mast_domain.hostname;
+    let mast_domain = $("#mastodonURI").text();
+    let mast_uri = 'https://' + mast_domain
     let id = $("#userMastodonID").text();
 
-    let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
-    let followersSpinner = $("#spinner").clone().removeAttr("hidden");
-    let followingSpinner = $("#spinner").clone().removeAttr("hidden");
-    $("#userInfoCard").append(userInfoSpinner);
-    $("#followings h5").after(followingSpinner);
-    $("#followers h5").after(followersSpinner);
-    $(".mast-following-more").hide();
-    $(".mast-followers-more").hide();
+    if (id && id != 'None' && mast_domain != 'twitter.com') {
+        // let userInfoSpinner = $("#spinner").clone().removeAttr("hidden");
+        let followersSpinner = $("#spinner").clone().removeAttr("hidden");
+        let followingSpinner = $("#spinner").clone().removeAttr("hidden");
+        // $("#userInfoCard").append(userInfoSpinner);
+        $("#followings h5").after(followingSpinner);
+        $("#followers h5").after(followersSpinner);
+        $(".mast-following-more").hide();
+        $(".mast-followers-more").hide();
 
-    getUserInfo(
-        id, 
-        mast_uri, 
-        token, 
-        function(userData) {
-            let userName;
-            if (userData.display_name) {
-                userName = translateEmojis(userData.display_name, userData.emojis, true);
-            } else {
-                userName = userData.username;
-            }
-            $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
-            $("#userInfoCard .mast-displayname").html(userName);
-            $("#userInfoCard .mast-brief").text($(userData.note).text());
-            $(userInfoSpinner).remove();
-        }
-    );
+        getFollowers(
+            id,
+            mast_uri,
+            token,
+            function(userList, request) {
+                if (userList.length == 0) {
+                    $(".mast-followers").hide();
+                    $(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
 
-    getFollowers(
-        id,
-        mast_uri,
-        token,
-        function(userList, request) {
-            if (userList.length == 0) {
-                $(".mast-followers").hide();
-                $(".mast-followers").before('<div style="margin-bottom: 20px;">暂无</div>');
-
-            } else {
-                if (userList.length > 4){
-                    userList = userList.slice(0, 4);
-                    $(".mast-followers-more").show();
+                } else {
+                    if (userList.length > 4){
+                        userList = userList.slice(0, 4);
+                        $(".mast-followers-more").show();
+                    }
+                    let template = $(".mast-followers li").clone();
+                    $(".mast-followers").html("");
+                    userList.forEach(data => {
+                        temp = $(template).clone();
+                        temp.find("img").attr("src", data.avatar);
+                        if (data.display_name) {
+                            temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
+                        } else {
+                            temp.find(".mast-displayname").text(data.username);
+                        }
+                        let url;
+                        if (data.acct.includes('@')) {
+                            url = $("#userPageURL").text().replace('0', data.acct);
+                        } else {
+                            url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
+                        }
+                        temp.find("a").attr('href', url);
+                        $(".mast-followers").append(temp);
+                    });
                 }
-                let template = $(".mast-followers li").clone();
-                $(".mast-followers").html("");
-                userList.forEach(data => {
-                    temp = $(template).clone();
-                    temp.find("img").attr("src", data.avatar);
-                    if (data.display_name) {
-                        temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
-                    } else {
-                        temp.find(".mast-displayname").text(data.username);
-                    }
-                    let url;
-                    if (data.acct.includes('@')) {
-                        url = $("#userPageURL").text().replace('0', data.acct);
-                    } else {
-                        url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
-                    }
-                    temp.find("a").attr('href', url);
-                    $(".mast-followers").append(temp);
-                });
+                $(followersSpinner).remove();
             }
-            $(followersSpinner).remove();
-        }
-    );
+        );
 
-    getFollowing(
-        id,
-        mast_uri,
-        token,
-        function(userList, request) {
-            if (userList.length == 0) {
-                $(".mast-following").hide();
-                $(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
-            } else {
-                if (userList.length > 4){
-                    userList = userList.slice(0, 4);
-                    $(".mast-following-more").show();
+        getFollowing(
+            id,
+            mast_uri,
+            token,
+            function(userList, request) {
+                if (userList.length == 0) {
+                    $(".mast-following").hide();
+                    $(".mast-following").before('<div style="margin-bottom: 20px;">暂无</div>');
+                } else {
+                    if (userList.length > 4){
+                        userList = userList.slice(0, 4);
+                        $(".mast-following-more").show();
+                    }
+                    let template = $(".mast-following li").clone();
+                    $(".mast-following").html("");
+                    userList.forEach(data => {
+                        temp = $(template).clone()
+                        temp.find("img").attr("src", data.avatar);
+                        if (data.display_name) {
+                            temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
+                        } else {
+                            temp.find(".mast-displayname").text(data.username);
+                        }
+                        let url;
+                        if (data.acct.includes('@')) {
+                            url = $("#userPageURL").text().replace('0', data.acct);
+                        } else {
+                            url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
+                        }
+                        temp.find("a").attr('href', url);
+                        $(".mast-following").append(temp);
+                    });
                 }
-                let template = $(".mast-following li").clone();
-                $(".mast-following").html("");
-                userList.forEach(data => {
-                    temp = $(template).clone()
-                    temp.find("img").attr("src", data.avatar);
-                    if (data.display_name) {
-                        temp.find(".mast-displayname").html(translateEmojis(data.display_name, data.emojis));
-                    } else {
-                        temp.find(".mast-displayname").text(data.username);
-                    }
-                    let url;
-                    if (data.acct.includes('@')) {
-                        url = $("#userPageURL").text().replace('0', data.acct);
-                    } else {
-                        url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
-                    }
-                    temp.find("a").attr('href', url);
-                    $(".mast-following").append(temp);
-                });
-            }
-            $(followingSpinner).remove();
+                $(followingSpinner).remove();
 
-        }
-    );
+            }
+        );
+    }
 
     // mobile dropdown
     $(".relation-dropdown__button").data("collapse", true);
@@ -118,7 +102,7 @@ $(document).ready( function() {
         button.children('.icon-arrow').toggleClass("icon-arrow--expand");
         button.siblings('.relation-dropdown__body').toggleClass("relation-dropdown__body--expand");
     }
-    $(".relation-dropdown__button").click(onClickDropdownButton)
+    $(".relation-dropdown__button").on('click', onClickDropdownButton);
 
     // close when click outside
     window.onclick = evt => {
@@ -129,7 +113,7 @@ $(document).ready( function() {
     };
 
     // import panel
-    $("#uploadBtn").click(e => {
+    $("#uploadBtn").on('click', e => {
         const btn = $("#uploadBtn")
         const form = $(".import-panel__body form")
 
@@ -201,7 +185,8 @@ $(document).ready( function() {
                     if (!data.total_items == 0) {
                         progress.attr("max", data.total_items);
                         progress.attr("value", data.finished_items);
-                        percent.text(Math.floor(100 * data.finished_items / data.total_items) + '%');
+                        progress.attr("value", data.finished_items);
+                        percent.text("" + data.finished_items + "/" + data.total_items);
                     }
                     setTimeout(() => {
                         poll();
diff --git a/common/static/js/mastodon.js b/common/static/js/mastodon.js
index 91defcbe..02a54679 100644
--- a/common/static/js/mastodon.js
+++ b/common/static/js/mastodon.js
@@ -54,38 +54,50 @@ const NUMBER_PER_REQUEST = 20
 //       "fields": []
 //     }
 //   ]
-function getFollowers(id, mastodonURI, token, callback) {
-    let url = mastodonURI + API_FOLLOWERS.replace(":id", id);
-    $.ajax({
-        url: url,
-        method: 'GET',
-        headers: {
-            'Authorization': 'Bearer ' + token,
-        },
-        data: {
-            'limit': NUMBER_PER_REQUEST
-        },
-        success: function(data, status, request){
-            callback(data, request);
-        },
-    });
+async function getFollowers(id, mastodonURI, token, callback) {
+    const url = mastodonURI + API_FOLLOWERS.replace(":id", id);
+    var response;
+    try {
+        response = await fetch(url+'?limit='+NUMBER_PER_REQUEST, {headers: {'Authorization': 'Bearer ' + token}});
+    } catch (e) {
+        console.error('loading followers failed.');
+        return;
+    }
+    const json = await response.json();
+    let nextUrl = null;
+    let links = response.headers.get('link');
+    if (links) {
+        links.split(',').forEach(link => {
+            if (link.includes('next')) {
+                let regex = /<(.*?)>/;
+                nextUrl = link.match(regex)[1];
+            }
+        });
+    }
+    callback(json, nextUrl);
 }
 
-function getFollowing(id, mastodonURI, token, callback) {
-    let url = mastodonURI + API_FOLLOWING.replace(":id", id);
-    $.ajax({
-        url: url,
-        method: 'GET',
-        headers: {
-            'Authorization': 'Bearer ' + token,
-        },        
-        data: {
-            'limit': NUMBER_PER_REQUEST
-        },
-        success: function(data, status, request){
-            callback(data, request);
-        },
-    });
+async function getFollowing(id, mastodonURI, token, callback) {
+    const url = mastodonURI + API_FOLLOWING.replace(":id", id);
+    var response;
+    try {
+        response = await fetch(url+'?limit='+NUMBER_PER_REQUEST, {headers: {'Authorization': 'Bearer ' + token}});
+    } catch (e) {
+        console.error('loading following failed.');
+        return;
+    }
+    const json = await response.json();
+    let nextUrl = null;
+    let links = response.headers.get('link');
+    if (links) {
+        links.split(',').forEach(link => {
+            if (link.includes('next')) {
+                let regex = /<(.*?)>/;
+                nextUrl = link.match(regex)[1];
+            }
+        });
+    }
+    callback(json, nextUrl);
 }
 
 // {
diff --git a/common/static/js/rating-star-readonly.js b/common/static/js/rating-star-readonly.js
index 76802ea8..a552a6b1 100644
--- a/common/static/js/rating-star-readonly.js
+++ b/common/static/js/rating-star-readonly.js
@@ -1,5 +1,5 @@
 $(document).ready( function() {
-    
+let render = function() {
     let ratingLabels = $(".rating-star");
     $(ratingLabels).each( function(index, value) {
         let ratingScore = $(this).data("rating-score") / 2;
@@ -8,5 +8,9 @@ $(document).ready( function() {
             readOnly: true
         });
     });
-    
+};
+document.body.addEventListener('htmx:load', function(evt) {
+    render();
+});
+render();
 });
\ No newline at end of file
diff --git a/common/static/js/scrape.js b/common/static/js/scrape.js
index 67ce3e3a..bd05bbd8 100644
--- a/common/static/js/scrape.js
+++ b/common/static/js/scrape.js
@@ -1,6 +1,6 @@
 $(document).ready( function() {
     
-    $(".submit").click(function(e) {
+    $(".submit").on('click', function(e) {
         e.preventDefault();
         let form = $("#scrapeForm form");
         if (form.data('submitted') === true) {
diff --git a/common/static/js/sort_layout.js b/common/static/js/sort_layout.js
index 1ac72eba..c20e370d 100644
--- a/common/static/js/sort_layout.js
+++ b/common/static/js/sort_layout.js
@@ -8,7 +8,7 @@ $(() => {
             $(e).data("visibility", true);
         }
         let btn = $("#toggleDisplayButtonTemplate").clone().removeAttr("id");
-        btn.click(e => {
+        btn.on('click', e => {
             if ($(e.currentTarget).parent().data('visibility') === true) {                
                 // flip text
                 $(e.currentTarget).children("span.showText").show();
@@ -72,7 +72,7 @@ $(() => {
     });
 
     // activate sorting
-    $("#sortEditButton").click(evt => {
+    $("#sortEditButton").on('click', evt => {
         // test if edit mode is activated
         isActivated = $("#sortSaveIcon").is(":visible");
 
@@ -134,7 +134,7 @@ $(() => {
     });
     
     // exit edit mode
-    $("#sortExitButton").click(evt => {
+    $("#sortExitButton").on('click', evt => {
         initialLayoutData.forEach(elem => {
             // set visiblity
             $('#' + elem.id).data('visibility', elem.visibility);
diff --git a/common/static/lib/css/milligram.css b/common/static/lib/css/milligram.css
deleted file mode 100644
index be254df8..00000000
--- a/common/static/lib/css/milligram.css
+++ /dev/null
@@ -1,605 +0,0 @@
-/*!
- * Milligram v1.3.0
- * https://milligram.github.io
- *
- * Copyright (c) 2017 CJ Patoilo
- * Licensed under the MIT license
- */
-
-*,
-*:after,
-*:before {
-  box-sizing: inherit;
-}
-
-html {
-  box-sizing: border-box;
-  font-size: 62.5%;
-}
-
-body {
-  color: #606c76;
-  font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
-  font-size: 1.6em;
-  font-weight: 300;
-  letter-spacing: .01em;
-  line-height: 1.6;
-}
-
-textarea {
-  font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
-}
-
-blockquote {
-  border-left: 0.3rem solid #d1d1d1;
-  margin-left: 0;
-  margin-right: 0;
-  padding: 1rem 1.5rem;
-}
-
-blockquote *:last-child {
-  margin-bottom: 0;
-}
-
-.button,
-button,
-input[type='button'],
-input[type='reset'],
-input[type='submit'] {
-  background-color: #00a1cc;
-  border: 0.1rem solid #00a1cc;
-  border-radius: .4rem;
-  color: #fff;
-  cursor: pointer;
-  display: inline-block;
-  font-size: 1.1rem;
-  font-weight: 700;
-  height: 3.8rem;
-  letter-spacing: .1rem;
-  line-height: 3.8rem;
-  padding: 0 3.0rem;
-  text-align: center;
-  text-decoration: none;
-  text-transform: uppercase;
-  white-space: nowrap;
-}
-
-.button:focus, .button:hover,
-button:focus,
-button:hover,
-input[type='button']:focus,
-input[type='button']:hover,
-input[type='reset']:focus,
-input[type='reset']:hover,
-input[type='submit']:focus,
-input[type='submit']:hover {
-  background-color: #606c76;
-  border-color: #606c76;
-  color: #fff;
-  outline: 0;
-}
-
-.button[disabled],
-button[disabled],
-input[type='button'][disabled],
-input[type='reset'][disabled],
-input[type='submit'][disabled] {
-  cursor: default;
-  opacity: .5;
-}
-
-.button[disabled]:focus, .button[disabled]:hover,
-button[disabled]:focus,
-button[disabled]:hover,
-input[type='button'][disabled]:focus,
-input[type='button'][disabled]:hover,
-input[type='reset'][disabled]:focus,
-input[type='reset'][disabled]:hover,
-input[type='submit'][disabled]:focus,
-input[type='submit'][disabled]:hover {
-  background-color: #00a1cc;
-  border-color: #00a1cc;
-}
-
-.button.button-outline,
-button.button-outline,
-input[type='button'].button-outline,
-input[type='reset'].button-outline,
-input[type='submit'].button-outline {
-  background-color: transparent;
-  color: #00a1cc;
-}
-
-.button.button-outline:focus, .button.button-outline:hover,
-button.button-outline:focus,
-button.button-outline:hover,
-input[type='button'].button-outline:focus,
-input[type='button'].button-outline:hover,
-input[type='reset'].button-outline:focus,
-input[type='reset'].button-outline:hover,
-input[type='submit'].button-outline:focus,
-input[type='submit'].button-outline:hover {
-  background-color: transparent;
-  border-color: #606c76;
-  color: #606c76;
-}
-
-.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
-button.button-outline[disabled]:focus,
-button.button-outline[disabled]:hover,
-input[type='button'].button-outline[disabled]:focus,
-input[type='button'].button-outline[disabled]:hover,
-input[type='reset'].button-outline[disabled]:focus,
-input[type='reset'].button-outline[disabled]:hover,
-input[type='submit'].button-outline[disabled]:focus,
-input[type='submit'].button-outline[disabled]:hover {
-  border-color: inherit;
-  color: #00a1cc;
-}
-
-.button.button-clear,
-button.button-clear,
-input[type='button'].button-clear,
-input[type='reset'].button-clear,
-input[type='submit'].button-clear {
-  background-color: transparent;
-  border-color: transparent;
-  color: #00a1cc;
-}
-
-.button.button-clear:focus, .button.button-clear:hover,
-button.button-clear:focus,
-button.button-clear:hover,
-input[type='button'].button-clear:focus,
-input[type='button'].button-clear:hover,
-input[type='reset'].button-clear:focus,
-input[type='reset'].button-clear:hover,
-input[type='submit'].button-clear:focus,
-input[type='submit'].button-clear:hover {
-  background-color: transparent;
-  border-color: transparent;
-  color: #606c76;
-}
-
-.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
-button.button-clear[disabled]:focus,
-button.button-clear[disabled]:hover,
-input[type='button'].button-clear[disabled]:focus,
-input[type='button'].button-clear[disabled]:hover,
-input[type='reset'].button-clear[disabled]:focus,
-input[type='reset'].button-clear[disabled]:hover,
-input[type='submit'].button-clear[disabled]:focus,
-input[type='submit'].button-clear[disabled]:hover {
-  color: #00a1cc;
-}
-
-code {
-  background: #f4f5f6;
-  border-radius: .4rem;
-  font-size: 86%;
-  margin: 0 .2rem;
-  padding: .2rem .5rem;
-  white-space: nowrap;
-}
-
-pre {
-  background: #f4f5f6;
-  border-left: 0.3rem solid #00a1cc;
-  overflow-y: hidden;
-}
-
-pre > code {
-  border-radius: 0;
-  display: block;
-  padding: 1rem 1.5rem;
-  white-space: pre;
-}
-
-hr {
-  border: 0;
-  border-top: 0.1rem solid #f4f5f6;
-  margin: 3.0rem 0;
-}
-
-input[type='email'],
-input[type='number'],
-input[type='password'],
-input[type='search'],
-input[type='tel'],
-input[type='text'],
-input[type='url'],
-textarea,
-select {
-  -webkit-appearance: none;
-     -moz-appearance: none;
-          appearance: none;
-  background-color: transparent;
-  border: 0.1rem solid #d1d1d1;
-  border-radius: .4rem;
-  box-shadow: none;
-  box-sizing: inherit;
-  height: 3.8rem;
-  padding: .6rem 1.0rem;
-  width: 100%;
-}
-
-input[type='email']:focus,
-input[type='number']:focus,
-input[type='password']:focus,
-input[type='search']:focus,
-input[type='tel']:focus,
-input[type='text']:focus,
-input[type='url']:focus,
-textarea:focus,
-select:focus {
-  border-color: #00a1cc;
-  outline: 0;
-}
-
-select {
-  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
-  padding-right: 3.0rem;
-}
-
-select:focus {
-  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#00a1cc" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
-}
-
-textarea {
-  min-height: 6.5rem;
-}
-
-label,
-legend {
-  display: block;
-  font-size: 1.6rem;
-  font-weight: 700;
-  margin-bottom: .5rem;
-}
-
-fieldset {
-  border-width: 0;
-  padding: 0;
-}
-
-input[type='checkbox'],
-input[type='radio'] {
-  display: inline;
-}
-
-.label-inline {
-  display: inline-block;
-  font-weight: normal;
-  margin-left: .5rem;
-}
-
-.container {
-  margin: 0 auto;
-  max-width: 112.0rem;
-  padding: 0 2.0rem;
-  position: relative;
-  width: 100%;
-}
-
-.row {
-  display: flex;
-  flex-direction: column;
-  padding: 0;
-  width: 100%;
-}
-
-.row.row-no-padding {
-  padding: 0;
-}
-
-.row.row-no-padding > .column {
-  padding: 0;
-}
-
-.row.row-wrap {
-  flex-wrap: wrap;
-}
-
-.row.row-top {
-  align-items: flex-start;
-}
-
-.row.row-bottom {
-  align-items: flex-end;
-}
-
-.row.row-center {
-  align-items: center;
-}
-
-.row.row-stretch {
-  align-items: stretch;
-}
-
-.row.row-baseline {
-  align-items: baseline;
-}
-
-.row .column {
-  display: block;
-  flex: 1 1 auto;
-  margin-left: 0;
-  max-width: 100%;
-  width: 100%;
-}
-
-.row .column.column-offset-10 {
-  margin-left: 10%;
-}
-
-.row .column.column-offset-20 {
-  margin-left: 20%;
-}
-
-.row .column.column-offset-25 {
-  margin-left: 25%;
-}
-
-.row .column.column-offset-33, .row .column.column-offset-34 {
-  margin-left: 33.3333%;
-}
-
-.row .column.column-offset-50 {
-  margin-left: 50%;
-}
-
-.row .column.column-offset-66, .row .column.column-offset-67 {
-  margin-left: 66.6666%;
-}
-
-.row .column.column-offset-75 {
-  margin-left: 75%;
-}
-
-.row .column.column-offset-80 {
-  margin-left: 80%;
-}
-
-.row .column.column-offset-90 {
-  margin-left: 90%;
-}
-
-.row .column.column-10 {
-  flex: 0 0 10%;
-  max-width: 10%;
-}
-
-.row .column.column-20 {
-  flex: 0 0 20%;
-  max-width: 20%;
-}
-
-.row .column.column-25 {
-  flex: 0 0 25%;
-  max-width: 25%;
-}
-
-.row .column.column-33, .row .column.column-34 {
-  flex: 0 0 33.3333%;
-  max-width: 33.3333%;
-}
-
-.row .column.column-40 {
-  flex: 0 0 40%;
-  max-width: 40%;
-}
-
-.row .column.column-50 {
-  flex: 0 0 50%;
-  max-width: 50%;
-}
-
-.row .column.column-60 {
-  flex: 0 0 60%;
-  max-width: 60%;
-}
-
-.row .column.column-66, .row .column.column-67 {
-  flex: 0 0 66.6666%;
-  max-width: 66.6666%;
-}
-
-.row .column.column-75 {
-  flex: 0 0 75%;
-  max-width: 75%;
-}
-
-.row .column.column-80 {
-  flex: 0 0 80%;
-  max-width: 80%;
-}
-
-.row .column.column-90 {
-  flex: 0 0 90%;
-  max-width: 90%;
-}
-
-.row .column .column-top {
-  align-self: flex-start;
-}
-
-.row .column .column-bottom {
-  align-self: flex-end;
-}
-
-.row .column .column-center {
-  -ms-grid-row-align: center;
-      align-self: center;
-}
-
-@media (min-width: 40rem) {
-  .row {
-    flex-direction: row;
-    margin-left: -1.0rem;
-    width: calc(100% + 2.0rem);
-  }
-  .row .column {
-    margin-bottom: inherit;
-    padding: 0 1.0rem;
-  }
-}
-
-a {
-  color: #00a1cc;
-  text-decoration: none;
-}
-
-a:focus, a:hover {
-  color: #606c76;
-}
-
-dl,
-ol,
-ul {
-  list-style: none;
-  margin-top: 0;
-  padding-left: 0;
-}
-
-dl dl,
-dl ol,
-dl ul,
-ol dl,
-ol ol,
-ol ul,
-ul dl,
-ul ol,
-ul ul {
-  font-size: 90%;
-  margin: 1.5rem 0 1.5rem 3.0rem;
-}
-
-ol {
-  list-style: decimal inside;
-}
-
-ul {
-  list-style: circle inside;
-}
-
-.button,
-button,
-dd,
-dt,
-li {
-  margin-bottom: 1.0rem;
-}
-
-fieldset,
-input,
-select,
-textarea {
-  margin-bottom: 1.5rem;
-}
-
-blockquote,
-dl,
-figure,
-form,
-ol,
-p,
-pre,
-table,
-ul {
-  margin-bottom: 2.5rem;
-}
-
-table {
-  border-spacing: 0;
-  width: 100%;
-}
-
-td,
-th {
-  border-bottom: 0.1rem solid #e1e1e1;
-  padding: 1.2rem 1.5rem;
-  text-align: left;
-}
-
-td:first-child,
-th:first-child {
-  padding-left: 0;
-}
-
-td:last-child,
-th:last-child {
-  padding-right: 0;
-}
-
-b,
-strong {
-  font-weight: bold;
-}
-
-p {
-  margin-top: 0;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  font-weight: 300;
-  letter-spacing: -.1rem;
-  margin-bottom: 2.0rem;
-  margin-top: 0;
-}
-
-h1 {
-  font-size: 4.6rem;
-  line-height: 1.2;
-}
-
-h2 {
-  font-size: 3.6rem;
-  line-height: 1.25;
-}
-
-h3 {
-  font-size: 2.8rem;
-  line-height: 1.3;
-}
-
-h4 {
-  font-size: 2.2rem;
-  letter-spacing: -.08rem;
-  line-height: 1.35;
-}
-
-h5 {
-  font-size: 1.8rem;
-  letter-spacing: -.05rem;
-  line-height: 1.5;
-}
-
-h6 {
-  font-size: 1.6rem;
-  letter-spacing: 0;
-  line-height: 1.4;
-}
-
-img {
-  max-width: 100%;
-}
-
-.clearfix:after {
-  clear: both;
-  content: ' ';
-  display: table;
-}
-
-.float-left {
-  float: left;
-}
-
-.float-right {
-  float: right;
-}
-
diff --git a/common/static/lib/css/multiple-select.min.css b/common/static/lib/css/multiple-select.min.css
deleted file mode 100644
index 5a6f1b19..00000000
--- a/common/static/lib/css/multiple-select.min.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
-  * multiple-select - Multiple select is a jQuery plugin to select multiple elements with checkboxes :).
-  *
-  * @version v1.5.2
-  * @homepage http://multiple-select.wenzhixin.net.cn
-  * @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
-  * @license MIT
-  */
-
-@charset "UTF-8";.ms-offscreen{clip:rect(0 0 0 0)!important;width:1px!important;height:1px!important;border:0!important;margin:0!important;padding:0!important;overflow:hidden!important;position:absolute!important;outline:0!important;left:auto!important;top:auto!important}.ms-parent{display:inline-block;position:relative;vertical-align:middle}.ms-choice{display:block;width:100%;height:26px;padding:0;overflow:hidden;cursor:pointer;border:1px solid #aaa;text-align:left;white-space:nowrap;line-height:26px;color:#444;text-decoration:none;border-radius:4px;background-color:#fff}.ms-choice.disabled{background-color:#f4f4f4;background-image:none;border:1px solid #ddd;cursor:default}.ms-choice>span{position:absolute;top:0;left:0;right:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;padding-left:8px}.ms-choice>span.placeholder{color:#999}.ms-choice>div.icon-close{position:absolute;top:0;right:16px;height:100%;width:16px}.ms-choice>div.icon-close:before{content:'×';color:#888;font-weight:bold;position:absolute;top:50%;margin-top:-14px}.ms-choice>div.icon-close:hover:before{color:#333}.ms-choice>div.icon-caret{position:absolute;width:0;height:0;top:50%;right:8px;margin-top:-2px;border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px}.ms-choice>div.icon-caret.open{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.ms-drop{width:auto;min-width:100%;overflow:hidden;display:none;margin-top:-1px;padding:0;position:absolute;z-index:1000;background:#fff;color:#000;border:1px solid #aaa;border-radius:4px}.ms-drop.bottom{top:100%;box-shadow:0 4px 5px rgba(0,0,0,0.15)}.ms-drop.top{bottom:100%;box-shadow:0 -4px 5px rgba(0,0,0,0.15)}.ms-search{display:inline-block;margin:0;min-height:26px;padding:2px;position:relative;white-space:nowrap;width:100%;z-index:10000;box-sizing:border-box}.ms-search input{width:100%;height:auto!important;min-height:24px;padding:0 5px;margin:0;outline:0;font-family:sans-serif;border:1px solid #aaa;border-radius:5px;box-shadow:none}.ms-drop ul{overflow:auto;margin:0;padding:0}.ms-drop ul>li{list-style:none;display:list-item;background-image:none;position:static;padding:.25rem 8px}.ms-drop ul>li .disabled{font-weight:normal!important;opacity:.35;filter:Alpha(Opacity=35);cursor:default}.ms-drop ul>li.multiple{display:block;float:left}.ms-drop ul>li.group{clear:both}.ms-drop ul>li.multiple label{width:100%;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ms-drop ul>li label{position:relative;padding-left:1.25rem;margin-bottom:0;font-weight:normal;display:block;white-space:nowrap;cursor:pointer}.ms-drop ul>li label.optgroup{font-weight:bold}.ms-drop ul>li.hide-radio{padding:0}.ms-drop ul>li.hide-radio:focus,.ms-drop ul>li.hide-radio:hover{background-color:#f8f9fa}.ms-drop ul>li.hide-radio.selected{color:#fff;background-color:#007bff}.ms-drop ul>li.hide-radio label{margin-bottom:0;padding:5px 8px}.ms-drop ul>li.hide-radio input{display:none}.ms-drop ul>li.option-level-1 label{padding-left:28px}.ms-drop input[type="radio"],.ms-drop input[type="checkbox"]{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.ms-drop .ms-no-results{display:none}
\ No newline at end of file
diff --git a/common/static/lib/css/neo.css b/common/static/lib/css/neo.css
new file mode 100644
index 00000000..2f2fcbce
--- /dev/null
+++ b/common/static/lib/css/neo.css
@@ -0,0 +1,166 @@
+.markdownx-preview h1 {
+    font-size: 2.5em;
+}
+
+.markdownx-preview h2 {
+    font-size: 2.0em;
+}
+
+.markdownx-preview h3 {
+    font-size: 1.6em;
+}
+
+.markdownx-preview blockquote {
+    border-left: lightgray solid 0.4em;
+    padding-left: 0.4em;
+}
+
+.collection-item-position-edit {
+    float: right;
+}
+
+.collection-item-position-edit a {
+    cursor: pointer;
+    color: #ccc;
+}
+
+.action-icon svg {
+    cursor: pointer;
+    fill: #ccc;
+    height: 12px;
+    vertical-align: text-bottom;
+}
+
+.entity-list__entity-img-wrapper {
+    position: relative;
+}
+
+.entity-list__entity-action-icon {
+    position: absolute;
+    top:0;
+    right:0;
+    mix-blend-mode: hard-light;
+    text-stroke: 1px black;
+    background-color: lightgray;
+    border-radius: 0 0 0 8px;
+    padding: 0 4px;
+    cursor: pointer;
+}
+
+
+/***** MODAL DIALOG ****/
+#modal {
+    /* Underlay covers entire screen. */
+    position: fixed;
+    top:0px;
+    bottom: 0px;
+    left:0px;
+    right:0px;
+    background-color:rgba(0,0,0,0.5);
+    z-index:1000;
+
+    /* Flexbox centers the .modal-content vertically and horizontally */
+    display:flex;
+    flex-direction:column;
+    align-items:center;
+
+    /* Animate when opening */
+    animation-name: fadeIn;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+#modal > .modal-underlay {
+    /* underlay takes up the entire viewport. This is only
+    required if you want to click to dismiss the popup */
+    position: absolute;
+    z-index: -1;
+    .collection_list_position_edittop:0px;
+
+
+    bottom:0px;
+    left: 0px;
+    right: 0px;
+}
+
+#modal > .modal-content {
+    /* Position visible dialog near the top of the window */
+    margin-top:10vh;
+
+    /* Sizing for visible dialog */
+    width:80%;
+    max-width:600px;
+
+    /* Display properties for visible dialog*/
+    background-color: #f7f7f7;
+    padding: 20px 20px 10px 20px;
+    color: #606c76;
+
+    /* Animate when opening */
+    animation-name:zoomIn;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+#modal.closing {
+    /* Animate when closing */
+    animation-name: fadeOut;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+#modal.closing > .modal-content {
+    /* Aniate when closing */
+    animation-name: zoomOut;
+    animation-duration:150ms;
+    animation-timing-function: ease;
+}
+
+@keyframes fadeIn {
+    0% {opacity: 0;}
+    100% {opacity: 1;}
+} 
+
+@keyframes fadeOut {
+    0% {opacity: 1;}
+    100% {opacity: 0;}
+} 
+
+@keyframes zoomIn {
+    0% {transform: scale(0.9);}
+    100% {transform: scale(1);}
+} 
+
+@keyframes zoomOut {
+    0% {transform: scale(1);}
+    100% {transform: scale(0.9);}
+}
+
+#modal .add-to-list-modal__head {
+  margin-bottom: 20px;
+}
+
+#modal .add-to-list-modal__head::after {
+  content: ' ';
+  clear: both;
+  display: table;
+}
+
+#modal .add-to-list-modal__title {
+  font-weight: bold;
+  font-size: 1.2em;
+  float: left;
+}
+
+#modal .add-to-list-modal__close-button {
+  float: right;
+  cursor: pointer;
+}
+
+#modal .add-to-list-modal__confirm-button {
+  float: right;
+}
+
+#modal li, #modal ul, #modal label {
+  display: inline;
+}
diff --git a/common/static/lib/js/hyperscript-0.9.5.min.js b/common/static/lib/js/hyperscript-0.9.5.min.js
new file mode 100644
index 00000000..a17cb5e1
--- /dev/null
+++ b/common/static/lib/js/hyperscript-0.9.5.min.js
@@ -0,0 +1,2 @@
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e||self)._hyperscript=t()}(this,function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function t(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}function n(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,o(e,t)}function r(e){return r=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},r(e)}function o(e,t){return o=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},o(e,t)}function a(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(e){return!1}}function i(e,t,n){return i=a()?Reflect.construct:function(e,t,n){var r=[null];r.push.apply(r,t);var a=new(Function.bind.apply(e,r));return n&&o(a,n.prototype),a},i.apply(null,arguments)}function u(e){var t="function"==typeof Map?new Map:void 0;return u=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return i(e,arguments,r(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),o(n,e)},u(e)}function l(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function s(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(n)return(n=n.call(e)).next.bind(n);if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return l(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0;return function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function c(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function f(e,t){var n=e[t];if(n)return n;var r={};return e[t]=r,r}function m(e,t){return new(e.bind.apply(e,[e].concat(t)))}var p,d=globalThis,v=function(e){function n(e,t,n){this._css=e,this.relativeToElement=t,this.escape=n}var r=n.prototype;return r.contains=function(e){for(var t,n=s(this);!(t=n()).done;)if(t.value.contains(e))return!0;return!1},r[e]=function(){return this.selectMatches()[Symbol.iterator]()},r.selectMatches=function(){return T.getRootNode(this.relativeToElement).querySelectorAll(this.css)},t(n,[{key:"css",get:function(){return this.escape?T.escapeSelector(this._css):this._css}},{key:"className",get:function(){return this._css.substr(1)}},{key:"id",get:function(){return this.className()}},{key:"length",get:function(){return this.selectMatches().length}}]),n}(Symbol.iterator),h=function(){var e={"+":"PLUS","-":"MINUS","*":"MULTIPLY","/":"DIVIDE",".":"PERIOD","..":"ELLIPSIS","\\":"BACKSLASH",":":"COLON","%":"PERCENT","|":"PIPE","!":"EXCLAMATION","?":"QUESTION","#":"POUND","&":"AMPERSAND",$:"DOLLAR",";":"SEMI",",":"COMMA","(":"L_PAREN",")":"R_PAREN","<":"L_ANG",">":"R_ANG","<=":"LTE_ANG",">=":"GTE_ANG","==":"EQ","===":"EQQ","!=":"NEQ","!==":"NEQQ","{":"L_BRACE","}":"R_BRACE","[":"L_BRACKET","]":"R_BRACKET","=":"EQUALS"};function t(e){return i(e)||a(e)||"-"===e||"_"===e||":"===e}function n(e){return i(e)||a(e)||"-"===e||"_"===e||":"===e}function r(e){return" "===e||"\t"===e||o(e)}function o(e){return"\r"===e||"\n"===e}function a(e){return e>="0"&&e<="9"}function i(e){return e>="a"&&e<="z"||e>="A"&&e<="Z"}function u(e,t){return"_"===e||"$"===e}function l(e,t,n){o();var r=null;function o(){for(;"WHITESPACE"===f(0,!0).type;)t.push(e.shift())}function a(e,t){E.raiseParseError(e,t)}function i(e){if(m()&&m().op&&m().value===e)return s()}function u(e,t,n,r){if(m()&&m().type&&[e,t,n,r].indexOf(m().type)>=0)return s()}function l(e,t){if(-1===p.indexOf(e))return t=t||"IDENTIFIER",m()&&m().value===e&&m().type===t?s():void 0}function s(){var n=e.shift();return t.push(n),r=n,o(),n}function c(n,r){for(var a=[],i=f(0,!0);!(null!=r&&i.type===r||null!=n&&i.value===n||"EOF"===i.type);){var u=e.shift();t.push(u),a.push(i),i=f(0,!0)}return o(),a}function f(t,n){var r,o=0;do{if(!n)for(;e[o]&&"WHITESPACE"===e[o].type;)o++;r=e[o],t--,o++}while(t>-1);return r||{type:"EOF",value:"<<<EOF>>>"}}function m(){return f(0)}var p=[];return{pushFollow:function(e){p.push(e)},popFollow:function(){p.pop()},clearFollow:function(){var e=p;return p=[],e},restoreFollow:function(e){p=e},matchAnyToken:function(e,t,n){for(var r=0;r<arguments.length;r++){var o=arguments[r],a=l(o);if(a)return a}},matchAnyOpToken:function(e,t,n){for(var r=0;r<arguments.length;r++){var o=arguments[r],a=i(o);if(a)return a}},matchOpToken:i,requireOpToken:function(e){var t=i(e);if(t)return t;a(this,"Expected '"+e+"' but found '"+m().value+"'")},matchTokenType:u,requireTokenType:function(e,t,n,r){var o=u(e,t,n,r);if(o)return o;a(this,"Expected one of "+JSON.stringify([e,t,n]))},consumeToken:s,peekToken:function(t,n,r){return e[n]&&e[n].value===t&&e[n].type===r},matchToken:l,requireToken:function(e,t){var n=l(e,t);if(n)return n;a(this,"Expected '"+e+"' but found '"+m().value+"'")},list:e,consumed:t,source:n,hasMore:function(){return e.length>0},currentToken:m,lastMatch:function(){return r},token:f,consumeUntil:c,consumeUntilWhitespace:function(){return c(null,"WHITESPACE")},lastWhitespace:function(){return t[t.length-1]&&"WHITESPACE"===t[t.length-1].type?t[t.length-1].value:""},sourceFor:function(){return n.substring(this.startToken.start,this.endToken.end)},lineFor:function(){return n.split("\n")[this.startToken.line-1]}}}function s(e){if(e.length>0){var t=e[e.length-1];if("IDENTIFIER"===t.type||"CLASS_REF"===t.type||"ID_REF"===t.type)return!1;if(t.op&&(">"===t.value||")"===t.value))return!1}return!0}return{tokenize:function(c,f){var m,p=[],d=c,v=0,h=0,E=1,y="<START>",T=0;function k(){return f&&0===T}for(;v<d.length;)if("-"!==C()||"-"!==A()||!r(F())&&""!==F())if(r(C()))p.push(D());else if(M()||"."!==C()||!i(A())&&"{"!==A())if(M()||"#"!==C()||!i(A())&&"{"!==A())if("["===C()&&"@"===A())p.push(q());else if("@"===C())p.push(w());else if("*"===C()&&i(A()))p.push(S());else if(i(C())||!k()&&u(C()))p.push(I());else if(a(C()))p.push(R());else if(k()||'"'!==C()&&"`"!==C())if(k()||"'"!==C()){if(e[C()])"$"===y&&"{"===C()&&T++,"}"===C()&&T--,p.push(O());else if(k()||"`"===(m=C())||"^"===m)p.push(g("RESERVED",P()));else if(v<d.length)throw Error("Unknown token: "+C()+" ")}else s(p)?p.push(L()):p.push(O());else p.push(L());else p.push(N());else p.push(b());else x();return l(p,[],d);function g(e,t){return{type:e,value:t,start:v,end:v+1,column:h,line:E}}function x(){for(;C()&&!o(C());)P();P()}function b(){var e=g("CLASS_REF"),n=P();if("{"===C()){for(e.template=!0,n+=P();C()&&"}"!==C();)n+=P();if("}"!==C())throw Error("Unterminated class reference");n+=P()}else for(;t(C());)n+=P();return e.value=n,e.end=v,e}function q(){for(var e=g("ATTRIBUTE_REF"),t=P();v<d.length&&"]"!==C();)t+=P();return"]"===C()&&(t+=P()),e.value=t,e.end=v,e}function w(){for(var e=g("ATTRIBUTE_REF"),t=P();n(C());)t+=P();return e.value=t,e.end=v,e}function S(){for(var e=g("STYLE_REF"),t=P();i(C())||"-"===C();)t+=P();return e.value=t,e.end=v,e}function N(){var e=g("ID_REF"),t=P();if("{"===C()){for(e.template=!0,t+=P();C()&&"}"!==C();)t+=P();if("}"!==C())throw Error("Unterminated id reference");P()}else for(;n(C());)t+=P();return e.value=t,e.end=v,e}function I(){for(var e=g("IDENTIFIER"),t=P();i(C())||a(C())||u(C());)t+=P();return"!"===C()&&"beep"===t&&(t+=P()),e.value=t,e.end=v,e}function R(){for(var e=g("NUMBER"),t=P();a(C());)t+=P();for("."===C()&&a(A())&&(t+=P());a(C());)t+=P();return e.value=t,e.end=v,e}function O(){for(var t=(r=void 0,(r=g(void 0,void 0)).op=!0,r),n=P();C()&&e[n+C()];)n+=P();var r;return t.type=e[n],t.value=n,t.end=v,t}function L(){for(var e,t=g("STRING"),n=P(),r="";C()&&C()!==n;)if("\\"===C()){P();var o=P();r+="b"===o?"\b":"f"===o?"\f":"n"===o?"\n":"r"===o?"\r":"t"===o?"\t":"v"===o?"\v":o}else r+=P();if(C()!==n)throw Error("Unterminated string at [Line: "+(e=t).line+", Column: "+e.column+"]");return P(),t.value=r,t.end=v,t.template="`"===n,t}function C(){return d.charAt(v)}function A(){return d.charAt(v+1)}function F(){return d.charAt(v+2)}function P(){return y=C(),v++,h++,y}function M(){return i(y)||a(y)||")"===y||'"'===y||"'"===y||"`"===y||"}"===y||"]"===y}function D(){for(var e=g("WHITESPACE"),t="";C()&&r(C());)o(C())&&(h=0,E++),t+=P();return e.value=t,e.end=v,e}},makeTokensObject:l}}(),E=function(){var e={},t={},n={},r=[],o=[];function a(e,t,n){e.startToken=t,e.sourceFor=n.sourceFor,e.lineFor=n.lineFor,e.programSource=n.source}function i(t,n,r){return void 0===r&&(r=void 0),function(r){var o=e[t];if(o){var i=n.currentToken(),u=o(E,T,n,r);if(u)for(a(u,i,n),u.endToken=u.endToken||n.lastMatch(),r=u.root;null!=r;)a(r,i,n),r=r.root;return u}}(r)}function u(e,t,n,r){var o=i(e,t,r);return o||c(t,n||"Expected "+e),o}function l(e,t){for(var n=0;n<e.length;n++){var r=i(e[n],t);if(r)return r}}function s(t,n){e[t]=n}function c(e,t){t=(t||"Unexpected Token : "+e.currentToken().value)+"\n\n"+function(e){var t=e.currentToken(),n=e.source.split("\n"),r=n[t&&t.line?t.line-1:n.length-1];return r+"\n"+" ".repeat(t&&t.line?t.column:r.length-1)+"^^\n\n"}(e);var n=new Error(t);throw n.tokens=e,n}function f(e){return t[e.value]}function m(e){return n[e.value]}return s("feature",function(e,t,r){if(r.matchOpToken("(")){var o=e.requireElement("feature",r);return r.requireOpToken(")"),o}var a=n[r.currentToken().value];if(a)return a(e,t,r)}),s("command",function(e,n,r){if(r.matchOpToken("(")){var o=e.requireElement("command",r);return r.requireOpToken(")"),o}var a,i=t[r.currentToken().value];return i?a=i(e,n,r):"IDENTIFIER"===r.currentToken().type&&(a=e.parseElement("pseudoCommand",r)),a?e.parseElement("indirectStatement",r,a):a}),s("commandList",function(e,t,n){var r=e.parseElement("command",n);if(r){n.matchToken("then");var o=e.parseElement("commandList",n);return o&&(r.next=o),r}}),s("leaf",function(e,t,n){var o=l(r,n);return null==o?i("symbol",n):o}),s("indirectExpression",function(e,t,n,r){for(var a=0;a<o.length;a++){var i=o[a];r.endToken=n.lastMatch();var u=e.parseElement(i,n,r);if(u)return u}return r}),s("indirectStatement",function(e,t,n,r){if(n.matchToken("unless")){r.endToken=n.lastMatch();var o={type:"unlessStatementModifier",args:[e.requireElement("expression",n)],op:function(e,t){return t?this.next:r},execute:function(e){return t.unifiedExec(this,e)}};return r.parent=o,o}return r}),s("primaryExpression",function(e,t,n){var r=e.parseElement("leaf",n);if(r)return e.parseElement("indirectExpression",n,r);e.raiseParseError(n,"Unexpected value: "+n.currentToken().value)}),{setParent:function e(t,n){"object"==typeof t&&(t.parent=n,"object"==typeof n&&(n.children=n.children||new Set,n.children.add(t)),e(t.next,n))},requireElement:u,parseElement:i,featureStart:m,commandStart:f,commandBoundary:function(e){return!("end"!=e.value&&"then"!=e.value&&"else"!=e.value&&"otherwise"!=e.value&&")"!=e.value&&!f(e)&&!m(e)&&"EOF"!=e.type)},parseAnyOf:l,parseHyperScript:function(e){var t=i("hyperscript",e);if(e.hasMore()&&c(e),t)return t},raiseParseError:c,addGrammarElement:s,addCommand:function(n,r){var o=n+"Command",a=function(e,t,n){var a=r(e,t,n);if(a)return a.type=o,a.execute=function(e){return e.meta.command=a,t.unifiedExec(this,e)},a};e[o]=a,t[n]=a},addFeature:function(t,r){var o=t+"Feature",a=function(e,n,a){var i=r(e,n,a);if(i)return i.isFeature=!0,i.keyword=t,i.type=o,i};e[o]=a,n[t]=a},addLeafExpression:function(e,t){r.push(e),s(e,t)},addIndirectExpression:function(e,t){o.push(e),s(e,t)},parseStringTemplate:function(e){var t=[""];do{if(t.push(e.lastWhitespace()),"$"===e.currentToken().value){e.consumeToken();var n=e.matchOpToken("{");t.push(u("expression",e)),n&&e.requireOpToken("}"),t.push("")}else if("\\"===e.currentToken().value)e.consumeToken(),e.consumeToken();else{var r=e.consumeToken();t[t.length-1]+=r?r.value:""}}while(e.hasMore());return t.push(e.lastWhitespace()),t},ensureTerminated:function(e){for(var t={type:"implicitReturn",op:function(e){return e.meta.returned=!0,e.meta.resolve&&e.meta.resolve(),T.HALT},execute:function(e){}},n=e;n.next;)n=n.next;n.next=t}}}(),y={dynamicResolvers:[function(e,t){if("Fixed"===e)return Number(t).toFixed();if(0===e.indexOf("Fixed:")){var n=e.split(":")[1];return Number(t).toFixed(parseInt(n))}}],String:function(e){return e.toString?e.toString():""+e},Int:function(e){return parseInt(e)},Float:function(e){return parseFloat(e)},Number:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return Number(e)}),Date:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return new Date(e)}),Array:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return Array.from(e)}),JSON:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return JSON.stringify(e)}),Object:function(e){return e instanceof String&&(e=e.toString()),"string"==typeof e?JSON.parse(e):c({},e)}},T=function(){function e(e,t){var n=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return n&&n.call(e,t)}function t(e,t,n,r){(n=n||{}).sender=r;var o=function(e,t){var n;return d.Event&&"function"==typeof d.Event?(n=new Event(e,{bubbles:!0,cancelable:!0})).detail=t:(n=document.createEvent("CustomEvent")).initCustomEvent(e,!0,!0,t),n}(t,n);return e.dispatchEvent(o)}function r(e){return Array.isArray(e)||"undefined"!=typeof NodeList&&(e instanceof NodeList||e instanceof HTMLCollection)}function o(e){return e instanceof v||r(e)}function a(e,t){if(null==e);else if(function(e){return"object"==typeof e&&Symbol.iterator in e&&"function"==typeof e[Symbol.iterator]}(e))for(var n,o=s(e);!(n=o()).done;)t(n.value);else if(r(e))for(var a=0;a<e.length;a++)t(e[a]);else t(e)}function i(e){for(var t=0;t<e.length;t++){var n=e[t];if(n.asyncWrapper&&(e[t]=n.value),Array.isArray(n))for(var r=0;r<n.length;r++){var o=n[r];o.asyncWrapper&&(n[r]=o.value)}}}var l={};function m(e,t){var n=[t],r=!1,o=!1;if(e.args)for(var a=0;a<e.args.length;a++){var u=e.args[a];if(null==u)n.push(null);else if(Array.isArray(u)){for(var l=[],s=0;s<u.length;s++){var c=u[s];(f=c?c.evaluate(t):null)&&(f.then?r=!0:f.asyncWrapper&&(o=!0)),l.push(f)}n.push(l)}else if(u.evaluate){var f;(f=u.evaluate(t))&&(f.then?r=!0:f.asyncWrapper&&(o=!0)),n.push(f)}else n.push(u)}return r?new Promise(function(t,r){n=function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n];Array.isArray(r)?t.push(Promise.all(r)):t.push(r)}return t}(n),Promise.all(n).then(function(n){o&&i(n);try{var a=e.op.apply(e,n);t(a)}catch(e){r(e)}}).catch(function(e){r(e)})}):(o&&i(n),e.op.apply(e,n))}var k=null;function g(){return null==k&&(k=p.config.attributes.replace(/ /g,"").split(",")),k}function x(e){for(var t=0;t<g().length;t++){var n=g()[t];if(e.hasAttribute&&e.hasAttribute(n))return e.getAttribute(n)}return e instanceof HTMLScriptElement&&"text/hyperscript"===e.type?e.innerText:null}var b=new WeakMap;function q(e){var t=b.get(e);return void 0===t&&b.set(e,t={}),t}function w(e,t){e&&(c(t,q(e)),w(e.parentElement,t))}function S(e,t,n,r){var o={meta:{parser:E,lexer:h,runtime:T,owner:e,feature:t,iterators:{}},me:n,event:r,target:r?r.target:null,detail:r?r.detail:null,sender:r&&r.detail?r.detail.sender:null,body:"document"in d?document.body:null};return o.meta.ctx=o,w(e,o),o}function N(e){var t=h.tokenize(e);if(E.commandStart(t.currentToken())){var n=E.requireElement("commandList",t);return t.hasMore()&&E.raiseParseError(t),E.ensureTerminated(n),n}if(E.featureStart(t.currentToken())){var r=E.requireElement("hyperscript",t);return t.hasMore()&&E.raiseParseError(t),r}var o=E.requireElement("expression",t);return t.hasMore()&&E.raiseParseError(t),o}function I(e,n){if(!e.closest||!e.closest(p.config.disableSelector)){var r=O(e);if(!r.initialized){var o=x(e);if(o)try{r.initialized=!0,r.script=o;var a=h.tokenize(o),i=E.parseHyperScript(a);if(!i)return;i.apply(n||e,e),setTimeout(function(){t(n||e,"load",{hyperscript:!0})},1)}catch(t){T.triggerEvent(e,"exception",{error:t}),console.error("hyperscript errors were found on the following element:",e,"\n\n",t.message,t.stack)}}}}var R=new WeakMap;function O(e){var t=R.get(e);return void 0===t&&R.set(e,t={}),t}function L(e){var t=e.meta&&e.meta.owner;if(t){var n=O(t),r="elementScope";return e.meta.feature&&e.meta.feature.behavior&&(r=e.meta.feature.behavior+"Scope"),f(n,r)}return{}}function C(e,t,n){if(null!=e){var r=n(e,t);if(void 0!==r)return r;if(o(e)){for(var a,i=[],u=s(e);!(a=u()).done;){var l=n(a.value,t);l&&i.push(l)}return i}}}return{typeCheck:function(e,t,n){return!(null!=e||!n)||Object.prototype.toString.call(e).slice(8,-1)===t},forEach:a,implicitLoop:function(e,t){if(o(e))for(var n,r=s(e);!(n=r()).done;)t(n.value);else t(e)},shouldAutoIterate:o,triggerEvent:t,matchesSelector:e,getScript:x,processNode:function(t){var n=T.getScriptSelector();e(t,n)&&I(t,t),t instanceof HTMLScriptElement&&"text/hyperscript"===t.type&&I(t,document.body),t.querySelectorAll&&a(t.querySelectorAll(n+", [type='text/hyperscript']"),function(e){I(e,e instanceof HTMLScriptElement&&"text/hyperscript"===e.type?document.body:e)})},evaluate:function(e,t,r){var o=function(e){function t(t){var n;return(n=e.call(this)||this).module=t,n}return n(t,e),t.prototype.toString=function(){return this.module.id},t}(u(EventTarget)),a="document"in d?d.document.body:new o(r&&r.module);t=c(S(a,null,a,null),t||{});var i=N(e);return i.execute?(i.execute(t),t.result):i.apply?(i.apply(a,a,r),q(a)):i.evaluate(t)},evaluateNoPromise:function(e,t){var n=e.evaluate(t);if(n.next)throw new Error(e.sourceFor()+" returned a Promise in a context that they are not allowed.");return n},parse:N,getScriptSelector:function(){return g().map(function(e){return"["+e+"]"}).join(", ")},resolveSymbol:function(e,t,n){if("me"===e||"my"===e||"I"===e)return t.me;if("it"===e||"its"===e)return t.result;if("you"===e||"your"===e||"yourself"===e)return t.beingTold;if("global"===n)return d[e];if("element"===n)return L(t)[e];if("local"===n)return t[e];if(t.meta&&t.meta.context){var r=t.meta.context[e];if(void 0!==r)return r}var o=t[e];return void 0!==o||void 0!==(o=L(t)[e])?o:d[e]},setSymbol:function(e,t,n,r){if("global"===n)d[e]=r;else if("element"===n)(o=L(t))[e]=r;else if("local"===n)t[e]=r;else{var o,a=t[e];void 0!==a?t[e]=r:void 0!==(a=(o=L(t))[e])?o[e]=r:t[e]=r}},makeContext:S,findNext:function e(t,n){if(t)return t.resolveNext?t.resolveNext(n):t.next?t.next:e(t.parent,n)},unifiedEval:m,convertValue:function(e,t){for(var n=y.dynamicResolvers,r=0;r<n.length;r++){var o=(0,n[r])(t,e);if(void 0!==o)return o}if(null==e)return null;var a=y[t];if(a)return a(e);throw"Unknown conversion : "+t},unifiedExec:function e(t,n){for(;;){try{var r=m(t,n)}catch(e){if(n.meta.handlingFinally)console.error(" Exception in finally block: ",e),r=l;else{if(T.registerHyperTrace(n,e),n.meta.errorHandler&&!n.meta.handlingError){n.meta.handlingError=!0,n[n.meta.errorSymbol]=e,t=n.meta.errorHandler;continue}n.meta.currentException=e,r=l}}if(null==r)return void console.error(t," did not return a next element to execute! context: ",n);if(r.then)return void r.then(function(t){e(t,n)}).catch(function(t){e({op:function(){throw t}},n)});if(r===l){if(!n.meta.finallyHandler||n.meta.handlingFinally){if(n.meta.onHalt&&n.meta.onHalt(),n.meta.currentException){if(n.meta.reject)return void n.meta.reject(n.meta.currentException);throw n.meta.currentException}return}n.meta.handlingFinally=!0,t=n.meta.finallyHandler}else t=r}},resolveProperty:function(e,t){return C(e,t,function(e,t){return e[t]})},resolveAttribute:function(e,t){return C(e,t,function(e,t){return e.getAttribute&&e.getAttribute(t)})},resolveStyle:function(e,t){return C(e,t,function(e,t){return e.style&&e.style[t]})},resolveComputedStyle:function(e,t){return C(e,t,function(e,t){return getComputedStyle(e).getPropertyValue(t)})},assignToNamespace:function(e,t,n,r){var o;for(o="undefined"!=typeof document&&e===document.body?d:q(e);t.length>0;){var a=t.shift(),i=o[a];null==i&&(o[a]=i={}),o=i}o[n]=r},registerHyperTrace:function(e,t){for(var n=[],r=null;null!=e;)n.push(e),r=e,e=e.meta.caller;null==r.meta.traceMap&&(r.meta.traceMap=new Map),r.meta.traceMap.get(t)||r.meta.traceMap.set(t,{trace:n,print:function(e){(e=e||console.error)("hypertrace /// ");for(var t=0,r=0;r<n.length;r++)t=Math.max(t,n[r].meta.feature.displayName.length);for(r=0;r<n.length;r++){var o=n[r];e("  ->",o.meta.feature.displayName.padEnd(t+2),"-",o.meta.owner)}}})},getHyperTrace:function(e,t){for(var n=e;n.meta.caller;)n=n.meta.caller;if(n.meta.traceMap)return n.meta.traceMap.get(t,[])},getInternalData:O,getHyperscriptFeatures:q,escapeSelector:function(e){return e.replace(/:/g,function(e){return"\\"+e})},nullCheck:function(e,t){if(null==e)throw new Error("'"+t.sourceFor()+"' is null")},isEmpty:function(e){return null==e||0===e.length},doesExist:function(e){if(null==e)return!1;if(o(e))for(var t=s(e);!t().done;)return!0;return!1},getRootNode:function(e){if(e&&e instanceof Node){var t=e.getRootNode();if(t instanceof Document||t instanceof ShadowRoot)return t}return document},getEventQueueFor:function(e,t){var n=O(e),r=n.eventQueues;null==r&&(r=new Map,n.eventQueues=r);var o=r.get(t);return null==o&&r.set(t,o={queue:[],executing:!1}),o},hyperscriptUrl:"document"in d?"undefined"==typeof document&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("_hyperscript_web.min.js",document.baseURI).href:null,HALT:l}}(),k=function(e,t,n){if(t.contains)return t.contains(n);if(t.includes)return t.includes(n);throw Error("The value of "+e.sourceFor()+" does not have a contains or includes method on it")},g=function(e,t,n){if(t.match)return!!t.match(n);if(t.matches)return t.matches(n);throw Error("The value of "+e.sourceFor()+" does not have a match or matches method on it")},x=function(e,t,n,r){var o=t.requireElement("eventName",r),a=t.parseElement("namedArgumentList",r);if("send"===e&&r.matchToken("to")||"trigger"===e&&r.matchToken("on"))var i=t.requireElement("expression",r);else i=t.requireElement("implicitMeTarget",r);var u={eventName:o,details:a,to:i,args:[i,o,a],op:function(e,t,r,o){return n.nullCheck(t,i),n.forEach(t,function(t){n.triggerEvent(t,r,o,e.me)}),n.findNext(u,e)}};return u},b=function(e,t){var n,r="text";return e.matchToken("a")||e.matchToken("an"),e.matchToken("json")||e.matchToken("Object")?r="json":e.matchToken("response")?r="response":e.matchToken("html")?r="html":e.matchToken("text")||(n=t.requireElement("dotOrColonPath",e).evaluate()),{type:r,conversion:n}};E.addLeafExpression("parenthesized",function(e,t,n){if(n.matchOpToken("(")){var r=n.clearFollow();try{var o=e.requireElement("expression",n)}finally{n.restoreFollow(r)}return n.requireOpToken(")"),o}}),E.addLeafExpression("string",function(e,t,n){var r=n.matchTokenType("STRING");if(r){var o,a=r.value;if(r.template){var i=h.tokenize(a,!0);o=e.parseStringTemplate(i)}else o=[];return{type:"string",token:r,args:o,op:function(e){for(var t="",n=1;n<arguments.length;n++){var r=arguments[n];void 0!==r&&(t+=r)}return t},evaluate:function(e){return 0===o.length?a:t.unifiedEval(this,e)}}}}),E.addGrammarElement("nakedString",function(e,t,n){if(n.hasMore()){var r=n.consumeUntilWhitespace();return n.matchTokenType("WHITESPACE"),{type:"nakedString",tokens:r,evaluate:function(e){return r.map(function(e){return e.value}).join("")}}}}),E.addLeafExpression("number",function(e,t,n){var r=n.matchTokenType("NUMBER");if(r){var o=r,a=parseFloat(r.value);return{type:"number",value:a,numberToken:o,evaluate:function(){return a}}}}),E.addLeafExpression("idRef",function(e,t,n){var r=n.matchTokenType("ID_REF");if(r){if(r.template){var o=r.value.substr(2,r.value.length-2),a=h.tokenize(o);return{type:"idRefTemplate",args:[e.requireElement("expression",a)],op:function(e,n){return t.getRootNode(e.me).getElementById(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}var i=r.value.substr(1);return{type:"idRef",css:r.value,value:i,evaluate:function(e){return t.getRootNode(e.me).getElementById(i)}}}}),E.addLeafExpression("classRef",function(e,t,n){var r=n.matchTokenType("CLASS_REF");if(r){if(r.template){var o=r.value.substr(2,r.value.length-2),a=h.tokenize(o);return{type:"classRefTemplate",args:[e.requireElement("expression",a)],op:function(e,t){return new v("."+t,e.me,!0)},evaluate:function(e){return t.unifiedEval(this,e)}}}var i=r.value;return{type:"classRef",css:i,evaluate:function(e){return new v(i,e.me,!0)}}}});var q=function(e,r){function o(t,n,r){var o;return(o=e.call(this,t,n)||this).templateParts=r,o.elements=r.filter(function(e){return e instanceof Element}),o}return n(o,e),o.prototype[r]=function(){this.elements.forEach(function(e,t){return e.dataset.hsQueryId=t});var t=e.prototype[Symbol.iterator].call(this);return this.elements.forEach(function(e){return e.removeAttribute("data-hs-query-id")}),t},t(o,[{key:"css",get:function(){for(var e,t="",n=0,r=s(this.templateParts);!(e=r()).done;){var o=e.value;o instanceof Element?t+="[data-hs-query-id='"+n+++"']":t+=o}return t}}]),o}(v,Symbol.iterator);E.addLeafExpression("queryRef",function(e,t,n){if(n.matchOpToken("<")){var r=n.consumeUntil("/");n.requireOpToken("/"),n.requireOpToken(">");var o=r.map(function(e){return"STRING"===e.type?'"'+e.value+'"':e.value}).join("");if(o.indexOf("$")>=0)var a=!0,i=h.tokenize(o,!0),u=e.parseStringTemplate(i);return{type:"queryRef",css:o,args:u,op:function(e){return a?new q(o,e.me,[].slice.call(arguments,1)):new v(o,e.me)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("attributeRef",function(e,t,n){var r=n.matchTokenType("ATTRIBUTE_REF");if(r){var o=r.value;if(0===o.indexOf("["))var a=o.substring(2,o.length-1);else a=o.substring(1);var i="["+a+"]",u=a.split("="),l=u[0],s=u[1];return s&&0===s.indexOf('"')&&(s=s.substring(1,s.length-1)),{type:"attributeRef",name:l,css:i,value:s,op:function(e){var t=e.beingTold||e.me;if(t)return t.getAttribute(l)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("styleRef",function(e,t,n){var r=n.matchTokenType("STYLE_REF");if(r){var o=r.value.substr(1);return o.startsWith("computed-")?{type:"computedStyleRef",name:o=o.substr("computed-".length),op:function(e){var n=e.beingTold||e.me;if(n)return t.resolveComputedStyle(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}}:{type:"styleRef",name:o,op:function(e){var n=e.beingTold||e.me;if(n)return t.resolveStyle(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("objectKey",function(e,t,n){var r;if(r=n.matchTokenType("STRING"))return{type:"objectKey",key:r.value,evaluate:function(){return r.value}};if(n.matchOpToken("[")){var o=e.parseElement("expression",n);return n.requireOpToken("]"),{type:"objectKey",expr:o,args:[o],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}var a="";do{(r=n.matchTokenType("IDENTIFIER")||n.matchOpToken("-"))&&(a+=r.value)}while(r);return{type:"objectKey",key:a,evaluate:function(){return a}}}),E.addLeafExpression("objectLiteral",function(e,t,n){if(n.matchOpToken("{")){var r=[],o=[];if(!n.matchOpToken("}")){do{var a=e.requireElement("objectKey",n);n.requireOpToken(":");var i=e.requireElement("expression",n);o.push(i),r.push(a)}while(n.matchOpToken(","));n.requireOpToken("}")}return{type:"objectLiteral",args:[r,o],op:function(e,t,n){for(var r={},o=0;o<t.length;o++)r[t[o]]=n[o];return r},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("nakedNamedArgumentList",function(e,t,n){var r=[],o=[];if("IDENTIFIER"===n.currentToken().type)do{var a=n.requireTokenType("IDENTIFIER");n.requireOpToken(":");var i=e.requireElement("expression",n);o.push(i),r.push({name:a,value:i})}while(n.matchOpToken(","));return{type:"namedArgumentList",fields:r,args:[o],op:function(e,t){for(var n={_namedArgList_:!0},o=0;o<t.length;o++)n[r[o].name.value]=t[o];return n},evaluate:function(e){return t.unifiedEval(this,e)}}}),E.addGrammarElement("namedArgumentList",function(e,t,n){if(n.matchOpToken("(")){var r=e.requireElement("nakedNamedArgumentList",n);return n.requireOpToken(")"),r}}),E.addGrammarElement("symbol",function(e,t,n){var r="default";n.matchToken("global")?r="global":n.matchToken("element")||n.matchToken("module")?(r="element",n.matchOpToken("'")&&n.requireToken("s")):n.matchToken("local")&&(r="local");var o=n.matchOpToken(":"),a=n.matchTokenType("IDENTIFIER");if(a){var i=a.value;return o&&(i=":"+i),"default"===r&&(0===i.indexOf("$")&&(r="global"),0===i.indexOf(":")&&(r="element")),{type:"symbol",token:a,scope:r,name:i,evaluate:function(e){return t.resolveSymbol(i,e,r)}}}}),E.addGrammarElement("implicitMeTarget",function(e,t,n){return{type:"implicitMeTarget",evaluate:function(e){return e.beingTold||e.me}}}),E.addLeafExpression("boolean",function(e,t,n){var r=n.matchToken("true")||n.matchToken("false");if(r){var o="true"===r.value;return{type:"boolean",evaluate:function(e){return o}}}}),E.addLeafExpression("null",function(e,t,n){if(n.matchToken("null"))return{type:"null",evaluate:function(e){return null}}}),E.addLeafExpression("arrayLiteral",function(e,t,n){if(n.matchOpToken("[")){var r=[];if(!n.matchOpToken("]")){do{var o=e.requireElement("expression",n);r.push(o)}while(n.matchOpToken(","));n.requireOpToken("]")}return{type:"arrayLiteral",values:r,args:[r],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("blockLiteral",function(e,t,n){if(n.matchOpToken("\\")){var r=[],o=n.matchTokenType("IDENTIFIER");if(o)for(r.push(o);n.matchOpToken(",");)r.push(n.requireTokenType("IDENTIFIER"));n.requireOpToken("-"),n.requireOpToken(">");var a=e.requireElement("expression",n);return{type:"blockLiteral",args:r,expr:a,evaluate:function(e){return function(){for(var t=0;t<r.length;t++)e[r[t].value]=arguments[t];return a.evaluate(e)}}}}}),E.addIndirectExpression("propertyAccess",function(e,t,n,r){if(n.matchOpToken(".")){var o=n.requireTokenType("IDENTIFIER");return e.parseElement("indirectExpression",n,{type:"propertyAccess",root:r,prop:o,args:[r],op:function(e,n){return t.resolveProperty(n,o.value)},evaluate:function(e){return t.unifiedEval(this,e)}})}}),E.addIndirectExpression("of",function(e,t,n,r){if(n.matchToken("of")){for(var o=e.requireElement("unaryExpression",n),a=null,i=r;i.root;)a=i,i=i.root;"symbol"!==i.type&&"attributeRef"!==i.type&&"styleRef"!==i.type&&"computedStyleRef"!==i.type&&e.raiseParseError(n,"Cannot take a property of a non-symbol: "+i.type);var u="attributeRef"===i.type,l="styleRef"===i.type||"computedStyleRef"===i.type;if(u||l)var s=i;var c=i.name,f={type:"ofExpression",prop:i.token,root:o,attribute:s,expression:r,args:[o],op:function(e,n){return u?t.resolveAttribute(n,c):l?"computedStyleRef"===i.type?t.resolveComputedStyle(n,c):t.resolveStyle(n,c):t.resolveProperty(n,c)},evaluate:function(e){return t.unifiedEval(this,e)}};return"attributeRef"===i.type&&(f.attribute=i),a?(a.root=f,a.args=[f]):r=f,e.parseElement("indirectExpression",n,r)}}),E.addIndirectExpression("possessive",function(e,t,n,r){if(!e.possessivesDisabled){var o=n.matchOpToken("'");if(o||"symbol"===r.type&&("my"===r.name||"its"===r.name||"your"===r.name)&&("IDENTIFIER"===n.currentToken().type||"ATTRIBUTE_REF"===n.currentToken().type||"STYLE_REF"===n.currentToken().type)){o&&n.requireToken("s");var a=e.parseElement("attributeRef",n);if(null==a){var i=e.parseElement("styleRef",n);if(null==i)var u=n.requireTokenType("IDENTIFIER")}return e.parseElement("indirectExpression",n,{type:"possessive",root:r,attribute:a||i,prop:u,args:[r],op:function(e,n){if(a)var r=t.resolveAttribute(n,a.name);else r=i?"computedStyleRef"===i.type?t.resolveComputedStyle(n,i.name):t.resolveStyle(n,i.name):t.resolveProperty(n,u.value);return r},evaluate:function(e){return t.unifiedEval(this,e)}})}}}),E.addIndirectExpression("inExpression",function(e,t,n,r){if(n.matchToken("in")){var o={type:"inExpression",root:r,args:[r,e.requireElement("unaryExpression",n)],op:function(e,n,r){var o=[];if(n.css)t.implicitLoop(r,function(e){for(var t=e.querySelectorAll(n.css),r=0;r<t.length;r++)o.push(t[r])});else if(n instanceof Element){var a=!1;if(t.implicitLoop(r,function(e){e.contains(n)&&(a=!0)}),a)return n}else t.implicitLoop(n,function(e){t.implicitLoop(r,function(t){e===t&&o.push(e)})});return o},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",n,o)}}),E.addIndirectExpression("asExpression",function(e,t,n,r){if(n.matchToken("as")){n.matchToken("a")||n.matchToken("an");var o=e.requireElement("dotOrColonPath",n).evaluate();return e.parseElement("indirectExpression",n,{type:"asExpression",root:r,args:[r],op:function(e,n){return t.convertValue(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}})}}),E.addIndirectExpression("functionCall",function(e,t,n,r){if(n.matchOpToken("(")){var o=[];if(!n.matchOpToken(")")){do{o.push(e.requireElement("expression",n))}while(n.matchOpToken(","));n.requireOpToken(")")}if(r.root)var a={type:"functionCall",root:r,argExressions:o,args:[r.root,o],op:function(e,n,o){t.nullCheck(n,r.root);var a=n[r.prop.value];return t.nullCheck(a,r),a.hyperfunc&&o.push(e),a.apply(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}};else a={type:"functionCall",root:r,argExressions:o,args:[r,o],op:function(e,n,o){return t.nullCheck(n,r),n.hyperfunc&&o.push(e),n.apply(null,o)},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",n,a)}}),E.addIndirectExpression("attributeRefAccess",function(e,t,n,r){var o=e.parseElement("attributeRef",n);if(o)return{type:"attributeRefAccess",root:r,attribute:o,args:[r],op:function(e,n){return t.resolveAttribute(n,o.name)},evaluate:function(e){return T.unifiedEval(this,e)}}}),E.addIndirectExpression("arrayIndex",function(e,t,n,r){if(n.matchOpToken("[")){var o=!1,a=!1,i=null,u=null;n.matchOpToken("..")?(o=!0,i=e.requireElement("expression",n)):(i=e.requireElement("expression",n),n.matchOpToken("..")&&(a=!0,"R_BRACKET"!==n.currentToken().type&&(u=e.parseElement("expression",n)))),n.requireOpToken("]");var l={type:"arrayIndex",root:r,prop:i,firstIndex:i,secondIndex:u,args:[r,i,u],op:function(e,t,n,r){return null==t?null:o?(n<0&&(n=t.length+n),t.slice(0,n+1)):a?null!=r?(r<0&&(r=t.length+r),t.slice(n,r+1)):t.slice(n):t[n]},evaluate:function(e){return T.unifiedEval(this,e)}};return E.parseElement("indirectExpression",n,l)}});var w=["em","ex","cap","ch","ic","rem","lh","rlh","vw","vh","vi","vb","vmin","vmax","cm","mm","Q","pc","pt","px"];E.addGrammarElement("postfixExpression",function(e,t,n){var r=e.parseElement("primaryExpression",n),o=n.matchAnyToken.apply(n,w)||n.matchOpToken("%");if(o)return{type:"stringPostfix",postfix:o.value,args:[r],op:function(e,t){return""+t+o.value},evaluate:function(e){return t.unifiedEval(this,e)}};var a=null;if(n.matchToken("s")||n.matchToken("seconds")?a=1e3:(n.matchToken("ms")||n.matchToken("milliseconds"))&&(a=1),a)return{type:"timeExpression",time:r,factor:a,args:[r],op:function(e,t){return t*a},evaluate:function(e){return t.unifiedEval(this,e)}};if(n.matchOpToken(":")){var i=n.requireTokenType("IDENTIFIER"),u=!n.matchOpToken("!");return{type:"typeCheck",typeName:i,nullOk:u,args:[r],op:function(e,n){if(t.typeCheck(n,i.value,u))return n;throw new Error("Typecheck failed!  Expected: "+i.value)},evaluate:function(e){return t.unifiedEval(this,e)}}}return r}),E.addGrammarElement("logicalNot",function(e,t,n){if(n.matchToken("not")){var r=e.requireElement("unaryExpression",n);return{type:"logicalNot",root:r,args:[r],op:function(e,t){return!t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("noExpression",function(e,t,n){if(n.matchToken("no")){var r=e.requireElement("unaryExpression",n);return{type:"noExpression",root:r,args:[r],op:function(e,n){return t.isEmpty(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("some",function(e,t,n){if(n.matchToken("some")){var r=e.requireElement("expression",n);return{type:"noExpression",root:r,args:[r],op:function(e,n){return!t.isEmpty(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("negativeNumber",function(e,t,n){if(n.matchOpToken("-")){var r=e.requireElement("unaryExpression",n);return{type:"negativeNumber",root:r,args:[r],op:function(e,t){return-1*t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("unaryExpression",function(e,t,n){return n.matchToken("the"),e.parseAnyOf(["beepExpression","logicalNot","relativePositionalExpression","positionalExpression","noExpression","negativeNumber","postfixExpression"],n)}),E.addGrammarElement("beepExpression",function(e,t,n){if(n.matchToken("beep!")){var r=e.parseElement("unaryExpression",n);if(r){r.booped=!0;var o=r.evaluate;return r.evaluate=function(e){var n=o.apply(r,arguments),a=e.me;if(t.triggerEvent(a,"hyperscript:beep",{element:a,expression:r,value:n})){var i,u=n;"String"===(i=n?n instanceof v?"ElementCollection":n.constructor?n.constructor.name:"unknown":"object (null)")?u='"'+u+'"':n instanceof v&&(u=Array.from(n)),console.log("///_ BEEP! The expression ("+r.sourceFor().substr(6)+") evaluates to:",u,"of type "+i)}return n},r}}});var S=function(e,t,n,r){var o=[];T.forEach(t,function(t){(t.matches(n)||t===e)&&o.push(t)});for(var a=0;a<o.length-1;a++)if(o[a]===e)return o[a+1];if(r){var i=o[0];if(i&&i.matches(n))return i}};E.addGrammarElement("relativePositionalExpression",function(e,t,n){var r=n.matchAnyToken("next","previous");if(r){if("next"===r.value)var o=!0;var a=e.parseElement("expression",n);if(n.matchToken("from")){n.pushFollow("in");try{var i=e.requireElement("unaryExpression",n)}finally{n.popFollow()}}else i=e.requireElement("implicitMeTarget",n);var u,l=!1;if(n.matchToken("in")){l=!0;var s=e.requireElement("unaryExpression",n)}else u=n.matchToken("within")?e.requireElement("unaryExpression",n):document.body;var c=!1;return n.matchToken("with")&&(n.requireToken("wrapping"),c=!0),{type:"relativePositionalExpression",from:i,forwardSearch:o,inSearch:l,wrapping:c,inElt:s,withinElt:u,operator:r.value,args:[a,i,s,u],op:function(e,t,n,r,a){var i,u,s=t.css;if(null==s)throw"Expected a CSS value";if(l){if(r)return o?S(n,r,s,c):(i=s,u=c,S(n,Array.from(r).reverse(),i,u))}else if(a)return o?function(e,t,n,r){for(var o=t.querySelectorAll(n),a=0;a<o.length;a++){var i=o[a];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return i}if(r)return o[0]}(n,a,s,c):function(e,t,n,r){for(var o=t.querySelectorAll(n),a=o.length-1;a>=0;a--){var i=o[a];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}if(r)return o[o.length-1]}(n,a,s,c)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("positionalExpression",function(e,t,n){var r=n.matchAnyToken("first","last","random");if(r){n.matchAnyToken("in","from","of");var o=e.requireElement("unaryExpression",n),a=r.value;return{type:"positionalExpression",rhs:o,operator:r.value,args:[o],op:function(e,t){if(t&&!Array.isArray(t)&&(t=t.children?t.children:Array.from(t)),t){if("first"===a)return t[0];if("last"===a)return t[t.length-1];if("random"===a)return t[Math.floor(Math.random()*t.length)]}},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("mathOperator",function(e,t,n){var r,o=e.parseElement("unaryExpression",n),a=null;for(r=n.matchAnyOpToken("+","-","*","/","%");r;){var i=r.value;(a=a||r).value!==i&&e.raiseParseError(n,"You must parenthesize math operations with different operators");var u=e.parseElement("unaryExpression",n);o={type:"mathOperator",lhs:o,rhs:u,operator:i,args:[o,u],op:function(e,t,n){return"+"===i?t+n:"-"===i?t-n:"*"===i?t*n:"/"===i?t/n:"%"===i?t%n:void 0},evaluate:function(e){return t.unifiedEval(this,e)}},r=n.matchAnyOpToken("+","-","*","/","%")}return o}),E.addGrammarElement("mathExpression",function(e,t,n){return e.parseAnyOf(["mathOperator","unaryExpression"],n)}),E.addGrammarElement("comparisonOperator",function(e,t,n){var r=e.parseElement("mathExpression",n),o=n.matchAnyOpToken("<",">","<=",">=","==","===","!=","!=="),a=o?o.value:null,i=!0,u=!1;if(null==a&&(n.matchToken("is")||n.matchToken("am")?n.matchToken("not")?n.matchToken("in")?a="not in":n.matchToken("a")?(a="not a",u=!0):n.matchToken("empty")?(a="not empty",i=!1):a="!=":n.matchToken("in")?a="in":n.matchToken("a")?(a="a",u=!0):n.matchToken("empty")?(a="empty",i=!1):n.matchToken("less")?(n.requireToken("than"),n.matchToken("or")?(n.requireToken("equal"),n.requireToken("to"),a="<="):a="<"):n.matchToken("greater")?(n.requireToken("than"),n.matchToken("or")?(n.requireToken("equal"),n.requireToken("to"),a=">="):a=">"):a="==":n.matchToken("exist")||n.matchToken("exists")?(a="exist",i=!1):n.matchToken("matches")||n.matchToken("match")?a="match":n.matchToken("contains")||n.matchToken("contain")?a="contain":n.matchToken("includes")||n.matchToken("include")?a="include":(n.matchToken("do")||n.matchToken("does"))&&(n.requireToken("not"),n.matchToken("matches")||n.matchToken("match")?a="not match":n.matchToken("contains")||n.matchToken("contain")?a="not contain":n.matchToken("exist")||n.matchToken("exist")?(a="not exist",i=!1):n.matchToken("include")?a="not include":e.raiseParseError(n,"Expected matches or contains"))),a){if(u)var l=n.requireTokenType("IDENTIFIER"),s=!n.matchOpToken("!");else if(i){var c=e.requireElement("mathExpression",n);"match"!==a&&"not match"!==a||(c=c.css?c.css:c)}var f=r;r={type:"comparisonOperator",operator:a,typeName:l,nullOk:s,lhs:r,rhs:c,args:[r,c],op:function(e,n,r){if("=="===a)return n==r;if("!="===a)return n!=r;if("match"===a)return null!=n&&g(f,n,r);if("not match"===a)return null==n||!g(f,n,r);if("in"===a)return null!=r&&k(c,r,n);if("not in"===a)return null==r||!k(c,r,n);if("contain"===a)return null!=n&&k(f,n,r);if("not contain"===a)return null==n||!k(f,n,r);if("include"===a)return null!=n&&k(f,n,r);if("not include"===a)return null==n||!k(f,n,r);if("==="===a)return n===r;if("!=="===a)return n!==r;if("<"===a)return n<r;if(">"===a)return n>r;if("<="===a)return n<=r;if(">="===a)return n>=r;if("empty"===a)return t.isEmpty(n);if("not empty"===a)return!t.isEmpty(n);if("exist"===a)return t.doesExist(n);if("not exist"===a)return!t.doesExist(n);if("a"===a)return t.typeCheck(n,l.value,s);if("not a"===a)return!t.typeCheck(n,l.value,s);throw"Unknown comparison : "+a},evaluate:function(e){return t.unifiedEval(this,e)}}}return r}),E.addGrammarElement("comparisonExpression",function(e,t,n){return e.parseAnyOf(["comparisonOperator","mathExpression"],n)}),E.addGrammarElement("logicalOperator",function(e,t,n){var r,o=e.parseElement("comparisonExpression",n),a=null;r=n.matchToken("and")||n.matchToken("or");for(var i=function(){(a=a||r).value!==r.value&&e.raiseParseError(n,"You must parenthesize logical operations with different operators"),u=e.requireElement("comparisonExpression",n);var i=r.value;o={type:"logicalOperator",operator:i,lhs:o,rhs:u,args:[o,u],op:function(e,t,n){return"and"===i?t&&n:t||n},evaluate:function(e){return t.unifiedEval(this,e)}},r=n.matchToken("and")||n.matchToken("or")};r;){var u;i()}return o}),E.addGrammarElement("logicalExpression",function(e,t,n){return e.parseAnyOf(["logicalOperator","mathExpression"],n)}),E.addGrammarElement("asyncExpression",function(e,t,n){return n.matchToken("async")?{type:"asyncExpression",value:e.requireElement("logicalExpression",n),evaluate:function(e){return{asyncWrapper:!0,value:this.value.evaluate(e)}}}:e.parseElement("logicalExpression",n)}),E.addGrammarElement("expression",function(e,t,n){return n.matchToken("the"),e.parseElement("asyncExpression",n)}),E.addGrammarElement("assignableExpression",function(e,t,n){n.matchToken("the");var r=e.parseElement("primaryExpression",n);return!r||"symbol"!==r.type&&"ofExpression"!==r.type&&"propertyAccess"!==r.type&&"attributeRefAccess"!==r.type&&"attributeRef"!==r.type&&"styleRef"!==r.type&&"arrayIndex"!==r.type&&"possessive"!==r.type?(E.raiseParseError(n,"A target expression must be writable.  The expression type '"+(r&&r.type)+"' is not."),r):r}),E.addGrammarElement("hyperscript",function(e,t,n){var r=[];if(n.hasMore())for(;e.featureStart(n.currentToken())||"("===n.currentToken().value;){var o=e.requireElement("feature",n);r.push(o),n.matchToken("end")}return{type:"hyperscript",features:r,apply:function(e,t,n){for(var o,a=s(r);!(o=a()).done;)o.value.install(e,t,n)}}});var N=function(e){var t=[];if("("===e.token(0).value&&(")"===e.token(1).value||","===e.token(2).value||")"===e.token(2).value)){e.matchOpToken("(");do{t.push(e.requireTokenType("IDENTIFIER"))}while(e.matchOpToken(","));e.requireOpToken(")")}return t};E.addFeature("on",function(e,t,n){if(n.matchToken("on")){var r=!1;n.matchToken("every")&&(r=!0);var o=[],a=null;do{var i=e.requireElement("eventName",n,"Expected event name").evaluate();a=a?a+" or "+i:"on "+i;var u=N(n),l=null;if(n.matchOpToken("[")&&(l=e.requireElement("expression",n),n.requireOpToken("]")),"NUMBER"===n.currentToken().type){var f=n.consumeToken(),m=parseInt(f.value);if(n.matchToken("to"))var p=n.consumeToken(),d=parseInt(p.value);else if(n.matchToken("and")){var v=!0;n.requireToken("on")}}if("intersection"===i){var h={};if(n.matchToken("with")&&(h.with=e.requireElement("expression",n).evaluate()),n.matchToken("having"))do{n.matchToken("margin")?h.rootMargin=e.requireElement("stringLike",n).evaluate():n.matchToken("threshold")?h.threshold=e.requireElement("expression",n).evaluate():e.raiseParseError(n,"Unknown intersection config specification")}while(n.matchToken("and"))}else if("mutation"===i){var E={};if(n.matchToken("of"))do{if(n.matchToken("anything"))E.attributes=!0,E.subtree=!0,E.characterData=!0,E.childList=!0;else if(n.matchToken("childList"))E.childList=!0;else if(n.matchToken("attributes"))E.attributes=!0,E.attributeOldValue=!0;else if(n.matchToken("subtree"))E.subtree=!0;else if(n.matchToken("characterData"))E.characterData=!0,E.characterDataOldValue=!0;else if("ATTRIBUTE_REF"===n.currentToken().type){var y=n.consumeToken();null==E.attributeFilter&&(E.attributeFilter=[]),0==y.value.indexOf("@")?E.attributeFilter.push(y.value.substring(1)):e.raiseParseError(n,"Only shorthand attribute references are allowed here")}else e.raiseParseError(n,"Unknown mutation config specification")}while(n.matchToken("or"));else E.attributes=!0,E.characterData=!0,E.childList=!0}var k=null,g=!1;if(n.matchToken("from")&&(n.matchToken("elsewhere")?g=!0:(k=e.parseElement("expression",n))||e.raiseParseError(n,'Expected either target value or "elsewhere".')),null===k&&!1===g&&n.matchToken("elsewhere")&&(g=!0),n.matchToken("in"))var x=e.parseElement("unaryExpression",n);if(n.matchToken("debounced")){n.requireToken("at");var b=e.requireElement("expression",n).evaluate({})}else if(n.matchToken("throttled")){n.requireToken("at");var q=e.requireElement("expression",n).evaluate({})}o.push({execCount:0,every:r,on:i,args:u,filter:l,from:k,inExpr:x,elsewhere:g,startCount:m,endCount:d,unbounded:v,debounceTime:b,throttleTime:q,mutationSpec:E,intersectionSpec:h,debounced:void 0,lastExec:void 0})}while(n.matchToken("or"));var w=!0;if(!r&&n.matchToken("queue"))if(n.matchToken("all"))w=!1;else if(n.matchToken("first"))var S=!0;else if(n.matchToken("none"))var I=!0;else n.requireToken("last");var R=e.requireElement("commandList",n);if(e.ensureTerminated(R),n.matchToken("catch")){var O=n.requireTokenType("IDENTIFIER").value,L=e.requireElement("commandList",n);e.ensureTerminated(L)}if(n.matchToken("finally")){var C=e.requireElement("commandList",n);e.ensureTerminated(C)}var A={displayName:a,events:o,start:R,every:r,execCount:0,errorHandler:L,errorSymbol:O,execute:function(e){var n=t.getEventQueueFor(e.me,A);if(n.executing&&!1===r){if(I||S&&n.queue.length>0)return;return w&&(n.queue.length=0),void n.queue.push(e)}A.execCount++,n.executing=!0,e.meta.onHalt=function(){n.executing=!1;var e=n.queue.shift();e&&setTimeout(function(){A.execute(e)},1)},e.meta.reject=function(n){console.error(n.message?n.message:n);var r=t.getHyperTrace(e,n);r&&r.print(),t.triggerEvent(e.me,"exception",{error:n})},R.execute(e)},install:function(e,n){for(var r,o=function(){var n=r.value;i=n.elsewhere?[document]:n.from?n.from.evaluate(t.makeContext(e,A,e,null)):[e],t.implicitLoop(i,function(r){var o=n.on;if(n.mutationSpec&&(o="hyperscript:mutation",new MutationObserver(function(e,t){A.executing||T.triggerEvent(r,o,{mutationList:e,observer:t})}).observe(r,n.mutationSpec)),n.intersectionSpec){o="hyperscript:insersection";var a=new IntersectionObserver(function(e){for(var t,n=s(e);!(t=n()).done;){var i=t.value,u={observer:a};(u=c(u,i)).intersecting=i.isIntersecting,T.triggerEvent(r,o,u)}},n.intersectionSpec);a.observe(r)}(r.addEventListener||r.on).call(r,o,function a(i){if("undefined"!=typeof Node&&e instanceof Node&&r!==e&&!e.isConnected)r.removeEventListener(o,a);else{var u=t.makeContext(e,A,e,i);if(!n.elsewhere||!e.contains(i.target)){n.from&&(u.result=r);for(var l,c=s(n.args);!(l=c()).done;){var f=l.value,m=u.event[f.value];void 0!==m?u[f.value]=m:"detail"in u.event&&(u[f.value]=u.event.detail[f.value])}if(u.meta.errorHandler=L,u.meta.errorSymbol=O,u.meta.finallyHandler=C,n.filter){var p=u.meta.context;u.meta.context=u.event;try{if(!n.filter.evaluate(u))return}finally{u.meta.context=p}}if(n.inExpr)for(var d=i.target;;){if(d.matches&&d.matches(n.inExpr.css)){u.result=d;break}if(null==(d=d.parentElement))return}if(n.execCount++,n.startCount)if(n.endCount){if(n.execCount<n.startCount||n.execCount>n.endCount)return}else if(n.unbounded){if(n.execCount<n.startCount)return}else if(n.execCount!==n.startCount)return;if(n.debounceTime)return n.debounced&&clearTimeout(n.debounced),void(n.debounced=setTimeout(function(){A.execute(u)},n.debounceTime));if(n.throttleTime){if(n.lastExec&&Date.now()<n.lastExec+n.throttleTime)return;n.lastExec=Date.now()}A.execute(u)}}})})},a=s(A.events);!(r=a()).done;){var i;o()}}};return e.setParent(R,A),A}}),E.addFeature("def",function(e,t,n){if(n.matchToken("def")){var r=e.requireElement("dotOrColonPath",n).evaluate(),o=r.split("."),a=o.pop(),i=[];if(n.matchOpToken("("))if(n.matchOpToken(")"));else{do{i.push(n.requireTokenType("IDENTIFIER"))}while(n.matchOpToken(","));n.requireOpToken(")")}var u=e.requireElement("commandList",n);if(n.matchToken("catch"))var l=n.requireTokenType("IDENTIFIER").value,s=e.parseElement("commandList",n);if(n.matchToken("finally")){var c=e.requireElement("commandList",n);e.ensureTerminated(c)}var f={displayName:a+"("+i.map(function(e){return e.value}).join(", ")+")",name:a,args:i,start:u,errorHandler:s,errorSymbol:l,finallyHandler:c,install:function(e,n){var m=function(){var r=t.makeContext(n,f,e,null);r.meta.errorHandler=s,r.meta.errorSymbol=l,r.meta.finallyHandler=c;for(var o=0;o<i.length;o++){var a=i[o],m=arguments[o];a&&(r[a.value]=m)}r.meta.caller=arguments[i.length],r.meta.caller&&(r.meta.callingCommand=r.meta.caller.meta.command);var p,d=null,v=new Promise(function(e,t){p=e,d=t});return u.execute(r),r.meta.returned?r.meta.returnValue:(r.meta.resolve=p,r.meta.reject=d,v)};m.hyperfunc=!0,m.hypername=r,t.assignToNamespace(e,o,a,m)}};return e.ensureTerminated(u),s&&e.ensureTerminated(s),e.setParent(u,f),f}}),E.addFeature("set",function(e,t,n){var r=e.parseElement("setCommand",n);if(r){"element"!==r.target.scope&&e.raiseParseError(n,"variables declared at the feature level must be element scoped.");var o={start:r,install:function(e,n){r&&r.execute(t.makeContext(e,o,e,null))}};return e.ensureTerminated(r),o}}),E.addFeature("init",function(e,t,n){if(n.matchToken("init")){var r=e.requireElement("commandList",n),o={start:r,install:function(e,n){setTimeout(function(){r&&r.execute(t.makeContext(e,o,e,null))},0)}};return e.ensureTerminated(r),e.setParent(r,o),o}}),E.addFeature("worker",function(e,t,n){n.matchToken("worker")&&e.raiseParseError(n,"In order to use the 'worker' feature, include the _hyperscript worker plugin. See https://hyperscript.org/features/worker/ for more info.")}),E.addFeature("behavior",function(e,t,n){if(n.matchToken("behavior")){var r=e.requireElement("dotOrColonPath",n).evaluate(),o=r.split("."),a=o.pop(),i=[];if(n.matchOpToken("(")&&!n.matchOpToken(")")){do{i.push(n.requireTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));n.requireOpToken(")")}for(var u=e.requireElement("hyperscript",n),l=0;l<u.features.length;l++)u.features[l].behavior=r;return{install:function(e,n){t.assignToNamespace(d.document&&d.document.body,o,a,function(e,n,o){for(var a=f(t.getInternalData(e),r+"Scope"),l=0;l<i.length;l++)a[i[l]]=o[i[l]];u.apply(e,n)})}}}}),E.addFeature("install",function(e,t,n){if(n.matchToken("install")){var r,o=e.requireElement("dotOrColonPath",n).evaluate(),a=o.split("."),i=e.parseElement("namedArgumentList",n);return r={install:function(e,n){t.unifiedEval({args:[i],op:function(t,r){for(var i=d,u=0;u<a.length;u++)if("object"!=typeof(i=i[a[u]])&&"function"!=typeof i)throw new Error("No such behavior defined as "+o);if(!(i instanceof Function))throw new Error(o+" is not a behavior");i(e,n,r)}},t.makeContext(e,r,e))}}}}),E.addGrammarElement("jsBody",function(e,t,n){for(var r=n.currentToken().start,o=n.currentToken(),a=[],i="",u=!1;n.hasMore();){o=n.consumeToken();var l=n.token(0,!0);if("IDENTIFIER"===l.type&&"end"===l.value)break;u?"IDENTIFIER"===o.type||"NUMBER"===o.type?i+=o.value:(""!==i&&a.push(i),i="",u=!1):"IDENTIFIER"===o.type&&"function"===o.value&&(u=!0)}return{type:"jsBody",exposedFunctionNames:a,jsSource:n.source.substring(r,o.end+1)}}),E.addFeature("js",function(e,t,n){if(n.matchToken("js")){var r=e.requireElement("jsBody",n),o=r.jsSource+"\nreturn { "+r.exposedFunctionNames.map(function(e){return e+":"+e}).join(",")+" } ",a=new Function(o);return{jsSource:o,function:a,exposedFunctionNames:r.exposedFunctionNames,install:function(){c(d,a())}}}}),E.addCommand("js",function(e,t,n){if(n.matchToken("js")){var r=[];if(n.matchOpToken("("))if(n.matchOpToken(")"));else{do{var o=n.requireTokenType("IDENTIFIER");r.push(o.value)}while(n.matchOpToken(","));n.requireOpToken(")")}var a=e.requireElement("jsBody",n);n.matchToken("end");var i=m(Function,r.concat([a.jsSource]));return{jsSource:a.jsSource,function:i,inputs:r,op:function(e){var n=[];r.forEach(function(r){n.push(t.resolveSymbol(r,e,"default"))});var o=i.apply(d,n);return o&&"function"==typeof o.then?new Promise(function(n){o.then(function(r){e.result=r,n(t.findNext(this,e))})}):(e.result=o,t.findNext(this,e))}}}}),E.addCommand("async",function(e,t,n){if(n.matchToken("async")){if(n.matchToken("do")){for(var r=o=e.requireElement("commandList",n);r.next;)r=r.next;r.next=t.HALT,n.requireToken("end")}else var o=e.requireElement("command",n);var a={body:o,op:function(e){return setTimeout(function(){o.execute(e)}),t.findNext(this,e)}};return e.setParent(o,a),a}}),E.addCommand("tell",function(e,t,n){var r=n.currentToken();if(n.matchToken("tell")){var o=e.requireElement("expression",n),a=e.requireElement("commandList",n);n.hasMore()&&!e.featureStart(n.currentToken())&&n.requireToken("end");var i="tell_"+r.start,u={value:o,body:a,args:[o],resolveNext:function(e){var n=e.meta.iterators[i];return n.index<n.value.length?(e.beingTold=n.value[n.index++],a):(e.beingTold=n.originalBeingTold,this.next?this.next:t.findNext(this.parent,e))},op:function(e,t){return null==t?t=[]:Array.isArray(t)||t instanceof NodeList||(t=[t]),e.meta.iterators[i]={originalBeingTold:e.beingTold,index:0,value:t},this.resolveNext(e)}};return e.setParent(a,u),u}}),E.addCommand("wait",function(e,t,n){if(n.matchToken("wait")){var r,o;if(n.matchToken("for")){n.matchToken("a");var a=[];do{var i=n.token(0);a.push("NUMBER"===i.type||"L_PAREN"===i.type?{time:e.requireElement("expression",n).evaluate()}:{name:E.requireElement("dotOrColonPath",n,"Expected event name").evaluate(),args:N(n)})}while(n.matchToken("or"));if(n.matchToken("from"))var u=e.requireElement("expression",n);return r={event:a,on:u,args:[u],op:function(e,n){var r=this,o=n||e.me;if(!(o instanceof EventTarget))throw new Error("Not a valid event target: "+this.on.sourceFor());return new Promise(function(n){for(var i,u=!1,l=function(){var a=i.value;f=function(o){if(e.result=o,a.args)for(var i,l=s(a.args);!(i=l()).done;){var c=i.value;e[c.value]=o[c.value]||(o.detail?o.detail[c.value]:null)}u||(u=!0,n(t.findNext(r,e)))},a.name?o.addEventListener(a.name,f,{once:!0}):null!=a.time&&setTimeout(f,a.time,a.time)},c=s(a);!(i=c()).done;){var f;l()}})}},r}return n.matchToken("a")?(n.requireToken("tick"),o=0):o=E.requireElement("expression",n),{type:"waitCmd",time:o,args:[o],op:function(e,n){var r=this;return new Promise(function(o){setTimeout(function(){o(t.findNext(r,e))},n)})},execute:function(e){return t.unifiedExec(this,e)}}}}),E.addGrammarElement("dotOrColonPath",function(e,t,n){var r=n.matchTokenType("IDENTIFIER");if(r){var o=[r.value],a=n.matchOpToken(".")||n.matchOpToken(":");if(a)do{o.push(n.requireTokenType("IDENTIFIER","NUMBER").value)}while(n.matchOpToken(a.value));return{type:"dotOrColonPath",path:o,evaluate:function(){return o.join(a?a.value:"")}}}}),E.addGrammarElement("eventName",function(e,t,n){var r;return(r=n.matchTokenType("STRING"))?{evaluate:function(){return r.value}}:e.parseElement("dotOrColonPath",n)}),E.addCommand("trigger",function(e,t,n){if(n.matchToken("trigger"))return x("trigger",e,t,n)}),E.addCommand("send",function(e,t,n){if(n.matchToken("send"))return x("send",e,t,n)});var I=function(e,t,n,r){if(r)if(e.commandBoundary(n.currentToken()))e.raiseParseError(n,"'return' commands must return a value.  If you do not wish to return a value, use 'exit' instead.");else var o=e.requireElement("expression",n);var a={value:o,args:[o],op:function(e,n){var r=e.meta.resolve;return e.meta.returned=!0,e.meta.returnValue=n,r&&(n?r(n):r()),t.HALT}};return a};E.addCommand("return",function(e,t,n){if(n.matchToken("return"))return I(e,t,n,!0)}),E.addCommand("exit",function(e,t,n){if(n.matchToken("exit"))return I(e,t,n,!1)}),E.addCommand("halt",function(e,t,n){if(n.matchToken("halt")){if(n.matchToken("the")){n.requireToken("event"),n.matchOpToken("'")&&n.requireToken("s");var r=!0}if(n.matchToken("bubbling"))var o=!0;else if(n.matchToken("default"))var a=!0;var i=I(e,t,n,!1);return{keepExecuting:!0,bubbling:o,haltDefault:a,exit:i,op:function(e){if(e.event)return o?e.event.stopPropagation():(a||e.event.stopPropagation(),e.event.preventDefault()),r?t.findNext(this,e):i}}}}),E.addCommand("log",function(e,t,n){if(n.matchToken("log")){for(var r=[e.parseElement("expression",n)];n.matchOpToken(",");)r.push(e.requireElement("expression",n));if(n.matchToken("with"))var o=e.requireElement("expression",n);var a={exprs:r,withExpr:o,args:[o,r],op:function(e,n,r){return n?n.apply(null,r):console.log.apply(null,r),t.findNext(this,e)}};return a}}),E.addCommand("throw",function(e,t,n){if(n.matchToken("throw")){var r=e.requireElement("expression",n),o={expr:r,args:[r],op:function(e,n){throw t.registerHyperTrace(e,n),n}};return o}});var R=function(e,t,n){var r=e.requireElement("expression",n),o={expr:r,args:[r],op:function(e,n){return e.result=n,t.findNext(o,e)}};return o};E.addCommand("call",function(e,t,n){if(n.matchToken("call")){var r=R(e,t,n);return r.expr&&"functionCall"!==r.expr.type&&e.raiseParseError(n,"Must be a function invocation"),r}}),E.addCommand("get",function(e,t,n){if(n.matchToken("get"))return R(e,t,n)}),E.addCommand("make",function(e,t,n){if(n.matchToken("make")){n.matchToken("a")||n.matchToken("an");var r,o=e.requireElement("expression",n),a=[];if("queryRef"!==o.type&&n.matchToken("from"))do{a.push(e.requireElement("expression",n))}while(n.matchOpToken(","));if(n.matchToken("called"))var i=e.requireElement("symbol",n);return"queryRef"===o.type?r={op:function(e){for(var n,r,a="div",u=[],l=/(?:(^|#|\.)([^#\. ]+))/g;n=l.exec(o.css);)""===n[1]?a=n[2].trim():"#"===n[1]?r=n[2].trim():u.push(n[2].trim());var s=document.createElement(a);void 0!==r&&(s.id=r);for(var c=0;c<u.length;c++)s.classList.add(u[c]);return e.result=s,i&&t.setSymbol(i.name,e,i.scope,s),t.findNext(this,e)}}:(r={args:[o,a],op:function(e,n,r){return e.result=m(n,r),i&&t.setSymbol(i.name,e,i.scope,e.result),t.findNext(this,e)}},r)}}),E.addGrammarElement("pseudoCommand",function(e,t,n){var r=n.token(1);if(!r||!r.op||"."!==r.value&&"("!==r.value)return null;for(var o=e.requireElement("primaryExpression",n),a=o.root,i=o;null!=a.root;)i=i.root,a=a.root;if("functionCall"!==o.type&&e.raiseParseError(n,"Pseudo-commands must be function calls"),"functionCall"===i.type&&null==i.root.root)if(n.matchAnyToken("the","to","on","with","into","from","at"))var u=e.requireElement("expression",n);else n.matchToken("me")&&(u=e.requireElement("implicitMeTarget",n));if(u)var l={type:"pseudoCommand",root:u,argExressions:i.argExressions,args:[u,i.argExressions],op:function(e,n,r){t.nullCheck(n,u);var o=n[i.root.name];return t.nullCheck(o,i),o.hyperfunc&&r.push(e),e.result=o.apply(n,r),t.findNext(l,e)},execute:function(e){return t.unifiedExec(this,e)}};else l={type:"pseudoCommand",expr:o,args:[o],op:function(e,n){return e.result=n,t.findNext(l,e)},execute:function(e){return t.unifiedExec(this,e)}};return l});var O=function(e,t,n,r,o){var a="symbol"===r.type,i="attributeRef"===r.type,u="styleRef"===r.type,l="arrayIndex"===r.type;i||u||a||null!=r.root||e.raiseParseError(n,"Can only put directly into symbols, not references");var s=null,c=null;if(a);else if(i||u){s=e.requireElement("implicitMeTarget",n);var f=r}else l?(c=r.firstIndex,s=r.root):(c=r.prop?r.prop.value:null,f=r.attribute,s=r.root);var m={target:r,symbolWrite:a,value:o,args:[s,c,o],op:function(e,n,o,i){return a?t.setSymbol(r.name,e,r.scope,i):(t.nullCheck(n,s),l?n[o]=i:t.implicitLoop(n,function(e){f?"attributeRef"===f.type?null==i?e.removeAttribute(f.name):e.setAttribute(f.name,i):e.style[f.name]=i:e[o]=i})),t.findNext(this,e)}};return m};E.addCommand("default",function(e,t,n){if(n.matchToken("default")){var r=e.requireElement("assignableExpression",n);n.requireToken("to");var o=e.requireElement("expression",n),a=O(e,t,n,r,o),i={target:r,value:o,setter:a,args:[r],op:function(e,n){return n?t.findNext(this,e):a}};return a.parent=i,i}}),E.addCommand("set",function(e,t,n){if(n.matchToken("set")){if("L_BRACE"===n.currentToken().type){var r=e.requireElement("objectLiteral",n);n.requireToken("on");var o={objectLiteral:r,target:a=e.requireElement("expression",n),args:[r,a],op:function(e,n,r){return c(r,n),t.findNext(this,e)}};return o}try{n.pushFollow("to");var a=e.requireElement("assignableExpression",n)}finally{n.popFollow()}n.requireToken("to");var i=e.requireElement("expression",n);return O(e,t,n,a,i)}}),E.addCommand("if",function(e,t,n){if(n.matchToken("if")){var r=e.requireElement("expression",n);n.matchToken("then");var o=e.parseElement("commandList",n);if(n.matchToken("else")||n.matchToken("otherwise"))var a=e.parseElement("commandList",n);n.hasMore()&&n.requireToken("end");var i={expr:r,trueBranch:o,falseBranch:a,args:[r],op:function(e,n){return n?o:a||t.findNext(this,e)}};return e.setParent(o,i),e.setParent(a,i),i}});var L=function(e,t,n,r){var o,a=t.currentToken();if(t.matchToken("for")||r){var i=t.requireTokenType("IDENTIFIER");o=i.value,t.requireToken("in");var u=e.requireElement("expression",t)}else if(t.matchToken("in"))o="it",u=e.requireElement("expression",t);else if(t.matchToken("while"))var l=e.requireElement("expression",t);else if(t.matchToken("until")){var s=!0;if(t.matchToken("event")){var c=E.requireElement("dotOrColonPath",t,"Expected event name");if(t.matchToken("from"))var f=e.requireElement("expression",t)}else l=e.requireElement("expression",t)}else if(e.commandBoundary(t.currentToken())||"forever"===t.currentToken().value){t.matchToken("forever");var m=!0}else{var p=e.requireElement("expression",t);t.requireToken("times")}if(t.matchToken("index"))var d=(i=t.requireTokenType("IDENTIFIER")).value;var v=e.parseElement("commandList",t);if(v&&c){for(var h=v;h.next;)h=h.next;var y={type:"waitATick",op:function(){return new Promise(function(e){setTimeout(function(){e(n.findNext(y))},0)})}};h.next=y}if(t.hasMore()&&t.requireToken("end"),null==o)var T=o="_implicit_repeat_"+a.start;else T=o+"_"+a.start;var k={identifier:o,indexIdentifier:d,slot:T,expression:u,forever:m,times:p,until:s,event:c,on:f,whileExpr:l,resolveNext:function(){return this},loop:v,args:[l,p],op:function(e,t,r){var a=e.meta.iterators[T],i=!1,u=null;if(this.forever)i=!0;else if(this.until)i=c?!1===e.meta.iterators[T].eventFired:!0!==t;else if(l)i=t;else if(r)i=a.index<r;else{var s=a.iterator.next();i=!s.done,u=s.value}return i?(e.result=a.value?e[o]=u:a.index,d&&(e[d]=a.index),a.index++,v):(e.meta.iterators[T]=null,n.findNext(this.parent,e))}};e.setParent(v,k);var g={name:"repeatInit",args:[u,c,f],op:function(e,t,n,r){var o={index:0,value:t,eventFired:!1};return e.meta.iterators[T]=o,t&&t[Symbol.iterator]&&(o.iterator=t[Symbol.iterator]()),c&&(r||e.me).addEventListener(n,function(t){e.meta.iterators[T].eventFired=!0},{once:!0}),k},execute:function(e){return n.unifiedExec(this,e)}};return e.setParent(k,g),g};if(E.addCommand("repeat",function(e,t,n){if(n.matchToken("repeat"))return L(e,n,t,!1)}),E.addCommand("for",function(e,t,n){if(n.matchToken("for"))return L(e,n,t,!0)}),E.addCommand("continue",function(e,t,n){if(n.matchToken("continue"))return{op:function(t){for(var r=this.parent;;r=r.parent)if(null==r&&e.raiseParseError(n,"Command `continue` cannot be used outside of a `repeat` loop."),null!=r.loop)return r.resolveNext(t)}}}),E.addCommand("break",function(e,t,n){if(n.matchToken("break"))return{op:function(r){for(var o=this.parent;;o=o.parent)if(null==o&&e.raiseParseError(n,"Command `continue` cannot be used outside of a `repeat` loop."),null!=o.loop)return t.findNext(o.parent,r)}}}),E.addGrammarElement("stringLike",function(e,t,n){return E.parseAnyOf(["string","nakedString"],n)}),E.addCommand("append",function(e,t,n){if(n.matchToken("append")){var r,o=e.requireElement("expression",n),a={type:"symbol",evaluate:function(e){return t.resolveSymbol("result",e)}};r=n.matchToken("to")?e.requireElement("expression",n):a;var i=null;"symbol"!==r.type&&"attributeRef"!==r.type&&null==r.root||(i=O(e,t,n,r,a));var u={value:o,target:r,args:[r,o],op:function(e,n,r){if(Array.isArray(n))return n.push(r),t.findNext(this,e);if(n instanceof Element)return n.innerHTML+=r,t.findNext(this,e);if(i)return e.result=(n||"")+r,i;throw Error("Unable to append a value!")},execute:function(e){return t.unifiedExec(this,e)}};return null!=i&&(i.parent=u),u}}),E.addCommand("increment",function(e,t,n){if(n.matchToken("increment")){var r,o=e.parseElement("assignableExpression",n);n.matchToken("by")&&(r=e.requireElement("expression",n));var a={type:"implicitIncrementOp",target:o,args:[o,r],op:function(e,t,n){var r=(t=t?parseFloat(t):0)+(n=n?parseFloat(n):1);return e.result=r,r},evaluate:function(e){return t.unifiedEval(this,e)}};return O(e,t,n,o,a)}}),E.addCommand("decrement",function(e,t,n){if(n.matchToken("decrement")){var r,o=e.parseElement("assignableExpression",n);n.matchToken("by")&&(r=e.requireElement("expression",n));var a={type:"implicitDecrementOp",target:o,args:[o,r],op:function(e,t,n){var r=(t=t?parseFloat(t):0)-(n=n?parseFloat(n):1);return e.result=r,r},evaluate:function(e){return t.unifiedEval(this,e)}};return O(e,t,n,o,a)}}),E.addCommand("fetch",function(e,t,n){if(n.matchToken("fetch")){var r=e.requireElement("stringLike",n);if(n.matchToken("as"))var o=b(n,e);if(n.matchToken("with")&&"{"!==n.currentToken().value)var a=e.parseElement("nakedNamedArgumentList",n);else a=e.parseElement("objectLiteral",n);null==o&&n.matchToken("as")&&(o=b(n,e));var i=o?o.type:"text",u=o?o.conversion:null,l={url:r,argExpressions:a,args:[r,a],op:function(e,n,r){var o=r||{};o.sender=e.me,o.headers=o.headers||{};var a=new AbortController,s=e.me.addEventListener("fetch:abort",function(){a.abort()},{once:!0});o.signal=a.signal,t.triggerEvent(e.me,"hyperscript:beforeFetch",o),t.triggerEvent(e.me,"fetch:beforeRequest",o);var c=!1;return(r=o).timeout&&setTimeout(function(){c||a.abort()},r.timeout),fetch(n,r).then(function(n){var r={response:n};return t.triggerEvent(e.me,"fetch:afterResponse",r),n=r.response,"response"===i?(e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)):"json"===i?n.json().then(function(n){return e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)}):n.text().then(function(n){return u&&(n=t.convertValue(n,u)),"html"===i&&(n=t.convertValue(n,"Fragment")),e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)})}).catch(function(n){throw t.triggerEvent(e.me,"fetch:error",{reason:n}),n}).finally(function(){e.me.removeEventListener("fetch:abort",s)})}};return l}}),"document"in d){var C=Array.from(document.querySelectorAll("script[type='text/hyperscript'][src]"));Promise.all(C.map(function(e){return fetch(e.src).then(function(e){return e.text()})})).then(function(e){return e.forEach(T.evaluate)}).then(function(){var e;e=function(){var e,t;(t=(e=document.querySelector('meta[name="htmx-config"]'))?function(e){try{return JSON.parse(e)}catch(e){return t=e,console.error?console.error(t):console.log&&console.log("ERROR: ",t),null}var t}(e.content):null)&&(p.config=c(p.config,t)),T.processNode(document.documentElement),document.addEventListener("htmx:load",function(e){T.processNode(e.detail.elt)})},"loading"!==document.readyState?setTimeout(e):document.addEventListener("DOMContentLoaded",e)})}var A=p=c(function(e,t){return T.evaluate(e,t)},{internals:{lexer:h,parser:E,runtime:T},ElementCollection:v,addFeature:function(e,t){E.addFeature(e,t)},addCommand:function(e,t){E.addCommand(e,t)},addLeafExpression:function(e,t){E.addLeafExpression(e,t)},addIndirectExpression:function(e,t){E.addIndirectExpression(e,t)},evaluate:T.evaluate.bind(T),parse:T.parse.bind(T),processNode:T.processNode.bind(T),config:{attributes:"_, script, data-script",defaultTransition:"all 500ms ease-in",disableSelector:"[disable-scripting], [data-disable-scripting]",conversions:y}});return function(e){e.addCommand("settle",function(e,t,n){if(n.matchToken("settle")){if(e.commandBoundary(n.currentToken()))r=e.requireElement("implicitMeTarget",n);else var r=e.requireElement("expression",n);var o={type:"settleCmd",args:[r],op:function(e,n){t.nullCheck(n,r);var a=null,i=!1,u=new Promise(function(e){a=e});return n.addEventListener("transitionstart",function(){i=!0},{once:!0}),setTimeout(function(){i||a(t.findNext(o,e))},500),n.addEventListener("transitionend",function(){a(t.findNext(o,e))},{once:!0}),u},execute:function(e){return t.unifiedExec(this,e)}};return o}}),e.addCommand("add",function(e,t,n){if(n.matchToken("add")){var r=e.parseElement("classRef",n),o=null,a=null;if(null==r)null==(o=e.parseElement("attributeRef",n))&&null==(a=e.parseElement("styleLiteral",n))&&e.raiseParseError(n,"Expected either a class reference or attribute expression");else for(var i=[r];r=e.parseElement("classRef",n);)i.push(r);if(n.matchToken("to"))var u=e.requireElement("expression",n);else u=e.requireElement("implicitMeTarget",n);if(n.matchToken("when")){a&&e.raiseParseError(n,"Only class and properties are supported with a when clause");var l=e.requireElement("expression",n)}return i?{classRefs:i,to:u,args:[u,i],op:function(e,n,r){return t.nullCheck(n,u),t.forEach(r,function(r){t.implicitLoop(n,function(n){l?(e.result=n,t.evaluateNoPromise(l,e)?n instanceof Element&&n.classList.add(r.className):n instanceof Element&&n.classList.remove(r.className),e.result=null):n instanceof Element&&n.classList.add(r.className)})}),t.findNext(this,e)}}:o?{type:"addCmd",attributeRef:o,to:u,args:[u],op:function(e,n,r){return t.nullCheck(n,u),t.implicitLoop(n,function(n){l?(e.result=n,t.evaluateNoPromise(l,e)?n.setAttribute(o.name,o.value):n.removeAttribute(o.name),e.result=null):n.setAttribute(o.name,o.value)}),t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}:{type:"addCmd",cssDeclaration:a,to:u,args:[u,a],op:function(e,n,r){return t.nullCheck(n,u),t.implicitLoop(n,function(e){e.style.cssText+=r}),t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}}}),e.internals.parser.addGrammarElement("styleLiteral",function(e,t,n){if(n.matchOpToken("{")){for(var r=[""],o=[];n.hasMore();){if(n.matchOpToken("\\"))n.consumeToken();else{if(n.matchOpToken("}"))break;if(n.matchToken("$")){var a=n.matchOpToken("{"),i=e.parseElement("expression",n);a&&n.requireOpToken("}"),o.push(i),r.push("")}else{var u=n.consumeToken();r[r.length-1]+=n.source.substring(u.start,u.end)}}r[r.length-1]+=n.lastWhitespace()}return{type:"styleLiteral",args:[o],op:function(e,t){var n="";return r.forEach(function(e,r){n+=e,r in t&&(n+=t[r])}),n},evaluate:function(e){return t.unifiedEval(this,e)}}}}),e.addCommand("remove",function(e,t,n){if(n.matchToken("remove")){var r=e.parseElement("classRef",n),o=null,a=null;if(null==r)null==(o=e.parseElement("attributeRef",n))&&null==(a=e.parseElement("expression",n))&&e.raiseParseError(n,"Expected either a class reference, attribute expression or value expression");else for(var i=[r];r=e.parseElement("classRef",n);)i.push(r);if(n.matchToken("from"))var u=e.requireElement("expression",n);else u=e.requireElement("implicitMeTarget",n);return a?{elementExpr:a,from:u,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(e){e.parentElement&&e.parentElement.removeChild(e)}),t.findNext(this,e)}}:{classRefs:i,attributeRef:o,elementExpr:a,from:u,args:[i,u],op:function(e,n,r){return t.nullCheck(r,u),n?t.forEach(n,function(e){t.implicitLoop(r,function(t){t.classList.remove(e.className)})}):t.implicitLoop(r,function(e){e.removeAttribute(o.name)}),t.findNext(this,e)}}}}),e.addCommand("toggle",function(e,t,n){if(n.matchToken("toggle")){if(n.matchAnyToken("the","my"),"STYLE_REF"===n.currentToken().type){var o=n.consumeToken().value.substr(1),a=!0,i=r(e,n,o);if(n.matchToken("of")){n.pushFollow("with");try{var u=e.requireElement("expression",n)}finally{n.popFollow()}}else u=e.requireElement("implicitMeTarget",n)}else if(n.matchToken("between")){var l=!0,s=e.parseElement("classRef",n);n.requireToken("and");var c=e.requireElement("classRef",n)}else{s=e.parseElement("classRef",n);var f=null;if(null==s)null==(f=e.parseElement("attributeRef",n))&&e.raiseParseError(n,"Expected either a class reference or attribute expression");else for(var m=[s];s=e.parseElement("classRef",n);)m.push(s)}if(!0!==a&&(u=n.matchToken("on")?e.requireElement("expression",n):e.requireElement("implicitMeTarget",n)),n.matchToken("for"))var p=e.requireElement("expression",n);else if(n.matchToken("until")){var d=e.requireElement("dotOrColonPath",n,"Expected event name");if(n.matchToken("from"))var v=e.requireElement("expression",n)}var h={classRef:s,classRef2:c,classRefs:m,attributeRef:f,on:u,time:p,evt:d,from:v,toggle:function(e,n,r,o){t.nullCheck(e,u),a?t.implicitLoop(e,function(e){i("toggle",e)}):l?t.implicitLoop(e,function(e){e.classList.contains(n.className)?(e.classList.remove(n.className),e.classList.add(r.className)):(e.classList.add(n.className),e.classList.remove(r.className))}):o?t.forEach(o,function(n){t.implicitLoop(e,function(e){e.classList.toggle(n.className)})}):t.forEach(e,function(e){e.hasAttribute(f.name)?e.removeAttribute(f.name):e.setAttribute(f.name,f.value)})},args:[u,p,d,v,s,c,m],op:function(e,n,r,o,a,i,u,l){return r?new Promise(function(o){h.toggle(n,i,u,l),setTimeout(function(){h.toggle(n,i,u,l),o(t.findNext(h,e))},r)}):o?new Promise(function(r){(a||e.me).addEventListener(o,function(){h.toggle(n,i,u,l),r(t.findNext(h,e))},{once:!0}),h.toggle(n,i,u,l)}):(this.toggle(n,i,u,l),t.findNext(h,e))}};return h}});var t={display:function(n,r,o){if(o)r.style.display=o;else if("toggle"===n)"none"===getComputedStyle(r).display?t.display("show",r,o):t.display("hide",r,o);else if("hide"===n){var a=e.internals.runtime.getInternalData(r);null==a.originalDisplay&&(a.originalDisplay=r.style.display),r.style.display="none"}else{var i=e.internals.runtime.getInternalData(r);i.originalDisplay&&"none"!==i.originalDisplay?r.style.display=i.originalDisplay:r.style.removeProperty("display")}},visibility:function(e,n,r){r?n.style.visibility=r:"toggle"===e?"hidden"===getComputedStyle(n).visibility?t.visibility("show",n,r):t.visibility("hide",n,r):n.style.visibility="hide"===e?"hidden":"visible"},opacity:function(e,n,r){r?n.style.opacity=r:"toggle"===e?"0"===getComputedStyle(n).opacity?t.opacity("show",n,r):t.opacity("hide",n,r):n.style.opacity="hide"===e?"0":"1"}},n=function(e,t,n){var r=n.currentToken();return"when"===r.value||"with"===r.value||e.commandBoundary(r)?e.parseElement("implicitMeTarget",n):e.parseElement("expression",n)},r=function(n,r,o){var a=e.config.defaultHideShowStrategy,i=t;e.config.hideShowStrategies&&(i=c(i,e.config.hideShowStrategies));var u=i[o=o||a||"display"];return null==u&&n.raiseParseError(r,"Unknown show/hide strategy : "+o),u};function o(t,n,r,o){if(null!=r)var a=t.resolveSymbol(r,n);else a=n;if(a instanceof Element||a instanceof HTMLDocument){for(;a.firstChild;)a.removeChild(a.firstChild);a.append(e.internals.runtime.convertValue(o,"Fragment"))}else{if(null==r)throw"Don't know how to put a value into "+typeof n;t.setSymbol(r,n,null,o)}}function a(e,t,n){var r;if(n.matchToken("the")||n.matchToken("element")||n.matchToken("elements")||"CLASS_REF"===n.currentToken().type||"ID_REF"===n.currentToken().type||n.currentToken().op&&"<"===n.currentToken().value){e.possessivesDisabled=!0;try{r=e.parseElement("expression",n)}finally{delete e.possessivesDisabled}n.matchOpToken("'")&&n.requireToken("s")}else if("IDENTIFIER"===n.currentToken().type&&"its"===n.currentToken().value){var o=n.matchToken("its");r={type:"pseudopossessiveIts",token:o,name:o.value,evaluate:function(e){return t.resolveSymbol("it",e)}}}else n.matchToken("my")||n.matchToken("me"),r=e.parseElement("implicitMeTarget",n);return r}e.addCommand("hide",function(e,t,o){if(o.matchToken("hide")){var a=n(e,0,o),i=null;o.matchToken("with")&&0===(i=o.requireTokenType("IDENTIFIER","STYLE_REF").value).indexOf("*")&&(i=i.substr(1));var u=r(e,o,i);return{target:a,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(e){u("hide",e)}),t.findNext(this,e)}}}}),e.addCommand("show",function(e,t,o){if(o.matchToken("show")){var a=n(e,0,o),i=null;o.matchToken("with")&&0===(i=o.requireTokenType("IDENTIFIER","STYLE_REF").value).indexOf("*")&&(i=i.substr(1));var u=null;if(o.matchOpToken(":")){var l=o.consumeUntilWhitespace();o.matchTokenType("WHITESPACE"),u=l.map(function(e){return e.value}).join("")}if(o.matchToken("when"))var s=e.requireElement("expression",o);var c=r(e,o,i);return{target:a,when:s,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(n){s?(e.result=n,t.evaluateNoPromise(s,e)?c("show",n,u):c("hide",n),e.result=null):c("show",n,u)}),t.findNext(this,e)}}}}),e.addCommand("take",function(e,t,n){if(n.matchToken("take")){var r=e.requireElement("classRef",n);if(n.matchToken("from"))var o=e.requireElement("expression",n);else o=r;if(n.matchToken("for"))var a=e.requireElement("expression",n);else a=e.requireElement("implicitMeTarget",n);return{classRef:r,from:o,forElt:a,args:[r,o,a],op:function(e,n,r,i){t.nullCheck(r,o),t.nullCheck(i,a);var u=n.className;return t.implicitLoop(r,function(e){e.classList.remove(u)}),t.implicitLoop(i,function(e){e.classList.add(u)}),t.findNext(this,e)}}}}),e.addCommand("put",function(e,t,n){if(n.matchToken("put")){var r=e.requireElement("expression",n),a=n.matchAnyToken("into","before","after");null==a&&n.matchToken("at")&&(n.matchToken("the"),a=n.matchAnyToken("start","end"),n.requireToken("of")),null==a&&e.raiseParseError(n,"Expected one of 'into', 'before', 'at start of', 'at end of', 'after'");var i=e.requireElement("expression",n),u=a.value,l=!1,s=!1,c=null,f=null;if("arrayIndex"===i.type&&"into"===u)l=!0,f=i.prop,c=i.root;else if(i.prop&&i.root&&"into"===u)f=i.prop.value,c=i.root;else if("symbol"===i.type&&"into"===u)s=!0,f=i.name;else if("attributeRef"===i.type&&"into"===u){var m=!0;f=i.name,c=e.requireElement("implicitMeTarget",n)}else if("styleRef"===i.type&&"into"===u){var p=!0;f=i.name,c=e.requireElement("implicitMeTarget",n)}else i.attribute&&"into"===u?(m="attributeRef"===i.attribute.type,p="styleRef"===i.attribute.type,f=i.attribute.name,c=i.root):c=i;var d={target:i,operation:u,symbolWrite:s,value:r,args:[c,f,r],op:function(e,n,r,a){if(s)o(t,e,r,a);else if(t.nullCheck(n,c),"into"===u)m?t.implicitLoop(n,function(e){e.setAttribute(r,a)}):p?t.implicitLoop(n,function(e){e.style[r]=a}):l?n[r]=a:t.implicitLoop(n,function(e){o(t,e,r,a)});else{var i="before"===u?Element.prototype.before:"after"===u?Element.prototype.after:"start"===u?Element.prototype.prepend:Element.prototype.append;t.implicitLoop(n,function(e){i.call(e,a instanceof Node?a:t.convertValue(a,"Fragment"))})}return t.findNext(this,e)}};return d}}),e.addCommand("transition",function(t,n,r){if(r.matchToken("transition")){for(var o=a(t,n,r),i=[],u=[],l=[],s=r.currentToken();!t.commandBoundary(s)&&"over"!==s.value&&"using"!==s.value;)"STYLE_REF"===r.currentToken().type?function(){var e=r.consumeToken().value.substr(1);i.push({type:"styleRefValue",evaluate:function(){return e}})}():i.push(t.requireElement("stringLike",r)),r.matchToken("from")?u.push(t.requireElement("expression",r)):u.push(null),r.requireToken("to"),r.matchToken("initial")?l.push({type:"initial_literal",evaluate:function(){return"initial"}}):l.push(t.requireElement("expression",r)),s=r.currentToken();if(r.matchToken("over"))var c=t.requireElement("expression",r);else if(r.matchToken("using"))var f=t.requireElement("expression",r);var m={to:l,args:[o,i,u,l,f,c],op:function(t,r,a,i,u,l,s){n.nullCheck(r,o);var c=[];return n.implicitLoop(r,function(t){var r=new Promise(function(r,o){var c=t.style.transition;t.style.transition=s?"all "+s+"ms ease-in":l||e.config.defaultTransition;for(var f=n.getInternalData(t),m=getComputedStyle(t),p={},d=0;d<m.length;d++){var v=m[d];p[v]=m[v]}for(f.initalStyles||(f.initalStyles=p),d=0;d<a.length;d++){var h=a[d],E=i[d];t.style[h]="computed"===E||null==E?p[h]:E}var y=!1,T=!1;t.addEventListener("transitionend",function(){T||(t.style.transition=c,T=!0,r())},{once:!0}),t.addEventListener("transitionstart",function(){y=!0},{once:!0}),setTimeout(function(){T||y||(t.style.transition=c,T=!0,r())},100),setTimeout(function(){for(var e=0;e<a.length;e++){var n=a[e],r=u[e];t.style[n]="initial"===r?f.initalStyles[n]:r}},0)});c.push(r)}),Promise.all(c).then(function(){return n.findNext(m,t)})}};return m}}),e.addCommand("measure",function(e,t,n){if(n.matchToken("measure")){var r=a(e,t,n),o=[];if(!e.commandBoundary(n.currentToken()))do{o.push(n.matchTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));return{properties:o,args:[r],op:function(e,n){t.nullCheck(n,r),0 in n&&(n=n[0]);var a=n.getBoundingClientRect(),i={top:n.scrollTop,left:n.scrollLeft,topMax:n.scrollTopMax,leftMax:n.scrollLeftMax,height:n.scrollHeight,width:n.scrollWidth};return e.result={x:a.x,y:a.y,left:a.left,top:a.top,right:a.right,bottom:a.bottom,width:a.width,height:a.height,bounds:a,scrollLeft:i.left,scrollTop:i.top,scrollLeftMax:i.leftMax,scrollTopMax:i.topMax,scrollWidth:i.width,scrollHeight:i.height,scroll:i},t.forEach(o,function(t){if(!(t in e.result))throw"No such measurement as "+t;e[t]=e.result[t]}),t.findNext(this,e)}}}}),e.addLeafExpression("closestExpr",function(e,t,n){if(n.matchToken("closest")){if(n.matchToken("parent"))var r=!0;var o=null;if("ATTRIBUTE_REF"===n.currentToken().type){var a=e.requireElement("attributeRefAccess",n,null);o="["+a.attribute.name+"]"}if(null==o){var i=e.requireElement("expression",n);null==i.css?e.raiseParseError(n,"Expected a CSS expression"):o=i.css}if(n.matchToken("to"))var u=e.parseElement("expression",n);else u=e.parseElement("implicitMeTarget",n);var l={type:"closestExpr",parentSearch:r,expr:i,css:o,to:u,args:[u],op:function(e,n){if(null==n)return null;var a=[];return t.implicitLoop(n,function(e){a.push(r?e.parentElement?e.parentElement.closest(o):null:e.closest(o))}),t.shouldAutoIterate(n)?a:a[0]},evaluate:function(e){return t.unifiedEval(this,e)}};return a?(a.root=l,a.args=[l],a):l}}),e.addCommand("go",function(e,t,n){if(n.matchToken("go")){if(n.matchToken("back"))var r=!0;else if(n.matchToken("to"),n.matchToken("url")){var o=e.requireElement("stringLike",n),a=!0;if(n.matchToken("in")){n.requireToken("new"),n.requireToken("window");var i=!0}}else{n.matchToken("the");var u=n.matchAnyToken("top","middle","bottom"),l=n.matchAnyToken("left","center","right");(u||l)&&n.requireToken("of"),o=e.requireElement("unaryExpression",n);var s=n.matchAnyOpToken("+","-");if(s){n.pushFollow("px");try{var c=e.requireElement("expression",n)}finally{n.popFollow()}}n.matchToken("px");var f=n.matchAnyToken("smoothly","instantly"),m={};u&&("top"===u.value?m.block="start":"bottom"===u.value?m.block="end":"middle"===u.value&&(m.block="center")),l&&("left"===l.value?m.inline="start":"center"===l.value?m.inline="center":"right"===l.value&&(m.inline="end")),f&&("smoothly"===f.value?m.behavior="smooth":"instantly"===f.value&&(m.behavior="instant"))}var p={target:o,args:[o,c],op:function(e,n,o){return r?window.history.back():a?n&&(i?window.open(n):window.location.href=n):t.implicitLoop(n,function(e){if(e===window&&(e=document.body),s){var t=e.getBoundingClientRect(),n=document.createElement("div");if("-"===s.value)var r=-o;else r=o;n.style.position="absolute",n.style.top=t.x+r+"px",n.style.left=t.y+r+"px",n.style.height=t.height+2*r+"px",n.style.width=t.width+2*r+"px",n.style.zIndex=""+Number.MIN_SAFE_INTEGER,n.style.opacity="0",document.body.appendChild(n),setTimeout(function(){document.body.removeChild(n)},100),e=n}e.scrollIntoView(m)}),t.findNext(p,e)}};return p}}),e.config.conversions.dynamicResolvers.push(function(t,n){if("Values"===t||0===t.indexOf("Values:")){var r=t.split(":")[1],o={};if((0,e.internals.runtime.implicitLoop)(n,function(e){var t=i(e);void 0===t?null!=e.querySelectorAll&&e.querySelectorAll("input,select,textarea").forEach(a):o[t.name]=t.value}),r){if("JSON"===r)return JSON.stringify(o);if("Form"===r)return new URLSearchParams(o).toString();throw"Unknown conversion: "+r}return o}function a(e){var t=i(e);null!=t&&(null!=o[t.name]?Array.isArray(o[t.name])&&Array.isArray(t.value)&&(o[t.name]=[].concat(o[t.name],t.value)):o[t.name]=t.value)}function i(e){try{var t={name:e.name,value:e.value};if(null==t.name||null==t.value)return;if("radio"==e.type&&0==e.checked)return;if("checkbox"==e.type&&(0==e.checked?t.value=void 0:"string"==typeof t.value&&(t.value=[t.value])),"select-multiple"==e.type){var n=e.querySelectorAll("option[selected]");t.value=[];for(var r=0;r<n.length;r++)t.value.push(n[r].value)}return t}catch(e){return}}}),e.config.conversions.HTML=function(e){return function e(t){if(t instanceof Array)return t.map(function(t){return e(t)}).join("");if(t instanceof HTMLElement)return t.outerHTML;if(t instanceof NodeList){for(var n="",r=0;r<t.length;r++){var o=t[r];o instanceof HTMLElement&&(n+=o.outerHTML)}return n}return t.toString?t.toString():""}(e)},e.config.conversions.Fragment=function(t){var n=document.createDocumentFragment();return e.internals.runtime.implicitLoop(t,function(e){if(e instanceof Node)n.append(e);else{var t=document.createElement("template");t.innerHTML=e,n.append(t.content)}}),n}}(A),A});
+//# sourceMappingURL=_hyperscript_web.min.js.map
diff --git a/common/static/lib/js/multiple-select.min.js b/common/static/lib/js/multiple-select.min.js
deleted file mode 100644
index 7bffe99d..00000000
--- a/common/static/lib/js/multiple-select.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
-  * multiple-select - Multiple select is a jQuery plugin to select multiple elements with checkboxes :).
-  *
-  * @version v1.5.2
-  * @homepage http://multiple-select.wenzhixin.net.cn
-  * @author wenzhixin <wenzhixin2010@gmail.com> (http://wenzhixin.net.cn/)
-  * @license MIT
-  */
-
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery")):"function"==typeof define&&define.amd?define(["jquery"],e):e((t=t||self).jQuery)}(this,(function(t){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function r(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t}function u(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){if(!(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t)))return;var n=[],i=!0,r=!1,u=void 0;try{for(var o,s=t[Symbol.iterator]();!(i=(o=s.next()).done)&&(n.push(o.value),!e||n.length!==e);i=!0);}catch(t){r=!0,u=t}finally{try{i||null==s.return||s.return()}finally{if(r)throw u}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}function o(t){return function(t){if(Array.isArray(t)){for(var e=0,n=new Array(t.length);e<t.length;e++)n[e]=t[e];return n}}(t)||function(t){if(Symbol.iterator in Object(t)||"[object Arguments]"===Object.prototype.toString.call(t))return Array.from(t)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}t=t&&t.hasOwnProperty("default")?t.default:t;var s="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function l(t,e){return t(e={exports:{}},e.exports),e.exports}var a,c,h,f="object",p=function(t){return t&&t.Math==Math&&t},d=p(typeof globalThis==f&&globalThis)||p(typeof window==f&&window)||p(typeof self==f&&self)||p(typeof s==f&&s)||Function("return this")(),v=function(t){try{return!!t()}catch(t){return!0}},g=!v((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})),y={}.propertyIsEnumerable,E=Object.getOwnPropertyDescriptor,b={f:E&&!y.call({1:2},1)?function(t){var e=E(this,t);return!!e&&e.enumerable}:y},m=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},A={}.toString,F=function(t){return A.call(t).slice(8,-1)},S="".split,C=v((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==F(t)?S.call(t,""):Object(t)}:Object,k=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t},D=function(t){return C(k(t))},O=function(t){return"object"==typeof t?null!==t:"function"==typeof t},w=function(t,e){if(!O(t))return t;var n,i;if(e&&"function"==typeof(n=t.toString)&&!O(i=n.call(t)))return i;if("function"==typeof(n=t.valueOf)&&!O(i=n.call(t)))return i;if(!e&&"function"==typeof(n=t.toString)&&!O(i=n.call(t)))return i;throw TypeError("Can't convert object to primitive value")},x={}.hasOwnProperty,$=function(t,e){return x.call(t,e)},B=d.document,j=O(B)&&O(B.createElement),T=function(t){return j?B.createElement(t):{}},L=!g&&!v((function(){return 7!=Object.defineProperty(T("div"),"a",{get:function(){return 7}}).a})),_=Object.getOwnPropertyDescriptor,I={f:g?_:function(t,e){if(t=D(t),e=w(e,!0),L)try{return _(t,e)}catch(t){}if($(t,e))return m(!b.f.call(t,e),t[e])}},R=function(t){if(!O(t))throw TypeError(String(t)+" is not an object");return t},M=Object.defineProperty,P={f:g?M:function(t,e,n){if(R(t),e=w(e,!0),R(n),L)try{return M(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(t[e]=n.value),t}},N=g?function(t,e,n){return P.f(t,e,m(1,n))}:function(t,e,n){return t[e]=n,t},H=function(t,e){try{N(d,t,e)}catch(n){d[t]=e}return e},G=l((function(t){var e=d["__core-js_shared__"]||H("__core-js_shared__",{});(t.exports=function(t,n){return e[t]||(e[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.2.1",mode:"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})})),U=G("native-function-to-string",Function.toString),W=d.WeakMap,V="function"==typeof W&&/native code/.test(U.call(W)),K=0,z=Math.random(),q=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++K+z).toString(36)},Y=G("keys"),J=function(t){return Y[t]||(Y[t]=q(t))},Q={},Z=d.WeakMap;if(V){var X=new Z,tt=X.get,et=X.has,nt=X.set;a=function(t,e){return nt.call(X,t,e),e},c=function(t){return tt.call(X,t)||{}},h=function(t){return et.call(X,t)}}else{var it=J("state");Q[it]=!0,a=function(t,e){return N(t,it,e),e},c=function(t){return $(t,it)?t[it]:{}},h=function(t){return $(t,it)}}var rt={set:a,get:c,has:h,enforce:function(t){return h(t)?c(t):a(t,{})},getterFor:function(t){return function(e){var n;if(!O(e)||(n=c(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}}},ut=l((function(t){var e=rt.get,n=rt.enforce,i=String(U).split("toString");G("inspectSource",(function(t){return U.call(t)})),(t.exports=function(t,e,r,u){var o=!!u&&!!u.unsafe,s=!!u&&!!u.enumerable,l=!!u&&!!u.noTargetGet;"function"==typeof r&&("string"!=typeof e||$(r,"name")||N(r,"name",e),n(r).source=i.join("string"==typeof e?e:"")),t!==d?(o?!l&&t[e]&&(s=!0):delete t[e],s?t[e]=r:N(t,e,r)):s?t[e]=r:H(e,r)})(Function.prototype,"toString",(function(){return"function"==typeof this&&e(this).source||U.call(this)}))})),ot=d,st=function(t){return"function"==typeof t?t:void 0},lt=function(t,e){return arguments.length<2?st(ot[t])||st(d[t]):ot[t]&&ot[t][e]||d[t]&&d[t][e]},at=Math.ceil,ct=Math.floor,ht=function(t){return isNaN(t=+t)?0:(t>0?ct:at)(t)},ft=Math.min,pt=function(t){return t>0?ft(ht(t),9007199254740991):0},dt=Math.max,vt=Math.min,gt=function(t,e){var n=ht(t);return n<0?dt(n+e,0):vt(n,e)},yt=function(t){return function(e,n,i){var r,u=D(e),o=pt(u.length),s=gt(i,o);if(t&&n!=n){for(;o>s;)if((r=u[s++])!=r)return!0}else for(;o>s;s++)if((t||s in u)&&u[s]===n)return t||s||0;return!t&&-1}},Et={includes:yt(!0),indexOf:yt(!1)},bt=Et.indexOf,mt=function(t,e){var n,i=D(t),r=0,u=[];for(n in i)!$(Q,n)&&$(i,n)&&u.push(n);for(;e.length>r;)$(i,n=e[r++])&&(~bt(u,n)||u.push(n));return u},At=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Ft=At.concat("length","prototype"),St={f:Object.getOwnPropertyNames||function(t){return mt(t,Ft)}},Ct={f:Object.getOwnPropertySymbols},kt=lt("Reflect","ownKeys")||function(t){var e=St.f(R(t)),n=Ct.f;return n?e.concat(n(t)):e},Dt=function(t,e){for(var n=kt(e),i=P.f,r=I.f,u=0;u<n.length;u++){var o=n[u];$(t,o)||i(t,o,r(e,o))}},Ot=/#|\.prototype\./,wt=function(t,e){var n=$t[xt(t)];return n==jt||n!=Bt&&("function"==typeof e?v(e):!!e)},xt=wt.normalize=function(t){return String(t).replace(Ot,".").toLowerCase()},$t=wt.data={},Bt=wt.NATIVE="N",jt=wt.POLYFILL="P",Tt=wt,Lt=I.f,_t=function(t,e){var n,i,r,u,o,s=t.target,l=t.global,a=t.stat;if(n=l?d:a?d[s]||H(s,{}):(d[s]||{}).prototype)for(i in e){if(u=e[i],r=t.noTargetGet?(o=Lt(n,i))&&o.value:n[i],!Tt(l?i:s+(a?".":"#")+i,t.forced)&&void 0!==r){if(typeof u==typeof r)continue;Dt(u,r)}(t.sham||r&&r.sham)&&N(u,"sham",!0),ut(n,i,u,t)}},It=!!Object.getOwnPropertySymbols&&!v((function(){return!String(Symbol())})),Rt=d.Symbol,Mt=G("wks"),Pt=function(t){return Mt[t]||(Mt[t]=It&&Rt[t]||(It?Rt:q)("Symbol."+t))},Nt=Object.keys||function(t){return mt(t,At)},Ht=g?Object.defineProperties:function(t,e){R(t);for(var n,i=Nt(e),r=i.length,u=0;r>u;)P.f(t,n=i[u++],e[n]);return t},Gt=lt("document","documentElement"),Ut=J("IE_PROTO"),Wt=function(){},Vt=function(){var t,e=T("iframe"),n=At.length;for(e.style.display="none",Gt.appendChild(e),e.src=String("javascript:"),(t=e.contentWindow.document).open(),t.write("<script>document.F=Object<\/script>"),t.close(),Vt=t.F;n--;)delete Vt.prototype[At[n]];return Vt()},Kt=Object.create||function(t,e){var n;return null!==t?(Wt.prototype=R(t),n=new Wt,Wt.prototype=null,n[Ut]=t):n=Vt(),void 0===e?n:Ht(n,e)};Q[Ut]=!0;var zt=Pt("unscopables"),qt=Array.prototype;null==qt[zt]&&N(qt,zt,Kt(null));var Yt=function(t){qt[zt][t]=!0},Jt=Et.includes;_t({target:"Array",proto:!0},{includes:function(t){return Jt(this,t,arguments.length>1?arguments[1]:void 0)}}),Yt("includes");var Qt=function(t){return Object(k(t))},Zt=Object.assign,Xt=!Zt||v((function(){var t={},e={},n=Symbol();return t[n]=7,"abcdefghijklmnopqrst".split("").forEach((function(t){e[t]=t})),7!=Zt({},t)[n]||"abcdefghijklmnopqrst"!=Nt(Zt({},e)).join("")}))?function(t,e){for(var n=Qt(t),i=arguments.length,r=1,u=Ct.f,o=b.f;i>r;)for(var s,l=C(arguments[r++]),a=u?Nt(l).concat(u(l)):Nt(l),c=a.length,h=0;c>h;)s=a[h++],g&&!o.call(l,s)||(n[s]=l[s]);return n}:Zt;_t({target:"Object",stat:!0,forced:Object.assign!==Xt},{assign:Xt});var te=Pt("match"),ee=function(t){var e;return O(t)&&(void 0!==(e=t[te])?!!e:"RegExp"==F(t))},ne=function(t){if(ee(t))throw TypeError("The method doesn't accept regular expressions");return t},ie=Pt("match");_t({target:"String",proto:!0,forced:!function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[ie]=!1,"/./"[t](e)}catch(t){}}return!1}("includes")},{includes:function(t){return!!~String(k(this)).indexOf(ne(t),arguments.length>1?arguments[1]:void 0)}});var re="\t\n\v\f\r                 \u2028\u2029\ufeff",ue="["+re+"]",oe=RegExp("^"+ue+ue+"*"),se=RegExp(ue+ue+"*$"),le=function(t){return function(e){var n=String(k(e));return 1&t&&(n=n.replace(oe,"")),2&t&&(n=n.replace(se,"")),n}},ae={start:le(1),end:le(2),trim:le(3)},ce=ae.trim;_t({target:"String",proto:!0,forced:function(t){return v((function(){return!!re[t]()||"​…᠎"!="​…᠎"[t]()||re[t].name!==t}))}("trim")},{trim:function(){return ce(this)}});var he={name:"",placeholder:"",data:void 0,locale:void 0,selectAll:!0,single:void 0,singleRadio:!1,multiple:!1,hideOptgroupCheckboxes:!1,multipleWidth:80,width:void 0,dropWidth:void 0,maxHeight:250,maxHeightUnit:"px",position:"bottom",displayValues:!1,displayTitle:!1,displayDelimiter:", ",minimumCountSelected:3,ellipsis:!1,isOpen:!1,keepOpen:!1,openOnHover:!1,container:null,filter:!1,filterGroup:!1,filterPlaceholder:"",filterAcceptOnEnter:!1,filterByDataLength:void 0,customFilter:function(t,e){return t.includes(e)},showClear:!1,animate:void 0,styler:function(){return!1},textTemplate:function(t){return t[0].innerHTML.trim()},labelTemplate:function(t){return t[0].getAttribute("label")},onOpen:function(){return!1},onClose:function(){return!1},onCheckAll:function(){return!1},onUncheckAll:function(){return!1},onFocus:function(){return!1},onBlur:function(){return!1},onOptgroupClick:function(){return!1},onClick:function(){return!1},onFilter:function(){return!1},onClear:function(){return!1},onAfterCreate:function(){return!1}},fe={formatSelectAll:function(){return"[Select all]"},formatAllSelected:function(){return"All selected"},formatCountSelected:function(t,e){return t+" of "+e+" selected"},formatNoMatchesFound:function(){return"No matches found"}};Object.assign(he,fe);var pe={VERSION:"1.5.2",BLOCK_ROWS:50,CLUSTER_BLOCKS:4,DEFAULTS:he,METHODS:["getOptions","refreshOptions","getSelects","setSelects","enable","disable","open","close","check","uncheck","checkAll","uncheckAll","checkInvert","focus","blur","refresh","destroy"],LOCALES:{en:fe,"en-US":fe}},de=Array.isArray||function(t){return"Array"==F(t)},ve=St.f,ge={}.toString,ye="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],Ee={f:function(t){return ye&&"[object Window]"==ge.call(t)?function(t){try{return ve(t)}catch(t){return ye.slice()}}(t):ve(D(t))}},be={f:Pt},me=P.f,Ae=function(t){var e=ot.Symbol||(ot.Symbol={});$(e,t)||me(e,t,{value:be.f(t)})},Fe=P.f,Se=Pt("toStringTag"),Ce=function(t,e,n){t&&!$(t=n?t:t.prototype,Se)&&Fe(t,Se,{configurable:!0,value:e})},ke=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t},De=Pt("species"),Oe=function(t,e){var n;return de(t)&&("function"!=typeof(n=t.constructor)||n!==Array&&!de(n.prototype)?O(n)&&null===(n=n[De])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===e?0:e)},we=[].push,xe=function(t){var e=1==t,n=2==t,i=3==t,r=4==t,u=6==t,o=5==t||u;return function(s,l,a,c){for(var h,f,p=Qt(s),d=C(p),v=function(t,e,n){if(ke(t),void 0===e)return t;switch(n){case 0:return function(){return t.call(e)};case 1:return function(n){return t.call(e,n)};case 2:return function(n,i){return t.call(e,n,i)};case 3:return function(n,i,r){return t.call(e,n,i,r)}}return function(){return t.apply(e,arguments)}}(l,a,3),g=pt(d.length),y=0,E=c||Oe,b=e?E(s,g):n?E(s,0):void 0;g>y;y++)if((o||y in d)&&(f=v(h=d[y],y,p),t))if(e)b[y]=f;else if(f)switch(t){case 3:return!0;case 5:return h;case 6:return y;case 2:we.call(b,h)}else if(r)return!1;return u?-1:i||r?r:b}},$e={forEach:xe(0),map:xe(1),filter:xe(2),some:xe(3),every:xe(4),find:xe(5),findIndex:xe(6)},Be=$e.forEach,je=J("hidden"),Te=Pt("toPrimitive"),Le=rt.set,_e=rt.getterFor("Symbol"),Ie=Object.prototype,Re=d.Symbol,Me=d.JSON,Pe=Me&&Me.stringify,Ne=I.f,He=P.f,Ge=Ee.f,Ue=b.f,We=G("symbols"),Ve=G("op-symbols"),Ke=G("string-to-symbol-registry"),ze=G("symbol-to-string-registry"),qe=G("wks"),Ye=d.QObject,Je=!Ye||!Ye.prototype||!Ye.prototype.findChild,Qe=g&&v((function(){return 7!=Kt(He({},"a",{get:function(){return He(this,"a",{value:7}).a}})).a}))?function(t,e,n){var i=Ne(Ie,e);i&&delete Ie[e],He(t,e,n),i&&t!==Ie&&He(Ie,e,i)}:He,Ze=function(t,e){var n=We[t]=Kt(Re.prototype);return Le(n,{type:"Symbol",tag:t,description:e}),g||(n.description=e),n},Xe=It&&"symbol"==typeof Re.iterator?function(t){return"symbol"==typeof t}:function(t){return Object(t)instanceof Re},tn=function(t,e,n){t===Ie&&tn(Ve,e,n),R(t);var i=w(e,!0);return R(n),$(We,i)?(n.enumerable?($(t,je)&&t[je][i]&&(t[je][i]=!1),n=Kt(n,{enumerable:m(0,!1)})):($(t,je)||He(t,je,m(1,{})),t[je][i]=!0),Qe(t,i,n)):He(t,i,n)},en=function(t,e){R(t);var n=D(e),i=Nt(n).concat(on(n));return Be(i,(function(e){g&&!nn.call(n,e)||tn(t,e,n[e])})),t},nn=function(t){var e=w(t,!0),n=Ue.call(this,e);return!(this===Ie&&$(We,e)&&!$(Ve,e))&&(!(n||!$(this,e)||!$(We,e)||$(this,je)&&this[je][e])||n)},rn=function(t,e){var n=D(t),i=w(e,!0);if(n!==Ie||!$(We,i)||$(Ve,i)){var r=Ne(n,i);return!r||!$(We,i)||$(n,je)&&n[je][i]||(r.enumerable=!0),r}},un=function(t){var e=Ge(D(t)),n=[];return Be(e,(function(t){$(We,t)||$(Q,t)||n.push(t)})),n},on=function(t){var e=t===Ie,n=Ge(e?Ve:D(t)),i=[];return Be(n,(function(t){!$(We,t)||e&&!$(Ie,t)||i.push(We[t])})),i};It||(ut((Re=function(){if(this instanceof Re)throw TypeError("Symbol is not a constructor");var t=arguments.length&&void 0!==arguments[0]?String(arguments[0]):void 0,e=q(t),n=function(t){this===Ie&&n.call(Ve,t),$(this,je)&&$(this[je],e)&&(this[je][e]=!1),Qe(this,e,m(1,t))};return g&&Je&&Qe(Ie,e,{configurable:!0,set:n}),Ze(e,t)}).prototype,"toString",(function(){return _e(this).tag})),b.f=nn,P.f=tn,I.f=rn,St.f=Ee.f=un,Ct.f=on,g&&(He(Re.prototype,"description",{configurable:!0,get:function(){return _e(this).description}}),ut(Ie,"propertyIsEnumerable",nn,{unsafe:!0})),be.f=function(t){return Ze(Pt(t),t)}),_t({global:!0,wrap:!0,forced:!It,sham:!It},{Symbol:Re}),Be(Nt(qe),(function(t){Ae(t)})),_t({target:"Symbol",stat:!0,forced:!It},{for:function(t){var e=String(t);if($(Ke,e))return Ke[e];var n=Re(e);return Ke[e]=n,ze[n]=e,n},keyFor:function(t){if(!Xe(t))throw TypeError(t+" is not a symbol");if($(ze,t))return ze[t]},useSetter:function(){Je=!0},useSimple:function(){Je=!1}}),_t({target:"Object",stat:!0,forced:!It,sham:!g},{create:function(t,e){return void 0===e?Kt(t):en(Kt(t),e)},defineProperty:tn,defineProperties:en,getOwnPropertyDescriptor:rn}),_t({target:"Object",stat:!0,forced:!It},{getOwnPropertyNames:un,getOwnPropertySymbols:on}),_t({target:"Object",stat:!0,forced:v((function(){Ct.f(1)}))},{getOwnPropertySymbols:function(t){return Ct.f(Qt(t))}}),Me&&_t({target:"JSON",stat:!0,forced:!It||v((function(){var t=Re();return"[null]"!=Pe([t])||"{}"!=Pe({a:t})||"{}"!=Pe(Object(t))}))},{stringify:function(t){for(var e,n,i=[t],r=1;arguments.length>r;)i.push(arguments[r++]);if(n=e=i[1],(O(e)||void 0!==t)&&!Xe(t))return de(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!Xe(e))return e}),i[1]=e,Pe.apply(Me,i)}}),Re.prototype[Te]||N(Re.prototype,Te,Re.prototype.valueOf),Ce(Re,"Symbol"),Q[je]=!0;var sn=P.f,ln=d.Symbol;if(g&&"function"==typeof ln&&(!("description"in ln.prototype)||void 0!==ln().description)){var an={},cn=function(){var t=arguments.length<1||void 0===arguments[0]?void 0:String(arguments[0]),e=this instanceof cn?new ln(t):void 0===t?ln():ln(t);return""===t&&(an[e]=!0),e};Dt(cn,ln);var hn=cn.prototype=ln.prototype;hn.constructor=cn;var fn=hn.toString,pn="Symbol(test)"==String(ln("test")),dn=/^Symbol\((.*)\)[^)]+$/;sn(hn,"description",{configurable:!0,get:function(){var t=O(this)?this.valueOf():this,e=fn.call(t);if($(an,t))return"";var n=pn?e.slice(7,-1):e.replace(dn,"$1");return""===n?void 0:n}}),_t({global:!0,forced:!0},{Symbol:cn})}Ae("iterator");var vn=function(t,e,n){var i=w(e);i in t?P.f(t,i,m(0,n)):t[i]=n},gn=Pt("species"),yn=function(t){return!v((function(){var e=[];return(e.constructor={})[gn]=function(){return{foo:1}},1!==e[t](Boolean).foo}))},En=Pt("isConcatSpreadable"),bn=!v((function(){var t=[];return t[En]=!1,t.concat()[0]!==t})),mn=yn("concat"),An=function(t){if(!O(t))return!1;var e=t[En];return void 0!==e?!!e:de(t)};_t({target:"Array",proto:!0,forced:!bn||!mn},{concat:function(t){var e,n,i,r,u,o=Qt(this),s=Oe(o,0),l=0;for(e=-1,i=arguments.length;e<i;e++)if(u=-1===e?o:arguments[e],An(u)){if(l+(r=pt(u.length))>9007199254740991)throw TypeError("Maximum allowed index exceeded");for(n=0;n<r;n++,l++)n in u&&vn(s,l,u[n])}else{if(l>=9007199254740991)throw TypeError("Maximum allowed index exceeded");vn(s,l++,u)}return s.length=l,s}});var Fn=$e.filter;_t({target:"Array",proto:!0,forced:!yn("filter")},{filter:function(t){return Fn(this,t,arguments.length>1?arguments[1]:void 0)}});var Sn=$e.find,Cn=!0;"find"in[]&&Array(1).find((function(){Cn=!1})),_t({target:"Array",proto:!0,forced:Cn},{find:function(t){return Sn(this,t,arguments.length>1?arguments[1]:void 0)}}),Yt("find");var kn,Dn,On,wn=!v((function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype})),xn=J("IE_PROTO"),$n=Object.prototype,Bn=wn?Object.getPrototypeOf:function(t){return t=Qt(t),$(t,xn)?t[xn]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?$n:null},jn=Pt("iterator"),Tn=!1;[].keys&&("next"in(On=[].keys())?(Dn=Bn(Bn(On)))!==Object.prototype&&(kn=Dn):Tn=!0),null==kn&&(kn={}),$(kn,jn)||N(kn,jn,(function(){return this}));var Ln={IteratorPrototype:kn,BUGGY_SAFARI_ITERATORS:Tn},_n=Ln.IteratorPrototype,In=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,n={};try{(t=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(n,[]),e=n instanceof Array}catch(t){}return function(n,i){return R(n),function(t){if(!O(t)&&null!==t)throw TypeError("Can't set "+String(t)+" as a prototype")}(i),e?t.call(n,i):n.__proto__=i,n}}():void 0),Rn=Ln.IteratorPrototype,Mn=Ln.BUGGY_SAFARI_ITERATORS,Pn=Pt("iterator"),Nn=function(){return this},Hn=function(t,e,n,i,r,u,o){!function(t,e,n){var i=e+" Iterator";t.prototype=Kt(_n,{next:m(1,n)}),Ce(t,i,!1)}(n,e,i);var s,l,a,c=function(t){if(t===r&&v)return v;if(!Mn&&t in p)return p[t];switch(t){case"keys":case"values":case"entries":return function(){return new n(this,t)}}return function(){return new n(this)}},h=e+" Iterator",f=!1,p=t.prototype,d=p[Pn]||p["@@iterator"]||r&&p[r],v=!Mn&&d||c(r),g="Array"==e&&p.entries||d;if(g&&(s=Bn(g.call(new t)),Rn!==Object.prototype&&s.next&&(Bn(s)!==Rn&&(In?In(s,Rn):"function"!=typeof s[Pn]&&N(s,Pn,Nn)),Ce(s,h,!0))),"values"==r&&d&&"values"!==d.name&&(f=!0,v=function(){return d.call(this)}),p[Pn]!==v&&N(p,Pn,v),r)if(l={values:c("values"),keys:u?v:c("keys"),entries:c("entries")},o)for(a in l)!Mn&&!f&&a in p||ut(p,a,l[a]);else _t({target:e,proto:!0,forced:Mn||f},l);return l},Gn=rt.set,Un=rt.getterFor("Array Iterator"),Wn=Hn(Array,"Array",(function(t,e){Gn(this,{type:"Array Iterator",target:D(t),index:0,kind:e})}),(function(){var t=Un(this),e=t.target,n=t.kind,i=t.index++;return!e||i>=e.length?(t.target=void 0,{value:void 0,done:!0}):"keys"==n?{value:i,done:!1}:"values"==n?{value:e[i],done:!1}:{value:[i,e[i]],done:!1}}),"values");Yt("keys"),Yt("values"),Yt("entries");var Vn=function(t,e){var n=[][t];return!n||!v((function(){n.call(null,e||function(){throw 1},1)}))},Kn=[].join,zn=C!=Object,qn=Vn("join",",");_t({target:"Array",proto:!0,forced:zn||qn},{join:function(t){return Kn.call(D(this),void 0===t?",":t)}});var Yn=$e.map;_t({target:"Array",proto:!0,forced:!yn("map")},{map:function(t){return Yn(this,t,arguments.length>1?arguments[1]:void 0)}});var Jn=Pt("species"),Qn=[].slice,Zn=Math.max;_t({target:"Array",proto:!0,forced:!yn("slice")},{slice:function(t,e){var n,i,r,u=D(this),o=pt(u.length),s=gt(t,o),l=gt(void 0===e?o:e,o);if(de(u)&&("function"!=typeof(n=u.constructor)||n!==Array&&!de(n.prototype)?O(n)&&null===(n=n[Jn])&&(n=void 0):n=void 0,n===Array||void 0===n))return Qn.call(u,s,l);for(i=new(void 0===n?Array:n)(Zn(l-s,0)),r=0;s<l;s++,r++)s in u&&vn(i,r,u[s]);return i.length=r,i}});var Xn=P.f,ti=Function.prototype,ei=ti.toString,ni=/^\s*function ([^ (]*)/;!g||"name"in ti||Xn(ti,"name",{configurable:!0,get:function(){try{return ei.call(this).match(ni)[1]}catch(t){return""}}});var ii=b.f,ri=function(t){return function(e){for(var n,i=D(e),r=Nt(i),u=r.length,o=0,s=[];u>o;)n=r[o++],g&&!ii.call(i,n)||s.push(t?[n,i[n]]:i[n]);return s}},ui={entries:ri(!0),values:ri(!1)}.entries;_t({target:"Object",stat:!0},{entries:function(t){return ui(t)}});var oi=v((function(){Nt(1)}));_t({target:"Object",stat:!0,forced:oi},{keys:function(t){return Nt(Qt(t))}});var si=Pt("toStringTag"),li="Arguments"==F(function(){return arguments}()),ai={};ai[Pt("toStringTag")]="z";var ci="[object z]"!==String(ai)?function(){return"[object "+function(t){var e,n,i;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=function(t,e){try{return t[e]}catch(t){}}(e=Object(t),si))?n:li?F(e):"Object"==(i=F(e))&&"function"==typeof e.callee?"Arguments":i}(this)+"]"}:ai.toString,hi=Object.prototype;ci!==hi.toString&&ut(hi,"toString",ci,{unsafe:!0});var fi=function(t){return function(e,n){var i,r,u=String(k(e)),o=ht(n),s=u.length;return o<0||o>=s?t?"":void 0:(i=u.charCodeAt(o))<55296||i>56319||o+1===s||(r=u.charCodeAt(o+1))<56320||r>57343?t?u.charAt(o):i:t?u.slice(o,o+2):r-56320+(i-55296<<10)+65536}},pi={codeAt:fi(!1),charAt:fi(!0)},di=pi.charAt,vi=rt.set,gi=rt.getterFor("String Iterator");Hn(String,"String",(function(t){vi(this,{type:"String Iterator",string:String(t),index:0})}),(function(){var t,e=gi(this),n=e.string,i=e.index;return i>=n.length?{value:void 0,done:!0}:(t=di(n,i),e.index+=t.length,{value:t,done:!1})}));var yi,Ei,bi=function(){var t=R(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e},mi=RegExp.prototype.exec,Ai=String.prototype.replace,Fi=mi,Si=(yi=/a/,Ei=/b*/g,mi.call(yi,"a"),mi.call(Ei,"a"),0!==yi.lastIndex||0!==Ei.lastIndex),Ci=void 0!==/()??/.exec("")[1];(Si||Ci)&&(Fi=function(t){var e,n,i,r,u=this;return Ci&&(n=new RegExp("^"+u.source+"$(?!\\s)",bi.call(u))),Si&&(e=u.lastIndex),i=mi.call(u,t),Si&&i&&(u.lastIndex=u.global?i.index+i[0].length:e),Ci&&i&&i.length>1&&Ai.call(i[0],n,(function(){for(r=1;r<arguments.length-2;r++)void 0===arguments[r]&&(i[r]=void 0)})),i});var ki=Fi,Di=Pt("species"),Oi=!v((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$<a>")})),wi=!v((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]})),xi=function(t,e,n,i){var r=Pt(t),u=!v((function(){var e={};return e[r]=function(){return 7},7!=""[t](e)})),o=u&&!v((function(){var e=!1,n=/a/;return n.exec=function(){return e=!0,null},"split"===t&&(n.constructor={},n.constructor[Di]=function(){return n}),n[r](""),!e}));if(!u||!o||"replace"===t&&!Oi||"split"===t&&!wi){var s=/./[r],l=n(r,""[t],(function(t,e,n,i,r){return e.exec===ki?u&&!r?{done:!0,value:s.call(e,n,i)}:{done:!0,value:t.call(n,e,i)}:{done:!1}})),a=l[0],c=l[1];ut(String.prototype,t,a),ut(RegExp.prototype,r,2==e?function(t,e){return c.call(t,this,e)}:function(t){return c.call(t,this)}),i&&N(RegExp.prototype[r],"sham",!0)}},$i=Pt("species"),Bi=pi.charAt,ji=function(t,e,n){return e+(n?Bi(t,e).length:1)},Ti=function(t,e){var n=t.exec;if("function"==typeof n){var i=n.call(t,e);if("object"!=typeof i)throw TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==F(t))throw TypeError("RegExp#exec called on incompatible receiver");return ki.call(t,e)},Li=[].push,_i=Math.min,Ii=!v((function(){return!RegExp(4294967295,"y")}));xi("split",2,(function(t,e,n){var i;return i="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,n){var i=String(k(this)),r=void 0===n?4294967295:n>>>0;if(0===r)return[];if(void 0===t)return[i];if(!ee(t))return e.call(i,t,r);for(var u,o,s,l=[],a=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),c=0,h=new RegExp(t.source,a+"g");(u=ki.call(h,i))&&!((o=h.lastIndex)>c&&(l.push(i.slice(c,u.index)),u.length>1&&u.index<i.length&&Li.apply(l,u.slice(1)),s=u[0].length,c=o,l.length>=r));)h.lastIndex===u.index&&h.lastIndex++;return c===i.length?!s&&h.test("")||l.push(""):l.push(i.slice(c)),l.length>r?l.slice(0,r):l}:"0".split(void 0,0).length?function(t,n){return void 0===t&&0===n?[]:e.call(this,t,n)}:e,[function(e,n){var r=k(this),u=null==e?void 0:e[t];return void 0!==u?u.call(e,r,n):i.call(String(r),e,n)},function(t,r){var u=n(i,t,this,r,i!==e);if(u.done)return u.value;var o=R(t),s=String(this),l=function(t,e){var n,i=R(t).constructor;return void 0===i||null==(n=R(i)[$i])?e:ke(n)}(o,RegExp),a=o.unicode,c=(o.ignoreCase?"i":"")+(o.multiline?"m":"")+(o.unicode?"u":"")+(Ii?"y":"g"),h=new l(Ii?o:"^(?:"+o.source+")",c),f=void 0===r?4294967295:r>>>0;if(0===f)return[];if(0===s.length)return null===Ti(h,s)?[s]:[];for(var p=0,d=0,v=[];d<s.length;){h.lastIndex=Ii?d:0;var g,y=Ti(h,Ii?s:s.slice(d));if(null===y||(g=_i(pt(h.lastIndex+(Ii?0:d)),s.length))===p)d=ji(s,d,a);else{if(v.push(s.slice(p,d)),v.length===f)return v;for(var E=1;E<=y.length-1;E++)if(v.push(y[E]),v.length===f)return v;d=p=g}}return v.push(s.slice(p)),v}]}),!Ii);var Ri={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0},Mi=$e.forEach,Pi=Vn("forEach")?function(t){return Mi(this,t,arguments.length>1?arguments[1]:void 0)}:[].forEach;for(var Ni in Ri){var Hi=d[Ni],Gi=Hi&&Hi.prototype;if(Gi&&Gi.forEach!==Pi)try{N(Gi,"forEach",Pi)}catch(t){Gi.forEach=Pi}}var Ui=Pt("iterator"),Wi=Pt("toStringTag"),Vi=Wn.values;for(var Ki in Ri){var zi=d[Ki],qi=zi&&zi.prototype;if(qi){if(qi[Ui]!==Vi)try{N(qi,Ui,Vi)}catch(t){qi[Ui]=Vi}if(qi[Wi]||N(qi,Wi,Ki),Ri[Ki])for(var Yi in Wn)if(qi[Yi]!==Wn[Yi])try{N(qi,Yi,Wn[Yi])}catch(t){qi[Yi]=Wn[Yi]}}}var Ji=function(){function t(e){var i=this;n(this,t),this.rows=e.rows,this.scrollEl=e.scrollEl,this.contentEl=e.contentEl,this.callback=e.callback,this.cache={},this.scrollTop=this.scrollEl.scrollTop,this.initDOM(this.rows),this.scrollEl.scrollTop=this.scrollTop,this.lastCluster=0;var r=function(){i.lastCluster!==(i.lastCluster=i.getNum())&&(i.initDOM(i.rows),i.callback())};this.scrollEl.addEventListener("scroll",r,!1),this.destroy=function(){i.contentEl.innerHtml="",i.scrollEl.removeEventListener("scroll",r,!1)}}return r(t,[{key:"initDOM",value:function(t){void 0===this.clusterHeight&&(this.cache.scrollTop=this.scrollEl.scrollTop,this.cache.data=this.contentEl.innerHTML=t[0]+t[0]+t[0],this.getRowsHeight(t));var e=this.initData(t,this.getNum()),n=e.rows.join(""),i=this.checkChanges("data",n),r=this.checkChanges("top",e.topOffset),u=this.checkChanges("bottom",e.bottomOffset),o=[];i&&r?(e.topOffset&&o.push(this.getExtra("top",e.topOffset)),o.push(n),e.bottomOffset&&o.push(this.getExtra("bottom",e.bottomOffset)),this.contentEl.innerHTML=o.join("")):u&&(this.contentEl.lastChild.style.height="".concat(e.bottomOffset,"px"))}},{key:"getRowsHeight",value:function(){if(void 0===this.itemHeight){var t=this.contentEl.children,e=t[Math.floor(t.length/2)];this.itemHeight=e.offsetHeight}this.blockHeight=this.itemHeight*pe.BLOCK_ROWS,this.clusterRows=pe.BLOCK_ROWS*pe.CLUSTER_BLOCKS,this.clusterHeight=this.blockHeight*pe.CLUSTER_BLOCKS}},{key:"getNum",value:function(){return this.scrollTop=this.scrollEl.scrollTop,Math.floor(this.scrollTop/(this.clusterHeight-this.blockHeight))||0}},{key:"initData",value:function(t,e){if(t.length<pe.BLOCK_ROWS)return{topOffset:0,bottomOffset:0,rowsAbove:0,rows:t};var n=Math.max((this.clusterRows-pe.BLOCK_ROWS)*e,0),i=n+this.clusterRows,r=Math.max(n*this.itemHeight,0),u=Math.max((t.length-i)*this.itemHeight,0),o=[],s=n;r<1&&s++;for(var l=n;l<i;l++)t[l]&&o.push(t[l]);return this.dataStart=n,this.dataEnd=i,{topOffset:r,bottomOffset:u,rowsAbove:s,rows:o}}},{key:"checkChanges",value:function(t,e){var n=e!==this.cache[t];return this.cache[t]=e,n}},{key:"getExtra",value:function(t,e){var n=document.createElement("li");return n.className="virtual-scroll-".concat(t),e&&(n.style.height="".concat(e,"px")),n.outerHTML}}]),t}(),Qi=Math.max,Zi=Math.min,Xi=Math.floor,tr=/\$([$&'`]|\d\d?|<[^>]*>)/g,er=/\$([$&'`]|\d\d?)/g;xi("replace",2,(function(t,e,n){return[function(n,i){var r=k(this),u=null==n?void 0:n[t];return void 0!==u?u.call(n,r,i):e.call(String(r),n,i)},function(t,r){var u=n(e,t,this,r);if(u.done)return u.value;var o=R(t),s=String(this),l="function"==typeof r;l||(r=String(r));var a=o.global;if(a){var c=o.unicode;o.lastIndex=0}for(var h=[];;){var f=Ti(o,s);if(null===f)break;if(h.push(f),!a)break;""===String(f[0])&&(o.lastIndex=ji(s,pt(o.lastIndex),c))}for(var p,d="",v=0,g=0;g<h.length;g++){f=h[g];for(var y=String(f[0]),E=Qi(Zi(ht(f.index),s.length),0),b=[],m=1;m<f.length;m++)b.push(void 0===(p=f[m])?p:String(p));var A=f.groups;if(l){var F=[y].concat(b,E,s);void 0!==A&&F.push(A);var S=String(r.apply(void 0,F))}else S=i(y,s,E,b,A,r);E>=v&&(d+=s.slice(v,E)+S,v=E+y.length)}return d+s.slice(v)}];function i(t,n,i,r,u,o){var s=i+t.length,l=r.length,a=er;return void 0!==u&&(u=Qt(u),a=tr),e.call(o,a,(function(e,o){var a;switch(o.charAt(0)){case"$":return"$";case"&":return t;case"`":return n.slice(0,i);case"'":return n.slice(s);case"<":a=u[o.slice(1,-1)];break;default:var c=+o;if(0===c)return e;if(c>l){var h=Xi(c/10);return 0===h?e:h<=l?void 0===r[h-1]?o.charAt(1):r[h-1]+o.charAt(1):e}a=r[c-1]}return void 0===a?"":a}))}}));var nr=function(t){if(t.normalize)return t.normalize("NFD").replace(/[\u0300-\u036F]/g,"");return[{base:"A",letters:/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},{base:"AA",letters:/[\uA732]/g},{base:"AE",letters:/[\u00C6\u01FC\u01E2]/g},{base:"AO",letters:/[\uA734]/g},{base:"AU",letters:/[\uA736]/g},{base:"AV",letters:/[\uA738\uA73A]/g},{base:"AY",letters:/[\uA73C]/g},{base:"B",letters:/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},{base:"C",letters:/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},{base:"D",letters:/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},{base:"DZ",letters:/[\u01F1\u01C4]/g},{base:"Dz",letters:/[\u01F2\u01C5]/g},{base:"E",letters:/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},{base:"F",letters:/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},{base:"G",letters:/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},{base:"H",letters:/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},{base:"I",letters:/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},{base:"J",letters:/[\u004A\u24BF\uFF2A\u0134\u0248]/g},{base:"K",letters:/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},{base:"L",letters:/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},{base:"LJ",letters:/[\u01C7]/g},{base:"Lj",letters:/[\u01C8]/g},{base:"M",letters:/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},{base:"N",letters:/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},{base:"NJ",letters:/[\u01CA]/g},{base:"Nj",letters:/[\u01CB]/g},{base:"O",letters:/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},{base:"OI",letters:/[\u01A2]/g},{base:"OO",letters:/[\uA74E]/g},{base:"OU",letters:/[\u0222]/g},{base:"P",letters:/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},{base:"Q",letters:/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},{base:"R",letters:/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},{base:"S",letters:/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},{base:"T",letters:/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},{base:"TZ",letters:/[\uA728]/g},{base:"U",letters:/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},{base:"V",letters:/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},{base:"VY",letters:/[\uA760]/g},{base:"W",letters:/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},{base:"X",letters:/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},{base:"Y",letters:/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},{base:"Z",letters:/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},{base:"a",letters:/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},{base:"aa",letters:/[\uA733]/g},{base:"ae",letters:/[\u00E6\u01FD\u01E3]/g},{base:"ao",letters:/[\uA735]/g},{base:"au",letters:/[\uA737]/g},{base:"av",letters:/[\uA739\uA73B]/g},{base:"ay",letters:/[\uA73D]/g},{base:"b",letters:/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},{base:"c",letters:/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},{base:"d",letters:/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},{base:"dz",letters:/[\u01F3\u01C6]/g},{base:"e",letters:/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},{base:"f",letters:/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},{base:"g",letters:/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},{base:"h",letters:/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},{base:"hv",letters:/[\u0195]/g},{base:"i",letters:/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},{base:"j",letters:/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},{base:"k",letters:/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},{base:"l",letters:/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},{base:"lj",letters:/[\u01C9]/g},{base:"m",letters:/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},{base:"n",letters:/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},{base:"nj",letters:/[\u01CC]/g},{base:"o",letters:/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},{base:"oi",letters:/[\u01A3]/g},{base:"ou",letters:/[\u0223]/g},{base:"oo",letters:/[\uA74F]/g},{base:"p",letters:/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},{base:"q",letters:/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},{base:"r",letters:/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},{base:"s",letters:/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},{base:"t",letters:/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},{base:"tz",letters:/[\uA729]/g},{base:"u",letters:/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},{base:"v",letters:/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},{base:"vy",letters:/[\uA761]/g},{base:"w",letters:/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},{base:"x",letters:/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},{base:"y",letters:/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},{base:"z",letters:/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}].reduce((function(t,e){var n=e.letters,i=e.base;return t.replace(n,i)}),t)},ir=function(t,e,n){var i=!0,r=!1,u=void 0;try{for(var o,s=t[Symbol.iterator]();!(i=(o=s.next()).done);i=!0){var l=o.value;if(l[e]===n||l[e]===+l[e]+""&&+l[e]===n)return l;if("optgroup"===l.type){var a=!0,c=!1,h=void 0;try{for(var f,p=l.children[Symbol.iterator]();!(a=(f=p.next()).done);a=!0){var d=f.value;if(d[e]===n||d[e]===+d[e]+""&&+d[e]===n)return d}}catch(t){c=!0,h=t}finally{try{a||null==p.return||p.return()}finally{if(c)throw h}}}}}catch(t){r=!0,u=t}finally{try{i||null==s.return||s.return()}finally{if(r)throw u}}},rr=function(t){return Object.keys(t).forEach((function(e){return void 0===t[e]?delete t[e]:""})),t},ur=function(){function i(e,r){n(this,i),this.$el=e,this.options=t.extend({},pe.DEFAULTS,r)}return r(i,[{key:"init",value:function(){this.initLocale(),this.initContainer(),this.initData(),this.initSelected(!0),this.initFilter(),this.initDrop(),this.initView(),this.options.onAfterCreate()}},{key:"initLocale",value:function(){if(this.options.locale){var e=t.fn.multipleSelect.locales,n=this.options.locale.split(/-|_/);n[0]=n[0].toLowerCase(),n[1]&&(n[1]=n[1].toUpperCase()),e[this.options.locale]?t.extend(this.options,e[this.options.locale]):e[n.join("-")]?t.extend(this.options,e[n.join("-")]):e[n[0]]&&t.extend(this.options,e[n[0]])}}},{key:"initContainer",value:function(){var e=this,n=this.$el[0],i=n.getAttribute("name")||this.options.name||"";this.$el.hide(),this.$label=this.$el.closest("label"),!this.$label.length&&this.$el.attr("id")&&(this.$label=t('label[for="'.concat(this.$el.attr("id"),'"]'))),this.$label.find(">input").length&&(this.$label=null),void 0===this.options.single&&(this.options.single=null===n.getAttribute("multiple")),this.$parent=t('\n      <div class="ms-parent '.concat(n.getAttribute("class")||"",'"\n      title="').concat(n.getAttribute("title")||"",'" />\n    ')),this.options.placeholder=this.options.placeholder||n.getAttribute("placeholder")||"",this.tabIndex=n.getAttribute("tabindex");var r="";if(null!==this.tabIndex&&(this.$el.attr("tabindex",-1),r=this.tabIndex&&'tabindex="'.concat(this.tabIndex,'"')),this.$choice=t('\n      <button type="button" class="ms-choice"'.concat(r,'>\n      <span class="placeholder">').concat(this.options.placeholder,"</span>\n      ").concat(this.options.showClear?'<div class="icon-close"></div>':"",'\n      <div class="icon-caret"></div>\n      </button>\n    ')),this.$drop=t('<div class="ms-drop '.concat(this.options.position,'" />')),this.$close=this.$choice.find(".icon-close"),this.options.dropWidth&&this.$drop.css("width",this.options.dropWidth),this.$el.after(this.$parent),this.$parent.append(this.$choice),this.$parent.append(this.$drop),n.disabled&&this.$choice.addClass("disabled"),this.selectAllName='data-name="selectAll'.concat(i,'"'),this.selectGroupName='data-name="selectGroup'.concat(i,'"'),this.selectItemName='data-name="selectItem'.concat(i,'"'),!this.options.keepOpen){var u=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return t=t||"".concat(+new Date).concat(~~(1e6*Math.random())),"click.multiple-select-".concat(t)}(this.$el.attr("id"));t(document).off(u).on(u,(function(i){t(i.target)[0]!==e.$choice[0]&&t(i.target).parents(".ms-choice")[0]!==e.$choice[0]&&(t(i.target)[0]===e.$drop[0]||t(i.target).parents(".ms-drop")[0]!==e.$drop[0]&&i.target!==n)&&e.options.isOpen&&e.close()}))}}},{key:"initData",value:function(){var n=this,i=[];if(this.options.data){if(Array.isArray(this.options.data))this.data=this.options.data.map((function(t){return"string"==typeof t||"number"==typeof t?{text:t,value:t}:t}));else if("object"===e(this.options.data)){for(var r=0,o=Object.entries(this.options.data);r<o.length;r++){var s=u(o[r],2),l=s[0],a=s[1];i.push({value:l,text:a})}this.data=i}}else t.each(this.$el.children(),(function(t,e){n.initRow(t,e)&&i.push(n.initRow(t,e))})),this.options.data=i,this.data=i,this.fromHtml=!0;this.dataTotal=function(t){var e=0;return t.forEach((function(t,n){"optgroup"===t.type?(t._key="group_".concat(n),t.visible=void 0===t.visible||t.visible,t.children.forEach((function(t,e){t._key="option_".concat(n,"_").concat(e),t.visible=void 0===t.visible||t.visible})),e+=t.children.length):(t._key="option_".concat(n),t.visible=void 0===t.visible||t.visible,e+=1)})),e}(this.data)}},{key:"initRow",value:function(e,n,i){var r=this,u={},o=t(n);return o.is("option")?(u.type="option",u.text=this.options.textTemplate(o),u.value=n.value,u.visible=!0,u.selected=!!n.selected,u.disabled=i||n.disabled,u.classes=n.getAttribute("class")||"",u.title=n.getAttribute("title")||"",o.data("value")&&(u._value=o.data("value")),Object.keys(o.data()).length&&(u._data=o.data()),u):o.is("optgroup")?(u.type="optgroup",u.label=this.options.labelTemplate(o),u.visible=!0,u.selected=!!n.selected,u.disabled=n.disabled,u.children=[],Object.keys(o.data()).length&&(u._data=o.data()),t.each(o.children(),(function(t,e){u.children.push(r.initRow(t,e,u.disabled))})),u):null}},{key:"initSelected",value:function(t){var e=0,n=!0,i=!1,r=void 0;try{for(var u,o=this.data[Symbol.iterator]();!(n=(u=o.next()).done);n=!0){var s=u.value;if("optgroup"===s.type){var l=s.children.filter((function(t){return t.selected&&!t.disabled&&t.visible})).length;s.selected=l&&l===s.children.filter((function(t){return!t.disabled&&t.visible})).length,e+=l}else e+=s.selected&&!s.disabled&&s.visible?1:0}}catch(t){i=!0,r=t}finally{try{n||null==o.return||o.return()}finally{if(i)throw r}}this.allSelected=this.data.filter((function(t){return t.selected&&!t.disabled&&t.visible})).length===this.data.filter((function(t){return!t.disabled&&t.visible})).length,t||(this.allSelected?this.options.onCheckAll():0===e&&this.options.onUncheckAll())}},{key:"initFilter",value:function(){if(this.filterText="",!this.options.filter&&this.options.filterByDataLength){var t=0,e=!0,n=!1,i=void 0;try{for(var r,u=this.data[Symbol.iterator]();!(e=(r=u.next()).done);e=!0){var o=r.value;"optgroup"===o.type?t+=o.children.length:t+=1}}catch(t){n=!0,i=t}finally{try{e||null==u.return||u.return()}finally{if(n)throw i}}this.options.filter=t>this.options.filterByDataLength}}},{key:"initDrop",value:function(){var t=this;this.initList(),this.update(!0),this.options.isOpen&&setTimeout((function(){t.open()}),50),this.options.openOnHover&&this.$parent.hover((function(){t.open()}),(function(){t.close()}))}},{key:"initList",value:function(){var t=[];this.options.filter&&t.push('\n        <div class="ms-search">\n          <input type="text" autocomplete="off" autocorrect="off"\n            autocapitalize="off" spellcheck="false"\n            placeholder="'.concat(this.options.filterPlaceholder,'">\n        </div>\n      ')),t.push("<ul></ul>"),this.$drop.html(t.join("")),this.$ul=this.$drop.find(">ul"),this.initListItems()}},{key:"initListItems",value:function(){var t=this,e=this.getListRows(),n=0;if(this.options.selectAll&&!this.options.single&&(n=-1),e.length>pe.BLOCK_ROWS*pe.CLUSTER_BLOCKS){this.virtualScroll&&this.virtualScroll.destroy();var i=this.$drop.is(":visible");i||this.$drop.css("left",-1e4).show();var r=function(){t.updateDataStart=t.virtualScroll.dataStart+n,t.updateDataEnd=t.virtualScroll.dataEnd+n,t.updateDataStart<0&&(t.updateDataStart=0),t.updateDataEnd>t.data.length&&(t.updateDataEnd=t.data.length)};this.virtualScroll=new Ji({rows:e,scrollEl:this.$ul[0],contentEl:this.$ul[0],callback:function(){r(),t.events()}}),r(),i||this.$drop.css("left",0).hide()}else this.$ul.html(e.join("")),this.updateDataStart=0,this.updateDataEnd=this.updateData.length,this.virtualScroll=null;this.events()}},{key:"getListRows",value:function(){var t=this,e=[];return this.options.selectAll&&!this.options.single&&e.push('\n        <li class="ms-select-all">\n        <label>\n        <input type="checkbox" '.concat(this.selectAllName).concat(this.allSelected?' checked="checked"':""," />\n        <span>").concat(this.options.formatSelectAll(),"</span>\n        </label>\n        </li>\n      ")),this.updateData=[],this.data.forEach((function(n){e.push.apply(e,o(t.initListItem(n)))})),e.push('<li class="ms-no-results">'.concat(this.options.formatNoMatchesFound(),"</li>")),e}},{key:"initListItem",value:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,i=t.title?'title="'.concat(t.title,'"'):"",r=this.options.multiple?"multiple":"",u=this.options.single?"radio":"checkbox",s="";if(!t.visible)return[];if(this.updateData.push(t),this.options.single&&!this.options.singleRadio&&(s="hide-radio "),t.selected&&(s+="selected "),"optgroup"===t.type){var l=this.options.styler(t),a=l?'style="'.concat(l,'"'):"",c=[],h=this.options.hideOptgroupCheckboxes||this.options.single?"<span ".concat(this.selectGroupName,' data-key="').concat(t._key,'"></span>'):'<input type="checkbox"\n          '.concat(this.selectGroupName,'\n          data-key="').concat(t._key,'"\n          ').concat(t.selected?' checked="checked"':"","\n          ").concat(t.disabled?' disabled="disabled"':"","\n        >");return s.includes("hide-radio")||!this.options.hideOptgroupCheckboxes&&!this.options.single||(s+="hide-radio "),c.push('\n        <li class="group '.concat(s,'" ').concat(a,'>\n        <label class="optgroup').concat(this.options.single||t.disabled?" disabled":"",'">\n        ').concat(h).concat(t.label,"\n        </label>\n        </li>\n      ")),t.children.forEach((function(t){c.push.apply(c,o(e.initListItem(t,1)))})),c}var f=this.options.styler(t),p=f?'style="'.concat(f,'"'):"";return s+=t.classes||"",n&&this.options.single&&(s+="option-level-".concat(n," ")),['\n      <li class="'.concat(r," ").concat(s,'" ').concat(i," ").concat(p,'>\n      <label class="').concat(t.disabled?"disabled":"",'">\n      <input type="').concat(u,'"\n        value="').concat(t.value,'"\n        data-key="').concat(t._key,'"\n        ').concat(this.selectItemName,"\n        ").concat(t.selected?' checked="checked"':"","\n        ").concat(t.disabled?' disabled="disabled"':"","\n      >\n      <span>").concat(t.text,"</span>\n      </label>\n      </li>\n    ")]}},{key:"events",value:function(){var e=this;this.$searchInput=this.$drop.find(".ms-search input"),this.$selectAll=this.$drop.find("input[".concat(this.selectAllName,"]")),this.$selectGroups=this.$drop.find("input[".concat(this.selectGroupName,"],span[").concat(this.selectGroupName,"]")),this.$selectItems=this.$drop.find("input[".concat(this.selectItemName,"]:enabled")),this.$disableItems=this.$drop.find("input[".concat(this.selectItemName,"]:disabled")),this.$noResults=this.$drop.find(".ms-no-results");var n=function(n){n.preventDefault(),t(n.target).hasClass("icon-close")||e[e.options.isOpen?"close":"open"]()};this.$label&&this.$label.length&&this.$label.off("click").on("click",(function(t){"label"===t.target.nodeName.toLowerCase()&&(n(t),e.options.filter&&e.options.isOpen||e.focus(),t.stopPropagation())})),this.$choice.off("click").on("click",n).off("focus").on("focus",this.options.onFocus).off("blur").on("blur",this.options.onBlur),this.$parent.off("keydown").on("keydown",(function(t){27!==t.which||e.options.keepOpen||(e.close(),e.$choice.focus())})),this.$close.off("click").on("click",(function(t){t.preventDefault(),e._checkAll(!1,!0),e.initSelected(!1),e.updateSelected(),e.update(),e.options.onClear()})),this.$searchInput.off("keydown").on("keydown",(function(t){9===t.keyCode&&t.shiftKey&&e.close()})).off("keyup").on("keyup",(function(t){if(e.options.filterAcceptOnEnter&&[13,32].includes(t.which)&&e.$searchInput.val()){if(e.options.single){var n=e.$selectItems.closest("li").filter(":visible");n.length&&e.setSelects([n.first().find("input[".concat(e.selectItemName,"]")).val()])}else e.$selectAll.click();return e.close(),void e.focus()}e.filter()})),this.$selectAll.off("click").on("click",(function(n){e._checkAll(t(n.currentTarget).prop("checked"))})),this.$selectGroups.off("click").on("click",(function(n){var i=t(n.currentTarget),r=i.prop("checked"),u=ir(e.data,"_key",i.data("key"));e._checkGroup(u,r),e.options.onOptgroupClick(rr({label:u.label,selected:u.selected,data:u._data,children:u.children.map((function(t){return rr({text:t.text,value:t.value,selected:t.selected,disabled:t.disabled,data:t._data})}))}))})),this.$selectItems.off("click").on("click",(function(n){var i=t(n.currentTarget),r=i.prop("checked"),u=ir(e.data,"_key",i.data("key"));e._check(u,r),e.options.onClick(rr({text:u.text,value:u.value,selected:u.selected,data:u._data})),e.options.single&&e.options.isOpen&&!e.options.keepOpen&&e.close()}))}},{key:"initView",value:function(){var t;window.getComputedStyle?"auto"===(t=window.getComputedStyle(this.$el[0]).width)&&(t=this.$drop.outerWidth()+20):t=this.$el.outerWidth()+20,this.$parent.css("width",this.options.width||t),this.$el.show().addClass("ms-offscreen")}},{key:"open",value:function(){if(!this.$choice.hasClass("disabled")){if(this.options.isOpen=!0,this.$choice.find(">div").addClass("open"),this.$drop[this.animateMethod("show")](),this.$selectAll.parent().show(),this.$noResults.hide(),this.data.length||(this.$selectAll.parent().hide(),this.$noResults.show()),this.options.container){var e=this.$drop.offset();this.$drop.appendTo(t(this.options.container)),this.$drop.offset({top:e.top,left:e.left}).css("min-width","auto").outerWidth(this.$parent.outerWidth())}var n=this.options.maxHeight;"row"===this.options.maxHeightUnit&&(n=this.$drop.find(">ul>li").first().outerHeight()*this.options.maxHeight),this.$drop.find(">ul").css("max-height","".concat(n,"px")),this.$drop.find(".multiple").css("width","".concat(this.options.multipleWidth,"px")),this.data.length&&this.options.filter&&(this.$searchInput.val(""),this.$searchInput.focus(),this.filter(!0)),this.options.onOpen()}}},{key:"close",value:function(){this.options.isOpen=!1,this.$choice.find(">div").removeClass("open"),this.$drop[this.animateMethod("hide")](),this.options.container&&(this.$parent.append(this.$drop),this.$drop.css({top:"auto",left:"auto"})),this.options.onClose()}},{key:"animateMethod",value:function(t){return{show:{fade:"fadeIn",slide:"slideDown"},hide:{fade:"fadeOut",slide:"slideUp"}}[t][this.options.animate]||t}},{key:"update",value:function(t){var e=this.getSelects(),n=this.getSelects("text");this.options.displayValues&&(n=e);var i=this.$choice.find(">span"),r=e.length,u="";0===r?i.addClass("placeholder").html(this.options.placeholder):u=r<this.options.minimumCountSelected?n.join(this.options.displayDelimiter):this.options.formatAllSelected()&&r===this.dataTotal?this.options.formatAllSelected():this.options.ellipsis&&r>this.options.minimumCountSelected?"".concat(n.slice(0,this.options.minimumCountSelected).join(this.options.displayDelimiter),"..."):this.options.formatCountSelected()&&r>this.options.minimumCountSelected?this.options.formatCountSelected(r,this.dataTotal):n.join(this.options.displayDelimiter),u&&i.removeClass("placeholder").html(u),this.options.displayTitle&&i.prop("title",this.getSelects("text")),this.$el.val(this.getSelects()),t||this.$el.trigger("change")}},{key:"updateSelected",value:function(){for(var t=this.updateDataStart;t<this.updateDataEnd;t++){var e=this.updateData[t];this.$drop.find("input[data-key=".concat(e._key,"]")).prop("checked",e.selected).closest("li").toggleClass("selected",e.selected)}var n=0===this.data.filter((function(t){return t.visible})).length;this.$selectAll.length&&this.$selectAll.prop("checked",this.allSelected).closest("li").toggle(!n),this.$noResults.toggle(n),this.virtualScroll&&(this.virtualScroll.rows=this.getListRows())}},{key:"getOptions",value:function(){var e=t.extend({},this.options);return delete e.data,t.extend(!0,{},e)}},{key:"refreshOptions",value:function(e){(function(t,e,n){var i=Object.keys(t),r=Object.keys(e);if(n&&i.length!==r.length)return!1;for(var u=0,o=i;u<o.length;u++){var s=o[u];if(r.includes(s)&&t[s]!==e[s])return!1}return!0})(this.options,e,!0)||(this.options=t.extend(this.options,e),this.destroy(),this.init())}},{key:"getSelects",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"value",e=[],n=!0,i=!1,r=void 0;try{for(var u,s=this.data[Symbol.iterator]();!(n=(u=s.next()).done);n=!0){var l=u.value;if("optgroup"===l.type){var a=l.children.filter((function(t){return t.selected}));if(!a.length)continue;if("value"===t||this.options.single)e.push.apply(e,o(a.map((function(e){return"value"===t&&e._value||e[t]}))));else{var c=[];c.push("["),c.push(l.label),c.push(": ".concat(a.map((function(e){return e[t]})).join(", "))),c.push("]"),e.push(c.join(""))}}else l.selected&&e.push("value"===t&&l._value||l[t])}}catch(t){i=!0,r=t}finally{try{n||null==s.return||s.return()}finally{if(i)throw r}}return e}},{key:"setSelects",value:function(t,e){var n=!1,i=function(e){var i=!0,r=!1,u=void 0;try{for(var o,s=e[Symbol.iterator]();!(i=(o=s.next()).done);i=!0){var l=o.value,a=t.includes(l._value||l.value);a||l.value!==+l.value+""||(a=t.includes(+l.value)),l.selected!==a&&(n=!0),l.selected=a}}catch(t){r=!0,u=t}finally{try{i||null==s.return||s.return()}finally{if(r)throw u}}},r=!0,u=!1,o=void 0;try{for(var s,l=this.data[Symbol.iterator]();!(r=(s=l.next()).done);r=!0){var a=s.value;"optgroup"===a.type?i(a.children):i([a])}}catch(t){u=!0,o=t}finally{try{r||null==l.return||l.return()}finally{if(u)throw o}}n&&(this.initSelected(e),this.updateSelected(),this.update(e))}},{key:"enable",value:function(){this.$choice.removeClass("disabled")}},{key:"disable",value:function(){this.$choice.addClass("disabled")}},{key:"check",value:function(t){var e=ir(this.data,"value",t);e&&this._check(e,!0)}},{key:"uncheck",value:function(t){var e=ir(this.data,"value",t);e&&this._check(e,!1)}},{key:"_check",value:function(t,e){this.options.single&&this._checkAll(!1,!0),t.selected=e,this.initSelected(),this.updateSelected(),this.update()}},{key:"checkAll",value:function(){this._checkAll(!0)}},{key:"uncheckAll",value:function(){this._checkAll(!1)}},{key:"_checkAll",value:function(t,e){var n=!0,i=!1,r=void 0;try{for(var u,o=this.data[Symbol.iterator]();!(n=(u=o.next()).done);n=!0){var s=u.value;"optgroup"===s.type?this._checkGroup(s,t,!0):s.disabled||!e&&!s.visible||(s.selected=t)}}catch(t){i=!0,r=t}finally{try{n||null==o.return||o.return()}finally{if(i)throw r}}e||(this.initSelected(),this.updateSelected(),this.update())}},{key:"_checkGroup",value:function(t,e,n){t.selected=e,t.children.forEach((function(t){t.disabled||!n&&!t.visible||(t.selected=e)})),n||(this.initSelected(),this.updateSelected(),this.update())}},{key:"checkInvert",value:function(){if(!this.options.single){var t=!0,e=!1,n=void 0;try{for(var i,r=this.data[Symbol.iterator]();!(t=(i=r.next()).done);t=!0){var u=i.value;if("optgroup"===u.type){var o=!0,s=!1,l=void 0;try{for(var a,c=u.children[Symbol.iterator]();!(o=(a=c.next()).done);o=!0){var h=a.value;h.selected=!h.selected}}catch(t){s=!0,l=t}finally{try{o||null==c.return||c.return()}finally{if(s)throw l}}}else u.selected=!u.selected}}catch(t){e=!0,n=t}finally{try{t||null==r.return||r.return()}finally{if(e)throw n}}this.initSelected(),this.updateSelected(),this.update()}}},{key:"focus",value:function(){this.$choice.focus(),this.options.onFocus()}},{key:"blur",value:function(){this.$choice.blur(),this.options.onBlur()}},{key:"refresh",value:function(){this.destroy(),this.init()}},{key:"filter",value:function(e){var n=t.trim(this.$searchInput.val()),i=n.toLowerCase();if(this.filterText!==i){this.filterText=i;var r=!0,u=!1,o=void 0;try{for(var s,l=this.data[Symbol.iterator]();!(r=(s=l.next()).done);r=!0){var a=s.value;if("optgroup"===a.type)if(this.options.filterGroup){var c=this.options.customFilter(nr(a.label.toLowerCase()),nr(i),a.label,n);a.visible=c;var h=!0,f=!1,p=void 0;try{for(var d,v=a.children[Symbol.iterator]();!(h=(d=v.next()).done);h=!0){d.value.visible=c}}catch(t){f=!0,p=t}finally{try{h||null==v.return||v.return()}finally{if(f)throw p}}}else{var g=!0,y=!1,E=void 0;try{for(var b,m=a.children[Symbol.iterator]();!(g=(b=m.next()).done);g=!0){var A=b.value;A.visible=this.options.customFilter(nr(A.text.toLowerCase()),nr(i),A.text,n)}}catch(t){y=!0,E=t}finally{try{g||null==m.return||m.return()}finally{if(y)throw E}}a.visible=a.children.filter((function(t){return t.visible})).length>0}else a.visible=this.options.customFilter(nr(a.text.toLowerCase()),nr(i),a.text,n)}}catch(t){u=!0,o=t}finally{try{r||null==l.return||l.return()}finally{if(u)throw o}}this.initListItems(),this.initSelected(e),this.updateSelected(),e||this.options.onFilter(i)}}},{key:"destroy",value:function(){this.$parent&&(this.$el.before(this.$parent).removeClass("ms-offscreen"),null!==this.tabIndex&&this.$el.attr("tabindex",this.tabIndex),this.$parent.remove(),this.fromHtml&&(delete this.options.data,this.fromHtml=!1))}}]),i}();t.fn.multipleSelect=function(n){for(var i=arguments.length,r=new Array(i>1?i-1:0),u=1;u<i;u++)r[u-1]=arguments[u];var o;return this.each((function(i,u){var s=t(u),l=s.data("multipleSelect"),a=t.extend({},s.data(),"object"===e(n)&&n);if(l||(l=new ur(s,a),s.data("multipleSelect",l)),"string"==typeof n){var c;if(t.inArray(n,pe.METHODS)<0)throw new Error("Unknown method: ".concat(n));o=(c=l)[n].apply(c,r),"destroy"===n&&s.removeData("multipleSelect")}else l.init()})),void 0!==o?o:this},t.fn.multipleSelect.defaults=pe.DEFAULTS,t.fn.multipleSelect.locales=pe.LOCALES,t.fn.multipleSelect.methods=pe.METHODS}));
diff --git a/common/static/opensearch.xml b/common/static/opensearch.xml
new file mode 100644
index 00000000..03ea8902
--- /dev/null
+++ b/common/static/opensearch.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+<ShortName>NeoDB</ShortName>
+<Description>输入关键字或站外条目链接,搜索NeoDB书影音游戏</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image type="image/jpeg" width="64" height="64">https://neodb.social/static/img/logo-square.jpg</Image>
+<Url type="text/html" template="https://neodb.social/search/?q={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/common/static/sass/_AsideSection.sass b/common/static/sass/_AsideSection.sass
index 1ae9ee88..35ff5b0f 100644
--- a/common/static/sass/_AsideSection.sass
+++ b/common/static/sass/_AsideSection.sass
@@ -236,7 +236,7 @@ $panel-padding : 0
             background-color: $color-quaternary
             border-radius: 0
             height: 10px
-            width: 65%
+            width: 54%
 
         progress::-webkit-progress-bar
             background-color: $color-quaternary
diff --git a/common/static/sass/_Label.sass b/common/static/sass/_Label.sass
index cdea7e25..ea0a28b1 100644
--- a/common/static/sass/_Label.sass
+++ b/common/static/sass/_Label.sass
@@ -7,10 +7,18 @@ $spotify-color-primary: #1ed760
 $spotify-color-secondary: black
 $imdb-color-primary: #F5C518
 $imdb-color-secondary: #121212
+$igdb-color-primary: #323A44
+$igdb-color-secondary: #DFE1E2
 $steam-color-primary: #1387b8
 $steam-color-secondary: #111d2e
 $bangumi-color-primary: #F09199
 $bangumi-color-secondary: #FCFCFC
+$goodreads-color-primary: #372213
+$goodreads-color-secondary: #F4F1EA
+$tmdb-color-primary: #91CCA3
+$tmdb-color-secondary: #1FB4E2
+$bandcamp-color-primary: #28A0C1
+$bandcamp-color-secondary: white
 
 .source-label
     display: inline
@@ -50,6 +58,11 @@ $bangumi-color-secondary: #FCFCFC
         color: $imdb-color-secondary
         border: none
         font-weight: bold
+    &.source-label__igdb
+        background-color: $igdb-color-primary
+        color: $igdb-color-secondary
+        border: none
+        font-weight: bold
     &.source-label__steam
         background: linear-gradient(30deg, $steam-color-primary, $steam-color-secondary)
         color: white
@@ -60,4 +73,27 @@ $bangumi-color-secondary: #FCFCFC
         background: $bangumi-color-secondary
         color: $bangumi-color-primary
         font-style: italic
-        font-weight: 600
\ No newline at end of file
+        font-weight: 600
+    &.source-label__goodreads
+        background: $goodreads-color-secondary
+        color: $goodreads-color-primary
+        font-weight: lighter
+    &.source-label__tmdb
+        background: linear-gradient(90deg, $tmdb-color-primary, $tmdb-color-secondary)
+        color: white
+        border: none
+        font-weight: lighter
+        padding-top: 2px
+    &.source-label__googlebooks
+        color: white
+        background-color: #4285F4
+        border-color: #4285F4
+    &.source-label__bandcamp
+        color: $bandcamp-color-secondary
+        background-color: $bandcamp-color-primary
+        // transform: skewX(-30deg)
+        display: inline-block
+    &.source-label__bandcamp span
+        // transform: skewX(30deg)
+        display: inline-block
+        margin: 0 4px
diff --git a/common/static/sass/_Modal.sass b/common/static/sass/_Modal.sass
index df146ca3..dda313d5 100644
--- a/common/static/sass/_Modal.sass
+++ b/common/static/sass/_Modal.sass
@@ -115,10 +115,12 @@
             &__content
                 word-break: break-all
 
+.add-to-list-modal
+    @include modal
 
 // Small devices (landscape phones, 576px and up)
 @media (max-width: $small-devices)
-    .mark-modal, .confirm-modal, .announcement-modal
+    .mark-modal, .confirm-modal, .announcement-modal .add-to-list-modal
         width: 100%
 // Medium devices (tablets, 768px and up)
 @media (max-width: $medium-devices)
diff --git a/common/static/sass/_Vendor.sass b/common/static/sass/_Vendor.sass
index b68099e6..bffa2a2f 100644
--- a/common/static/sass/_Vendor.sass
+++ b/common/static/sass/_Vendor.sass
@@ -50,4 +50,7 @@
 .tippy-content
 
 .tag-input input
-    flex-grow: 1
\ No newline at end of file
+    flex-grow: 1
+
+.tools-section-wrapper input, .tools-section-wrapper select
+    width: unset
diff --git a/common/templates/common/error.html b/common/templates/common/error.html
index e2c1b70c..f4290972 100644
--- a/common/templates/common/error.html
+++ b/common/templates/common/error.html
@@ -5,9 +5,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta http-equiv="refresh" content="3;url={% url 'common:home' %}">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
+    <meta http-equiv="refresh" content="5;url={% if url %}{{url}}{% else %}{% url 'common:home' %}{% endif %}">
+    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
     <title>{% trans '错误' %}</title>
diff --git a/common/templates/common/external_search_result.html b/common/templates/common/external_search_result.html
new file mode 100644
index 00000000..9d2132bb
--- /dev/null
+++ b/common/templates/common/external_search_result.html
@@ -0,0 +1,48 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+{% for item in external_items %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{{ item.link }}">
+            <img src="{{ item.cover_url }}" alt="" class="entity-list__entity-img">
+        </a>
+    </div>
+    <div class="entity-list__entity-text">
+        <div class="entity-list__entity-title" style="font-style:italic;">
+            <a href="{{ item.link }}" class="entity-list__entity-link">
+                {% if request.GET.q %}
+                    {{ item.title | highlight:request.GET.q }}
+                {% else %}
+                    {{ item.title }}
+                {% endif %}
+            </a>
+            
+            {% if not request.GET.c or not request.GET.c in categories %}
+            <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ item.source_url }}">
+                <span class="source-label source-label__{{ item.source_site }}">{{ item.source_site.label }}</span>
+            </a>
+        </div>
+
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+            {{item.subtitle}}
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ item.brief }}
+        </p>
+        <div class="tag-collection">
+        </div>
+    </div>
+
+</li>
+{% endfor %}
\ No newline at end of file
diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html
index cc420eed..0144093d 100644
--- a/common/templates/common/search_result.html
+++ b/common/templates/common/search_result.html
@@ -14,12 +14,14 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 搜索结果' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '搜索结果' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -43,399 +45,39 @@
                                     
                                 <ul class="entity-list__entities">
                                     {% for item in items %}
-                                    
-                                    {% if item.category_name|lower == 'book' %}
-                                    
-                                        {% with book=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                <a href="{% url 'books:retrieve' book.id %}">
-                                                    <img src="{{ book.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                </a>
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                        
-                                                <div class="entity-list__entity-title">
-                                        
-                                                    <a href="{% url 'books:retrieve' book.id %}" class="entity-list__entity-link">
-                                                        {% if request.GET.q %}
-                                                        {{ book.title | highlight:request.GET.q }}
-                                                        {% else %}
-                                                        {{ book.title }}
-                                                        {% endif %}
-                                                        
-                                                    </a>
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ book.source_url }}">
-                                                        <span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-                                        
-                                                {% if book.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ book.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-                                        
-                                                <span class="entity-list__entity-info">
-                                                    {% if book.pub_year %}
-                                                    {{ book.pub_year }}{% trans '年' %}
-                                                    {% if book.pub_month %}
-                                                    {{book.pub_month }}{% trans '月' %} /
-                                                    {% endif %}
-                                                    {% endif %}
-                                        
-                                                    {% if book.author %}
-                                                    {% trans '作者' %}
-                                                    {% for author in book.author %}
-                                                    {{ author }}{% if not forloop.last %},{% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                    {% if book.translator %}
-                                                    {% trans '译者' %}
-                                                    {% for translator in book.translator %}
-                                                    {{ translator }}{% if not forloop.last %},{% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                    {% if book.orig_title %}
-                                                    &nbsp;{% trans '原名' %}
-                                                    {{ book.orig_title }}
-                                                    {% endif %}
-                                                </span>
-                                                <p class="entity-list__entity-brief">
-                                                    {{ book.brief }}
-                                                </p>
-                                        
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in book.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        </li>
-                                        {% endwith %}   
-
-                                    {% elif item.category_name|lower == 'movie' %}    
-                                        
-                                        {% with movie=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                <a href="{% url 'movies:retrieve' movie.id %}">
-                                                    <img src="{{ movie.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                </a>
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                                <div class="entity-list__entity-title">
-                                                    <a href="{% url 'movies:retrieve' movie.id %}" class="entity-list__entity-link">
-                                                        {% if movie.season %}
-                                                        
-                                                            {% if request.GET.q %}
-                                                                {{ movie.title | highlight:request.GET.q }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
-                                                                {{ movie.orig_title | highlight:request.GET.q }} Season {{ movie.season }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% else %}
-                                                                {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
-                                                                {{ movie.orig_title }} Season {{ movie.season }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% endif %}
-                                        
-                                                        {% else %}
-                                                            {% if request.GET.q %}
-                                                                {{ movie.title | highlight:request.GET.q }} {{ movie.orig_title | highlight:request.GET.q }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% else %}
-                                                                {{ movie.title }} {{ movie.orig_title }}
-                                                                {% if movie.year %}({{ movie.year }}){% endif %}
-                                                            {% endif %}
-                                                        {% endif %}
-                                                    </a>
-                                                    
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ movie.source_url }}">
-                                                        <span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-
-                                                {% if movie.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ movie.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-
-                                                <span class="entity-list__entity-info ">
-                                        
-                                        
-                                                    {% if movie.director %}{% trans '导演' %}
-                                                    {% for director in movie.director %}
-                                                    {{ director }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                    {% if movie.genre %}{% trans '类型' %}
-                                                    {% for genre in movie.get_genre_display %}
-                                                    {{ genre }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-                                        
-                                                </span>
-                                                <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                                    {% if movie.actor %}{% trans '主演' %}
-                                                    {% for actor in movie.actor %}
-                                                    <span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
-                                                    {% if forloop.counter <= 5 %}
-                                                        {% if not forloop.counter == 5 and not forloop.last %} {% endif %}
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endif %}
-                                                </span>
-                                                <p class="entity-list__entity-brief">
-                                                    {{ movie.brief }}
-                                                </p>
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in movie.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        
-                                        </li>
-                                        {% endwith %}
-
-                                    {% elif item.category_name|lower == 'game' %}    
-                                        
-                                        {% with game=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                <a href="{% url 'games:retrieve' game.id %}">
-                                                    <img src="{{ game.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                </a>
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                                <div class="entity-list__entity-title">
-                                                    <a href="{% url 'games:retrieve' game.id %}" class="entity-list__entity-link">
-                                                        {% if request.GET.q %}
-                                                            {{ game.title | highlight:request.GET.q }}
-                                                        {% else %}
-                                                            {{ game.title }}
-                                                        {% endif %}
-                                                    </a>
-                                                    
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ game.source_url }}">
-                                                        <span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-
-                                                {% if game.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ game.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-
-                                                <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                        
-                                                    {% if game.other_title %}{% trans '别名' %}
-                                                    {% for other_title in game.other_title %}
-                                                    {{ other_title }}{% 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.genre %}{% trans '类型' %}
-                                                    {% for genre in game.genre %}
-                                                    {{ genre }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-
-                                                    {% if game.platform %}{% trans '平台' %}
-                                                    {% for platform in game.platform %}
-                                                    {{ platform }}{% if not forloop.last %} {% endif %}
-                                                    {% endfor %}/
-                                                    {% endif %}
-
-                                                </span>
-                                                <p class="entity-list__entity-brief">
-                                                    {{ game.brief }}
-                                                </p>
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in game.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        
-                                        </li>
-                                        {% endwith %}
-
-                                    {% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %}    
-                                        
-                                        {% with music=item %}
-                                        <li class="entity-list__entity">
-                                            <div class="entity-list__entity-img-wrapper">
-                                                
-                                                {% if item.category_name|lower == 'album' %}
-                                                    <a href="{% url 'music:retrieve_album' music.id %}">
-                                                        <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                    </a>
-                                                {% elif item.category_name|lower == 'song' %}
-                                                    <a href="{% url 'music:retrieve_song' music.id %}">
-                                                        <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                                    </a>
-                                                {% endif %}
-                                                    
-                                            </div>
-                                            <div class="entity-list__entity-text">
-                                                <div class="entity-list__entity-title">
-                                                    
-                                                    {% if item.category_name|lower == 'album' %}
-                                                        <a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">                                                        
-                                                            {% if request.GET.q %}
-                                                            {{ music.title | highlight:request.GET.q }}
-                                                            {% else %}
-                                                            {{ music.title }}
-                                                            {% endif %}
-                                                        </a>
-                                                    {% elif item.category_name|lower == 'song' %}
-                                                        <a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">                                                        
-                                                            {% if request.GET.q %}
-                                                            {{ music.title | highlight:request.GET.q }}
-                                                            {% else %}
-                                                            {{ music.title }}
-                                                            {% endif %}
-                                                        </a>
-                                                    {% endif %}
-                                                        
-                                                    
-                                                    {% if not request.GET.c or not request.GET.c in categories %}
-                                                    <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
-                                                    {% endif %}
-                                                    <a href="{{ music.source_url }}">
-                                                        <span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
-                                                    </a>
-                                                </div>
-
-                                                {% if music.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ music.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">{{ music.rating }}</span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %}
-
-                                                <span class="entity-list__entity-info ">
-                                                    {% if music.artist %}{% trans '艺术家' %}
-                                                    {% for artist in music.artist %}
-                                                    <span>{{ artist }}</span>
-                                                    {% if not forloop.last %} {% endif %}
-                                                    {% endfor %}
-                                                    {% endif %}
-                                                    
-                                                    {% if music.genre %}/ {% trans '流派' %}
-                                                    {{ music.genre }}
-                                                    {% endif %}
-
-                                                    {% if music.release_date %}/ {% trans '发行日期' %}
-                                                        {{ music.release_date }}
-                                                    {% endif %}
-                                                </span>
-                                                <span class="entity-list__entity-info entity-list__entity-info--full-length">
-
-                                                </span>
-                                                
-                                                {% if music.brief %}
-                                                <p class="entity-list__entity-brief">
-                                                    {{ music.brief }}
-                                                </p>
-                                                {% elif music.category_name|lower == 'album' %}
-                                                <p class="entity-list__entity-brief">
-                                                    {% trans '曲目:' %}{{ music.track_list }}
-                                                </p>
-                                                {% else %}
-                                                <!-- song -->
-                                                <p class="entity-list__entity-brief">
-                                                    {% trans '所属专辑:' %}{{ music.album }}
-                                                </p>
-                                                {% endif %}
-                                                    
-                                                <div class="tag-collection">
-                                                    {% for tag_dict in music.tag_list %}
-                                                    {% for k, v in tag_dict.items %}
-                                                    {% if k == 'content' %}
-                                                    <span class="tag-collection__tag">
-                                                        <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                    </span>
-                                                    {% endif %}
-                                                    {% endfor %}
-                                                    {% endfor %}
-                                                </div>
-                                            </div>
-                                        
-                                        </li>
-                                        {% endwith %}
-                                                                                    
-                                    {% endif %}
-                                        
-                                        
-
+                                    {% include "partial/list_item.html" %}
                                     {% empty %}
-                                    {% trans '无结果' %}
+                                    <li class="entity-list__entity">
+                                    {% trans '无站内条目匹配' %}
+                                    </li>
                                     {% endfor %}
-                                        
+                                    {% if request.GET.q and user.is_authenticated %}
+                                    <li class="entity-list__entity" hx-get="{% url 'common:external_search' %}?q={{ request.GET.q }}&c={{ request.GET.c }}&page={% if pagination.current_page %}{{ pagination.current_page }}{% else %}1{% endif %}" hx-trigger="load" hx-swap="outerHTML">
+                                    {% trans '正在实时搜索站外条目' %}
+                                    </li>
+                                    {% endif %}
                                 </ul>
                             </div>
                             <div class="pagination" >
                                 
-                                {% if items.pagination.has_prev %}
-                                    <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                    <a href="?page={{ items.previous_page_number }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                {% if pagination.has_prev %}
+                                    <a href="?page=1&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                    <a href="?page={{ pagination.previous_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
                                 {% endif %}
                                     
-                                {% for page in items.pagination.page_range %}
+                                {% for page in pagination.page_range %}
                                     
-                                    {% if page == items.pagination.current_page %}
-                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                    {% if page == pagination.current_page %}
+                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
                                     {% else %}
-                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__page-link">{{ page }}</a>
+                                    <a href="?page={{ page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__page-link">{{ page }}</a>
                                     {% endif %}
                                         
                                 {% endfor %}
                                     
-                                {% if items.pagination.has_next %}
-                                    <a href="?page={{ items.next_page_number }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                    <a href="?page={{ items.pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}" class="pagination__nav-link">&raquo;</a>
+                                {% if pagination.has_next %}
+                                    <a href="?page={{ pagination.next_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                    <a href="?page={{ pagination.last_page }}&{% if request.GET.q %}q={{ request.GET.q }}{% elif request.GET.tag %}tag={{ request.GET.tag }}{% endif %}{% if request.GET.c %}&c={{ request.GET.c }}{% endif %}" class="pagination__nav-link">&raquo;</a>
                                 {% endif %}           
                               
                             </div>            
@@ -500,7 +142,7 @@
                                         </a>
                                     {% endif %}
                                 </div>
-                                <div class="add-entity-entries__entry">
+                                <!-- div class="add-entity-entries__entry">
                                     {% if request.GET.c and request.GET.c in categories %}
                                     
                                         {% if request.GET.c|lower == 'book' %}
@@ -560,7 +202,7 @@
                                     </a>
 
                                     {% endif %}
-                                </div>
+                                </div -->
 
                             </div>
 
@@ -573,15 +215,11 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
     </script>
 </body>
 
diff --git a/common/templates/partial/_announcement.html b/common/templates/partial/_announcement.html
new file mode 100644
index 00000000..166d2856
--- /dev/null
+++ b/common/templates/partial/_announcement.html
@@ -0,0 +1,61 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<div id="modals">
+    <style>
+        .bottom-link {
+            margin-top: 30px; text-align: center; margin-bottom: 5px;
+        }
+        .bottom-link a {
+            color: #ccc;
+        }
+    </style>
+    <div class="announcement-modal modal">
+        <div class="announcement-modal__head">
+            <h4 class="announcement-modal__title">{% trans '公告' %}</h4>
+    
+            <span class="announcement-modal__close-button modal-close">
+                <span class="icon-cross">
+                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                        <polygon
+                            points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
+                        </polygon>
+                    </svg>
+                </span>
+            </span>
+        </div>
+        <div class="announcement-modal__body">
+            <ul>
+                {% for ann in unread_announcements %}
+                    <li class="announcement">
+                        <a href="{% url 'management:retrieve' ann.pk %}">
+                            <h5 class="announcement__title">{{ ann.title }}</h5>
+                        </a>
+                        <span class="announcement__datetime">{{ ann.created_time }}</span>
+                        <p class="announcement__content">{{ ann.get_plain_content | truncate:200 }}</p>
+                    </li>
+                    {% if not forloop.last %}
+                        <div class="dividing-line" style="border-top-style: dashed;"></div>
+                    {% endif %}
+                {% endfor %}
+            </ul>
+            <div class="bottom-link">
+                <a href="{% url 'management:list' %}">{% trans '查看全部公告' %}</a>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="bg-mask"></div>
+<script>
+    // because the modal and mask elements only exist when there are new announcements
+    $(".announcement-modal").show();
+    $(".bg-mask").show();
+    $(".modal-close").on('click', function () {
+        $(this).parents(".modal").hide();
+        $(".bg-mask").hide();
+    });
+</script>
diff --git a/common/templates/partial/_common_libs.html b/common/templates/partial/_common_libs.html
new file mode 100644
index 00000000..9460e2da
--- /dev/null
+++ b/common/templates/partial/_common_libs.html
@@ -0,0 +1,23 @@
+{% load static %}
+{% if sentry_dsn %}
+<script src="https://static.neodb.social/browser.sentry-cdn.com/7.7.0/bundle.min.js"></script>
+<script>
+    if (window.Sentry) Sentry.init({
+      dsn: "{{ sentry_dsn }}",
+      release: "NeoDB@{{ version_hash }}",
+      environment: "{{ settings_module }}",
+      tracesSampleRate: 1.0,
+    });
+</script>
+{% endif %}
+{% if jquery %}
+<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+{% else %}
+<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/cash/8.1.1/cash.min.js"></script>
+{% endif %}
+<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+<script src="https://static.neodb.social/unpkg.com/hyperscript.org@0.9.7.js"></script>
+<link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
+<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+<link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+<link rel="search"type="application/opensearchdescription+xml" title="{{ site_name }}" href="{% static 'opensearch.xml' %}">
diff --git a/common/templates/partial/_footer.html b/common/templates/partial/_footer.html
index e5710473..412f00ad 100644
--- a/common/templates/partial/_footer.html
+++ b/common/templates/partial/_footer.html
@@ -1,13 +1,12 @@
 <footer class="footer">
     <div class="grid">
         <div class="footer__border">
-            <a class="footer__link" target="_blank" href="https://donotban.com/@whitiewhite">作者长毛象</a>
-            <a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic/issues">报告错误</a>
+            <a class="footer__link" target="_blank" href="https://donotban.com/@whitiewhite">原作者</a>
+            <a class="footer__link" target="_blank" href="{{ support_link }}">报告错误</a>
             <a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic" id="githubLink">Github</a>
-            <a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助项目</a>
+            <a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助上游项目</a>
             <a class="footer__link" target="_blank" href="/announcement/supported-sites/" id="supported-sites">支持的网站</a>
             <a class="footer__link" target="_blank" href="/announcement/" id="supported-sites">公告栏</a>
-            <a class="footer__link" href="javascript:void();" id="version">V0.4.4</a>
         </div>
     </div>
 </footer>
\ No newline at end of file
diff --git a/common/templates/partial/_navbar.html b/common/templates/partial/_navbar.html
index 171eda8d..90d7a459 100644
--- a/common/templates/partial/_navbar.html
+++ b/common/templates/partial/_navbar.html
@@ -1,24 +1,24 @@
 {% load static %}
 {% load i18n %}
 {% load admin_url %}
+<form method="get" action="{% url 'common:search' %}">
 <section id="navbar">
     <nav class="navbar">
         <div class="grid">
             <div class="navbar__wrapper">
-
                 <a href="{% url 'common:home' %}" class="navbar__logo">
                     <img src="{% static 'img/logo.svg' %}" alt="" class="navbar__logo-img">
                 </a>
                 <div class="navbar__search-box">
                     <!-- <input type="search" class="" name="q" id="searchInput" required="true" value="{% for v in request.GET.values %}{{ v }}{% endfor %}" -->
                     <input type="search" class="" name="q" id="searchInput" required="true" value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}"
-                        placeholder="搜索书影音">
-                    <select class="navbar__search-dropdown" id="searchCategory">
-                        <option value="all" {% if request.GET.c and request.GET.c != 'movie' and request.GET.c != 'book' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
-                        <option value="book" {% if request.GET.c and request.GET.c == 'book' %}selected{% endif %}>{% trans '书籍' %}</option>
-                        <option value="movie" {% if request.GET.c and request.GET.c == 'movie' %}selected{% endif %}>{% trans '电影' %}</option>
-                        <option value="music" {% if request.GET.c and request.GET.c == 'music' %}selected{% endif %}>{% trans '音乐' %}</option>
-                        <option value="game" {% if request.GET.c and request.GET.c == 'game' %}selected{% endif %}>{% trans '游戏' %}</option>
+                        placeholder="搜索书影音游戏,或输入站外条目链接如 https://movie.douban.com/subject/1297880/ 支持站点列表见页底公告栏">
+                    <select class="navbar__search-dropdown" id="searchCategory" name="c">
+                        <option value="all" {% if request.GET.c and request.GET.c == 'all' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
+                        <option value="book" {% if request.GET.c and request.GET.c == 'book' or '/books/' in request.path %}selected{% endif %}>{% trans '书籍' %}</option>
+                        <option value="movie" {% if request.GET.c and request.GET.c == 'movie' or '/movies/' in request.path  %}selected{% endif %}>{% trans '电影' %}</option>
+                        <option value="music" {% if request.GET.c and request.GET.c == 'music' or '/music/' in request.path  %}selected{% endif %}>{% trans '音乐' %}</option>
+                        <option value="game" {% if request.GET.c and request.GET.c == 'game' or '/games/' in request.path  %}selected{% endif %}>{% trans '游戏' %}</option>
                     </select>
                 </div>
                 <button class="navbar__dropdown-btn">• • •</button>
@@ -26,8 +26,11 @@
                     
                     {% if request.user.is_authenticated %}
          
+                        <a class="navbar__link" href="{% url 'users:home' request.user.mastodon_username %}">{% trans '主页' %}</a>
+                        <a class="navbar__link" href="{% url 'timeline:timeline' %}">{% trans '动态' %}</a>
+                        <a class="navbar__link" id="logoutLink" href="{% url 'users:data' %}">{% trans '数据' %}</a>
+                        <a class="navbar__link" id="logoutLink" href="{% url 'users:preferences' %}">{% trans '设置' %}</a>
                         <a class="navbar__link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
-                        <a class="navbar__link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
                         {% if request.user.is_staff %}
                         <a class="navbar__link" href="{% admin_url %}">{% trans '后台' %}</a>
                         {% endif %}
@@ -36,23 +39,9 @@
                         <a class="navbar__link" href="{% url 'users:login' %}?next={{ request.path }}">{% trans '登录' %}</a>
                     {% endif %}
                 </ul>
-
             </div>
 
         </div>
     </nav>
-    <script>
-            $("#searchInput").on('keyup', function (e) {
-                // e.preventDefault();
-                if (e.keyCode === 13) {
-                    let q = $(this).val();
-                    let c = $("#searchCategory").val();
-                    if (q) {
-                        let new_location = "{% url 'common:search' %}" + "?c=" + c + "&q=" + q;
-                        setTimeout(function () { document.location.href = new_location; }, 150);
-                    }
-                }
-            });
-   
-    </script>
-</section>
\ No newline at end of file
+</section>
+</form>
diff --git a/common/templates/partial/_sidebar.html b/common/templates/partial/_sidebar.html
new file mode 100644
index 00000000..90bc2c0d
--- /dev/null
+++ b/common/templates/partial/_sidebar.html
@@ -0,0 +1,186 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+{% load neo %}
+<div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
+    <div class="aside-section-wrapper aside-section-wrapper--no-margin">
+        <div class="user-profile" id="userInfoCard">
+            <div class="user-profile__header">
+                <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
+                <img src="{{ user.mastodon_account.avatar }}" class="user-profile__avatar mast-avatar">
+                <a href="{% url 'users:home' user.mastodon_username %}">
+                    <h5 class="user-profile__username mast-displayname">{{ user.mastodon_account.display_name }}</h5>
+                </a>
+            </div>
+            <p><a class="user-profile__link mast-acct" target="_blank" href="{{ user.mastodon_account.url }}">@{{ user.username }}@{{ user.mastodon_site }}</a>
+                {% current_user_relationship user as relationship %}
+                {% if relationship %}
+                <a class="user-profile__report-link">
+                    {{ relationship }}
+                </a>
+                {% endif %}
+            </p>
+            <p class="user-profile__bio mast-brief">{{ user.mastodon_account.note }}</p>
+
+            {% if request.user != user %}
+            <a href="{% url 'users:report' %}?user_id={{ user.id }}"
+                class="user-profile__report-link">{% trans '投诉用户' %}</a>
+            {% endif %}
+
+        </div>
+    </div>
+
+    <div class="relation-dropdown">
+        <div class="relation-dropdown__button">
+            <span class="icon-arrow">
+                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
+                    <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
+                </svg>
+            </span>
+        </div>
+        {% if user == request.user %}
+        <div class="relation-dropdown__body">
+            <div
+                class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
+
+                <div class="user-relation" id="followings">
+                    <h5 class="user-relation__label">
+                        {% trans '关注的人' %}
+                    </h5>
+                    <a href="{% url 'users:following' user.mastodon_username %}"
+                        class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
+                    <ul class="user-relation__related-user-list mast-following">
+                        <li class="user-relation__related-user">
+                            <a>
+                                <img src="" alt="" class="user-relation__related-user-avatar">
+                                <div class="user-relation__related-user-name mast-displayname">
+                                </div>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="user-relation" id="followers">
+                    <h5 class="user-relation__label">
+                        {% trans '被他们关注' %}
+                    </h5>
+                    <a href="{% url 'users:followers' user.mastodon_username %}"
+                        class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
+                    <ul class="user-relation__related-user-list mast-followers">
+                        <li class="user-relation__related-user">
+                            <a>
+                                <img src="" alt="" class="user-relation__related-user-avatar">
+                                <div class="user-relation__related-user-name mast-displayname">
+                                </div>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="user-relation">
+                    <h5 class="user-relation__label">
+                        {% trans '常用标签' %}
+                    </h5>
+                    <a href="{% url 'users:tag_list' user.mastodon_username %}">{% trans '更多' %}</a>
+                    <div class="tag-collection" style="margin-left: 0;">
+                        {% if book_tags %}
+                        <div>{% trans '书籍' %}</div>
+                        {% for v in book_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:book_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+
+                        {% if movie_tags %}
+                        <div>{% trans '电影和剧集' %}</div>
+                        {% for v in movie_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:movie_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+
+                        {% if music_tags %}
+                        <div>{% trans '音乐' %}</div>
+                        {% for v in music_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:music_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+
+                        {% if game_tags %}
+                        <div>{% trans '游戏' %}</div>
+                        {% for v in game_tags %}
+                        <span class="tag-collection__tag">
+                            <a href="{% url 'users:game_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                        </span>
+                        {% endfor %}
+                        <div class="clearfix"></div>
+                        {% endif %}
+                    </div>
+                </div>
+
+            </div>
+           
+            <div
+                class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
+                {% if request.user.is_staff and request.user == user%}
+                <div class="report-panel">
+                    <h5 class="report-panel__label">{% trans '投诉信息' %}</h5>
+                    <a class="report-panel__all-link"
+                        href="{% url 'users:manage_report' %}">全部投诉</a>
+                    <div class="report-panel__body">
+                        <ul class="report-panel__report-list">
+                            {% for report in reports %}
+                            <li class="report-panel__report">
+                                <a href="{% url 'users:home' report.submit_user.mastodon_username %}"
+                                    class="report-panel__user-link">{{ report.submit_user }}</a>{% trans '已投诉' %}<a
+                                    href="{% url 'users:home' report.reported_user.mastodon_username %}"
+                                    class="report-panel__user-link">{{ report.reported_user }}</a>
+                            </li>
+                            {% empty %}
+                            <div>{% trans '暂无新投诉' %}</div>
+                            {% endfor %}
+
+                        </ul>
+                    </div>
+                </div>
+                {% endif %}
+            </div>
+        </div>
+        {% endif %}
+    </div>
+</div>
+
+{% if user == request.user %}
+<div id="oauth2Token" hidden="true">{{ request.user.mastodon_token }}</div>
+<div id="mastodonURI" hidden="true">{{ request.user.mastodon_site }}</div>
+<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
+<div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
+
+<div id="spinner" hidden>
+    <div class="spinner">
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+    </div>
+</div>
+{% endif %}
diff --git a/common/templates/partial/list_item.html b/common/templates/partial/list_item.html
new file mode 100644
index 00000000..9b4eb9ad
--- /dev/null
+++ b/common/templates/partial/list_item.html
@@ -0,0 +1,9 @@
+{% if item.category_name|lower == 'book' %}
+{% include "partial/list_item_book.html" with book=item %}
+{% elif item.category_name|lower == 'movie' %}    
+{% include "partial/list_item_movie.html" with movie=item %}
+{% elif item.category_name|lower == 'game' %}    
+{% include "partial/list_item_game.html" with game=item %}
+{% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %}    
+{% include "partial/list_item_music.html" with music=item %}
+{% endif %}
\ No newline at end of file
diff --git a/common/templates/partial/list_item_book.html b/common/templates/partial/list_item_book.html
new file mode 100644
index 00000000..3c5dc9eb
--- /dev/null
+++ b/common/templates/partial/list_item_book.html
@@ -0,0 +1,159 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load neo %}
+{% current_user_marked_item book as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{% url 'books:retrieve' book.id %}">
+            <img src="{{ book.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{% url 'books:wish' book.id %}" title="加入想读">➕</a>
+        {% endif %}
+    </div>
+
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+
+            <a href="{% url 'books:retrieve' book.id %}" class="entity-list__entity-link">
+                {% if request.GET.q %}
+                {{ book.title | highlight:request.GET.q }}
+                {% else %}
+                {{ book.title }}
+                {% endif %}
+
+            </a>
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{book.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ book.source_url }}">
+                <span class="source-label source-label__{{ book.source_site }}">{{ book.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if book.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ book.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info">
+            {% if book.pub_year %} /
+            {{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{book.pub_month }}{% trans '月' %}{% endif %}
+            {% endif %}
+
+            {% if book.author %} /
+            {% for author in book.author %}
+            {% if request.GET.q %}
+            {{ author | highlight:request.GET.q }}
+            {% else %}
+            {{ author }}
+            {% endif %}
+            {% if not forloop.last %},{% endif %}
+            {% endfor %}
+            {% endif %}
+
+            {% if book.translator %} /
+            {% trans '翻译' %}:
+            {% for translator in book.translator %}
+            {% if request.GET.q %}
+            {{ translator | highlight:request.GET.q }}
+            {% else %}
+            {{ translator }}
+            {% endif %}
+            {% if not forloop.last %},{% endif %}
+            {% endfor %}
+            {% endif %}
+
+            {% if book.subtitle %} /
+            {% trans '副标题' %}:
+            {% if request.GET.q %}
+            {{ book.subtitle | highlight:request.GET.q }}
+            {% else %}
+            {{ book.subtitle }}
+            {% endif %}
+            {% endif %}
+
+            {% if book.orig_title %} /
+            {% trans '原名' %}:
+            {% if request.GET.q %}
+            {{ book.orig_title | highlight:request.GET.q }}
+            {% else %}
+            {{ book.orig_title }}
+            {% endif %}
+            {% endif %}
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ book.brief }}
+        </p>
+
+        <div class="tag-collection">
+            {% for tag_dict in book.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: <a href="{% url 'books:retrieve_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/list_item_game.html b/common/templates/partial/list_item_game.html
new file mode 100644
index 00000000..42346910
--- /dev/null
+++ b/common/templates/partial/list_item_game.html
@@ -0,0 +1,139 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load neo %}
+{% current_user_marked_item game as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{% url 'games:retrieve' game.id %}">
+            <img src="{{ game.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{% url 'games:wish' game.id %}" title="加入想玩">➕</a>
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+            <a href="{% url 'games:retrieve' game.id %}" class="entity-list__entity-link">
+                {% if request.GET.q %}
+                    {{ game.title | highlight:request.GET.q }}
+                {% else %}
+                    {{ game.title }}
+                {% endif %}
+            </a>
+            
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{item.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ game.source_url }}">
+                <span class="source-label source-label__{{ game.source_site }}">{{ game.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if game.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ game.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+
+            {% if game.other_title %}{% trans '别名' %}:
+            {% for other_title in game.other_title %}
+            {{ other_title }}{% 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.genre %}{% trans '类型' %}:
+            {% for genre in game.genre %}
+            {{ genre }}{% if not forloop.last %} {% endif %}
+            {% endfor %}/
+            {% endif %}
+
+            {% if game.platform %}{% trans '平台' %}:
+            {% for platform in game.platform %}
+            {{ platform }}{% if not forloop.last %} {% endif %}
+            {% endfor %}
+            {% endif %}
+
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ game.brief }}
+        </p>
+
+        <div class="tag-collection">
+            {% for tag_dict in game.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: <a href="{% url 'games:retrieve_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/list_item_movie.html b/common/templates/partial/list_item_movie.html
new file mode 100644
index 00000000..f20f40f5
--- /dev/null
+++ b/common/templates/partial/list_item_movie.html
@@ -0,0 +1,164 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load neo %}
+{% current_user_marked_item movie as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{% url 'movies:retrieve' movie.id %}">
+            <img src="{{ movie.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{% url 'movies:wish' movie.id %}" title="加入想看">➕</a>
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+            <a href="{% url 'movies:retrieve' movie.id %}" class="entity-list__entity-link">
+                {% if movie.season %}
+                
+                    {% if request.GET.q %}
+                        {{ movie.title | highlight:request.GET.q }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
+                        {{ movie.orig_title | highlight:request.GET.q }} Season {{ movie.season }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% else %}
+                        {{ movie.title }} {% trans '第' %}{{ movie.season|apnumber }}{% trans '季' %}
+                        {{ movie.orig_title }} Season {{ movie.season }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% endif %}
+
+                {% else %}
+                    {% if request.GET.q %}
+                        {{ movie.title | highlight:request.GET.q }} {{ movie.orig_title | highlight:request.GET.q }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% else %}
+                        {{ movie.title }} {{ movie.orig_title }}
+                        {% if movie.year %}({{ movie.year }}){% endif %}
+                    {% endif %}
+                {% endif %}
+            </a>
+            
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{movie.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ movie.source_url }}">
+                <span class="source-label source-label__{{ movie.source_site }}">{{ movie.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if movie.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ movie.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info ">
+
+            {% if movie.director %}{% trans '导演' %}:
+            {% for director in movie.director %}
+            {% if request.GET.q %}
+            {{ director | highlight:request.GET.q }}
+            {% else %}
+            {{ director }}
+            {% endif %}
+            {% if not forloop.last %},{% endif %}
+            {% endfor %}/
+            {% endif %}
+
+            {% if movie.genre %}{% trans '类型' %}:
+            {% for genre in movie.get_genre_display %}
+            {{ genre }}{% if not forloop.last %} {% endif %}
+            {% endfor %}/
+            {% endif %}
+
+        </span>
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+            {% if movie.actor %}{% trans '主演' %}:
+            {% for actor in movie.actor %}
+            <span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
+            {% if request.GET.q %}
+            {{ actor | highlight:request.GET.q }}
+            {% else %}
+            {{ actor }}
+            {% endif %}
+            </span>
+            {% if forloop.counter <= 5 %}
+                {% if not forloop.counter == 5 and not forloop.last %} {% endif %}
+            {% endif %}
+            {% endfor %}
+            {% endif %}
+        </span>
+        <p class="entity-list__entity-brief">
+            {{ movie.brief }}
+        </p>
+        <div class="tag-collection">
+            {% for tag_dict in movie.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: <a href="{% url 'movies:retrieve_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/list_item_music.html b/common/templates/partial/list_item_music.html
new file mode 100644
index 00000000..bcf0d29a
--- /dev/null
+++ b/common/templates/partial/list_item_music.html
@@ -0,0 +1,171 @@
+{% load thumb %}
+{% load highlight %}
+{% load i18n %}
+{% load l10n %}
+{% load neo %}
+{% current_user_marked_item music as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        
+        {% if music.category_name|lower == 'album' %}
+            <a href="{% url 'music:retrieve_album' music.id %}">
+                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+            </a>
+            {% if not marked %}
+            <a class="entity-list__entity-action-icon" hx-post="{% url 'music:wish_album' music.id %}" title="加入想听">➕</a>
+            {% endif %}
+        {% elif music.category_name|lower == 'song' %}
+            <a href="{% url 'music:retrieve_song' music.id %}">
+                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
+            </a>
+            {% if not marked %}
+            <a class="entity-list__entity-action-icon" hx-post="{% url 'music:wish_song' music.id %}" title="加入想听">➕</a>
+            {% endif %}
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        {% if editable %}
+        <div class="collection-item-position-edit">
+            {% if not forloop.first %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
+            {% endif %}
+            {% if not forloop.last %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
+            {% endif %}
+            <a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
+        </div>
+        {% endif %}
+
+        <div class="entity-list__entity-title">
+            
+            {% if music.category_name|lower == 'album' %}
+                <a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">                                                        
+                    {% if request.GET.q %}
+                    {{ music.title | highlight:request.GET.q }}
+                    {% else %}
+                    {{ music.title }}
+                    {% endif %}
+                </a>
+            {% elif music.category_name|lower == 'song' %}
+                <a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">                                                        
+                    {% if request.GET.q %}
+                    {{ music.title | highlight:request.GET.q }}
+                    {% else %}
+                    {{ music.title }}
+                    {% endif %}
+                </a>
+            {% endif %}
+                
+            
+            {% if not request.GET.c and not hide_category %}
+            <span class="entity-list__entity-category">[{{music.verbose_category_name}}]</span>
+            {% endif %}
+            <a href="{{ music.source_url }}">
+                <span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
+            </a>
+        </div>
+
+        {% if music.rating %}
+        <div class="rating-star entity-list__rating-star" data-rating-score="{{ music.rating | floatformat:"0" }}"></div>
+        <span class="entity-list__rating-score rating-score">{{ music.rating }}</span>
+        {% else %}
+        <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
+        {% endif %}
+
+        <span class="entity-list__entity-info ">
+            {% if music.artist %}{% trans '艺术家' %}:
+            {% for artist in music.artist %}
+            <span>{{ artist }}</span>
+            {% if not forloop.last %} {% endif %}
+            {% endfor %}
+            {% endif %}
+            
+            {% if music.genre %}/ {% trans '流派' %}:
+            {{ music.genre }}
+            {% endif %}
+
+            {% if music.release_date %}/ {% trans '发行日期' %}:
+                {{ music.release_date }}
+            {% endif %}
+        </span>
+        <span class="entity-list__entity-info entity-list__entity-info--full-length">
+
+        </span>
+        
+        {% if music.brief %}
+        <p class="entity-list__entity-brief">
+            {{ music.brief }}
+        </p>
+        {% elif music.category_name|lower == 'album' %}
+        <p class="entity-list__entity-brief">
+            {% trans '曲目:' %}{{ music.track_list }}
+        </p>
+        {% else %}
+        <!-- song -->
+        <p class="entity-list__entity-brief">
+            {% trans '所属专辑:' %}{{ music.album }}
+        </p>
+        {% endif %}
+            
+        <div class="tag-collection">
+            {% for tag_dict in music.top_tags %}
+            <span class="tag-collection__tag">
+                <a href="{% url 'common:search' %}?tag={{ tag_dict.content }}">{{ tag_dict.content }}</a>
+            </span>
+            {% endfor %}
+        </div>
+
+        {% if mark %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    {% if mark.rating %}
+                    <span class="entity-marks__rating-star rating-star"
+                        data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
+                    {% endif %}
+                    {% if mark.visibility > 0 %}
+                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
+                                <path
+                                    d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
+                        </svg></span>
+                    {% endif %}
+                    <span class="entity-marks__mark-time">
+                        {% trans '于' %} {{ mark.created_time }}
+                        {% if status == 'reviewed' %}
+                        {% trans '评论' %}: 
+                        {% if music.category_name|lower == 'album' %}
+                        <a href="{% url 'music:retrieve_album_review' mark.id %}">{{ mark.title }}</a>
+                        {% else %}
+                        <a href="{% url 'music:retrieve_song_review' mark.id %}">{{ mark.title }}</a>
+                        {% endif %}
+                        {% else %}
+                        {% trans '标记' %}
+                        {% endif %}
+                    </span>
+                    {% if mark.text %}
+                    <p class="entity-marks__mark-content">{{ mark.text }}</p>
+                    {% endif %}
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+        {% if collectionitem %}
+        <div class="clearfix"></div>
+        <div class="dividing-line dividing-line--dashed"></div>
+        <div class="entity-marks" style="margin-bottom: 0;">
+            <ul class="entity-marks__mark-list">
+                <li class="entity-marks__mark">
+                    <p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
+                        {% include "show_item_comment.html" %}
+                    </p>
+                </li>
+            </ul>
+        </div>
+        {% endif %}
+
+    </div>
+
+</li>
\ No newline at end of file
diff --git a/common/templates/partial/mark_list.html b/common/templates/partial/mark_list.html
new file mode 100644
index 00000000..71c4102a
--- /dev/null
+++ b/common/templates/partial/mark_list.html
@@ -0,0 +1,37 @@
+{% load i18n %}
+
+<ul class="entity-marks__mark-list">
+{% for others_mark in mark_list %}
+<li class="entity-marks__mark">
+    <a href="{% url 'users:home' others_mark.owner.mastodon_username %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
+
+    <span>{{ others_mark.get_status_display }}</span>
+
+    {% if others_mark.rating %}
+    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
+    {% endif %}
+
+    {% if others_mark.visibility > 0 %}
+    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
+    {% endif %}
+
+    {% if others_mark.shared_link %}
+    <a href="{{ others_mark.shared_link }}" target="_blank"><span class="entity-marks__mark-time">{{ others_mark.created_time }}</span></a>
+    {% else %}
+    <span class="entity-marks__mark-time">{{ others_mark.created_time }}</span>
+    {% endif %}
+
+    {% if current_item and others_mark.item != current_item %}
+    <span class="entity-marks__mark-time source-label"><a class="entity-marks__mark-time" href="{% url 'books:retrieve' others_mark.item.id %}">{{ others_mark.item.get_source_site_display }}</a></span>
+    {% endif %}
+
+    {% if others_mark.text %}
+    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
+    {% endif %}
+</li>
+{% empty %}
+
+<div> {% trans '暂无标记' %} </div>
+
+{% endfor %}
+</ul>
\ No newline at end of file
diff --git a/common/templatetags/highlight.py b/common/templatetags/highlight.py
index a2276800..02d79bbb 100644
--- a/common/templatetags/highlight.py
+++ b/common/templatetags/highlight.py
@@ -1,17 +1,19 @@
 from django import template
 from django.utils.safestring import mark_safe
 from django.template.defaultfilters import stringfilter
-from django.utils.html import format_html
+from opencc import OpenCC
 
-import re
 
+cc = OpenCC('t2s')
 register = template.Library()
 
+
 @register.filter
 @stringfilter
 def highlight(text, search):
-    to_be_replaced_words = set(re.findall(search, text, flags=re.IGNORECASE))
-
-    for word in to_be_replaced_words:
-        text = text.replace(word, f'<span class="highlight">{word}</span>')
+    for s in cc.convert(search.strip().lower()).split(' '):
+        if s:
+            p = cc.convert(text.lower()).find(s)
+            if p != -1:
+                text = f'{text[0:p]}<span class="highlight">{text[p:p+len(s)]}</span>{text[p+len(s):]}'
     return mark_safe(text)
diff --git a/common/templatetags/neo.py b/common/templatetags/neo.py
new file mode 100644
index 00000000..b915b7b9
--- /dev/null
+++ b/common/templatetags/neo.py
@@ -0,0 +1,48 @@
+from django import template
+import datetime
+from django.utils import timezone
+from collection.models import Collection
+
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def current_user_marked_item(context, item):
+    user = context['request'].user
+    if user and user.is_authenticated:
+        if isinstance(item, Collection) and item.owner == user:
+            return item
+        else:
+            return context['request'].user.get_mark_for_item(item)
+    return None
+
+
+@register.simple_tag(takes_context=True)
+def current_user_relationship(context, user):
+    current_user = context['request'].user
+    if current_user and current_user.is_authenticated:
+        if current_user.is_following(user):
+            if current_user.is_followed_by(user):
+                return '互相关注'
+            else:
+                return '已关注'
+        elif current_user.is_followed_by(user):
+            return '被ta关注'
+    return None
+
+
+@register.filter
+def prettydate(d):
+    diff = timezone.now() - d
+    s = diff.seconds
+    if diff.days > 14 or diff.days < 0:
+        return d.strftime('%Y年%m月%d日')
+    elif diff.days >= 1:
+        return '{} 天前'.format(diff.days)
+    elif s < 120:
+        return '刚刚'
+    elif s < 3600:
+        return '{} 分钟前'.format(s // 60)
+    else:
+        return '{} 小时前'.format(s // 3600)
diff --git a/common/templatetags/oauth_token.py b/common/templatetags/oauth_token.py
index 7aac83a1..b2f24677 100644
--- a/common/templatetags/oauth_token.py
+++ b/common/templatetags/oauth_token.py
@@ -7,7 +7,7 @@ register = template.Library()
 class OAuthTokenNode(template.Node):
     def render(self, context):
         request = context.get('request')
-        oauth_token = request.session.get('oauth_token', default='')
+        oauth_token = request.user.mastodon_token
         return format_html(oauth_token)
 
 
diff --git a/common/templatetags/thumb.py b/common/templatetags/thumb.py
index 2e21c6ca..aa698abb 100644
--- a/common/templatetags/thumb.py
+++ b/common/templatetags/thumb.py
@@ -12,4 +12,7 @@ def thumb(source, alias):
     if source.url.endswith('.svg'):
         return source.url
     else:
-        return thumbnail_url(source, alias)
\ No newline at end of file
+        try:
+            return thumbnail_url(source, alias)
+        except Exception as e:
+            return ''
diff --git a/common/urls.py b/common/urls.py
index 6de4f66a..b803a47d 100644
--- a/common/urls.py
+++ b/common/urls.py
@@ -6,4 +6,6 @@ urlpatterns = [
     path('', home),
     path('home/', home, name='home'),
     path('search/', search, name='search'),
+    path('search.json/', search, name='search.json'),
+    path('external_search/', external_search, name='external_search'),
 ]
diff --git a/common/utils.py b/common/utils.py
index 5d26a3d9..4d51ca16 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -10,6 +10,8 @@ class PageLinksGenerator:
     def __init__(self, length, current_page, total_pages):
         current_page = int(current_page)
         self.current_page = current_page
+        self.previous_page = current_page - 1 if current_page > 1 else None
+        self.next_page = current_page + 1 if current_page < total_pages else None
         self.start_page = None
         self.end_page = None
         self.page_range = None
diff --git a/common/views.py b/common/views.py
index c028184a..e527e09a 100644
--- a/common/views.py
+++ b/common/views.py
@@ -17,21 +17,93 @@ from music.models import Album, Song, AlbumMark, SongMark
 from users.models import Report, User, Preference
 from mastodon.decorators import mastodon_request_included
 from users.views import home as user_home
+from timeline.views import timeline as user_timeline
 from common.models import MarkStatusEnum
 from common.utils import PageLinksGenerator
-from common.scraper import scraper_registry
+from common.scraper import get_scraper_by_url, get_normalized_url
 from common.config import *
+from common.searcher import ExternalSources
 from management.models import Announcement
+from django.conf import settings
+from common.index import Indexer
+from django.http import JsonResponse
+from django.db.utils import IntegrityError
+
 
 logger = logging.getLogger(__name__)
 
+
 @login_required
 def home(request):
-    return user_home(request, request.user.id)
+    if request.user.get_preference().classic_homepage:
+        return redirect(reverse("users:home", args=[request.user.mastodon_username]))
+    else:
+        return redirect(reverse("timeline:timeline"))
 
 
 @login_required
+def external_search(request):
+    category = request.GET.get("c", default='all').strip().lower()
+    if category == 'all':
+        category = None
+    keywords = request.GET.get("q", default='').strip()
+    page_number = int(request.GET.get('page', default=1))
+    return render(
+        request,
+        "common/external_search_result.html",
+        {
+            "external_items": ExternalSources.search(category, keywords, page_number) if keywords else [],
+        }
+    )
+
+
 def search(request):
+    if settings.SEARCH_BACKEND is None:
+        return search2(request)
+    category = request.GET.get("c", default='all').strip().lower()
+    if category == 'all':
+        category = None
+    keywords = request.GET.get("q", default='').strip()
+    tag = request.GET.get("tag", default='').strip()
+    p = request.GET.get('page', default='1')
+    page_number = int(p) if p.isdigit() else 1
+    if not (keywords or tag):
+        return render(
+            request,
+            "common/search_result.html",
+            {
+                "items": None,
+            }
+        )
+    if request.user.is_authenticated:
+        url_validator = URLValidator()
+        try:
+            url_validator(keywords)
+            # validation success
+            return jump_or_scrape(request, keywords)
+        except ValidationError as e:
+            pass
+    
+    result = Indexer.search(keywords, page=page_number, category=category, tag=tag)
+    for item in result.items:
+        item.tag_list = item.all_tag_list[:TAG_NUMBER_ON_LIST]
+    if request.path.endswith('.json/'):
+        return JsonResponse({
+            'num_pages': result.num_pages,
+            'items':list(map(lambda i:i.get_json(), result.items))
+            })
+    return render(
+        request,
+        "common/search_result.html",
+        {
+            "items": result.items,
+            "pagination": PageLinksGenerator(PAGE_LINK_NUMBER, page_number, result.num_pages),
+            "categories": ['book', 'movie', 'music', 'game'],
+        }
+    )
+
+
+def search2(request):
     if request.method == 'GET':
 
         # test if input serach string is empty or not excluding param ?c=
@@ -109,7 +181,7 @@ def search(request):
             else:
                 ordered_queryset = list(queryset)
             return ordered_queryset
-            
+
         def movie_param_handler(**kwargs):
             # keywords
             keywords = kwargs.get('keywords')
@@ -240,7 +312,7 @@ def search(request):
                         elif music.__class__ == Song:
                             similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
                                 + 1/6 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() \
-                                + 1/6 * SequenceMatcher(None, keyword, music.album.title).quick_ratio()
+                                + 1/6 * (SequenceMatcher(None, keyword, music.album.title).quick_ratio() if music.album is not None else 0)
                         n += 1
                     music.similarity = similarity / n
                 elif tag:
@@ -322,32 +394,50 @@ def jump_or_scrape(request, url):
     if this_site in url:
         return redirect(url)
 
-    # match url to registerd sites
-    matched_host = None
-    for host in scraper_registry:
-        if host in url:
-            matched_host = host
-            break
-
-    if matched_host is None:
+    url = get_normalized_url(url)
+    scraper = get_scraper_by_url(url)
+    if scraper is None:
         # invalid url
-        return render(request, 'common/error.html', {'msg': _("链接非法,查询失败")})
+        return render(request, 'common/error.html', {'msg': _("链接无效,查询失败")})
     else:
-        scraper = scraper_registry[matched_host]
+        try:
+            effective_url = scraper.get_effective_url(url)
+        except ValueError:
+            return render(request, 'common/error.html', {'msg': _("链接无效,查询失败")})
         try:
             # raise ObjectDoesNotExist
-            effective_url = scraper.get_effective_url(url)
             entity = scraper.data_class.objects.get(source_url=effective_url)
             # if exists then jump to detail page
+            if request.path.endswith('.json/'):
+                return JsonResponse({
+                    'num_pages': 1,
+                    'items': [entity.get_json()]
+                    })
             return redirect(entity)
         except ObjectDoesNotExist:
             # scrape if not exists
             try:
                 scraper.scrape(url)
                 form = scraper.save(request_user=request.user)
+            except IntegrityError as ie:  # duplicate key on source_url may be caused by user's double submission
+                try:
+                    entity = scraper.data_class.objects.get(source_url=effective_url)
+                    return redirect(entity)
+                except Exception as e:
+                    logger.error(f"Scrape Failed URL: {url}\n{e}")
+                    if settings.DEBUG:
+                        logger.error("Expections during saving scraped data:", exc_info=e)
+                    return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
             except Exception as e:
-                logger.error(f"Scrape Failed URL: {url}")
-                logger.error("Expections during saving scraped data:", exc_info=e)
+                logger.error(f"Scrape Failed URL: {url}\n{e}")
+                if settings.DEBUG:
+                    logger.error("Expections during saving scraped data:", exc_info=e)
                 return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
             return redirect(form.instance)
 
+
+def go_relogin(request):
+    return render(request, 'common/error.html', {
+        'url': reverse("users:connect") + '?domain=' + request.user.mastodon_site,
+        'msg': _("信息已保存,但是未能分享到联邦网络"),
+        'secondary_msg': _("可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼")})
diff --git a/doc/GUIDE.md b/doc/GUIDE.md
new file mode 100644
index 00000000..055ca2bb
--- /dev/null
+++ b/doc/GUIDE.md
@@ -0,0 +1,106 @@
+NiceDB / NeoDB - Getting Start
+==============================
+This is a very basic guide with limited detail, contributions welcomed
+
+Install
+-------
+Install PostgreSQL, Redis and Python if not yet
+
+Setup database
+```
+CREATE DATABASE neodb ENCODING 'UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8' TEMPLATE template0;
+\c neodb;
+CREATE EXTENSION hstore WITH SCHEMA public;
+CREATE ROLE neodb with LOGIN ENCRYPTED PASSWORD 'abadface';
+GRANT ALL ON DATABASE neodb TO neodb;
+```
+
+Create and edit your own configuration file (optional but very much recommended)
+```
+mkdir mysite && cp boofilsic/settings.py mysite/
+export DJANGO_SETTINGS_MODULE=mysite.settings
+```
+
+Create and use `venv` as you normally would, then install packages 
+```
+python3 -m pip install -r requirements.txt
+```
+
+Quick check
+```
+python3 manage.py check
+```
+
+Initialize database
+```
+python3 manage.py makemigrations users books movies games music sync mastodon management collection
+python3 manage.py migrate users
+python3 manage.py migrate
+```
+
+Build static assets
+```
+python3 manage.py collectstatic
+```
+
+
+Start services
+--------------
+Make sure PostgreSQL and Redis are running
+
+Start job queue server
+```
+export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES  # required and only for macOS, otherwise it may crash
+python3 manage.py rqworker --with-scheduler doufen export mastodon
+```
+
+Run web server in dev mode
+```
+python3 manage.py runserver 0.0.0.0:80
+```
+
+It should be ready to serve from here, to run web server for production, consider `gunicorn -w 8 boofilsic.wsgi` in systemd or sth similar
+
+
+Migrate from an earlier version
+-------------------------------
+Update database
+```
+python3 manage.py makemigrations
+python3 manage.py migrate
+```
+
+Rebuild static assets
+```
+python3 manage.py sass common/static/sass/boofilsic.sass common/static/css/boofilsic.min.css -t compressed
+python3 manage.py sass common/static/sass/boofilsic.sass common/static/css/boofilsic.css
+python3 manage.py collectstatic
+```
+
+Add Cron Jobs
+-------------
+add `python manage.py refresh_mastodon` to crontab to run hourly, it will refresh cached users' follow/mute/block from mastodon
+
+Index and Search
+----------------
+Install TypeSense or Meilisearch, change `SEARCH_BACKEND` and coniguration for search server in `settings.py`
+
+Build initial index, it may take a few minutes or hours
+```
+python3 manage.py init_index
+python3 manage.py reindex
+```
+
+Other maintenance tasks
+-----------------------
+Requeue failed jobs
+```
+rq requeue --all --queue doufen
+```
+
+Run in Docker
+```
+docker-compose build
+docker-compose up db && docker exec -it app_db_1 psql -U postgres postgres -c 'CREATE EXTENSION hstore WITH SCHEMA public;'  # first time only
+docker-compose up
+```
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..86f8c09f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+version: '3'
+
+services:
+  redis:
+    image: redis:alpine
+
+  db:
+    image: postgres:14-alpine
+    volumes:
+      - /tmp/data/db:/var/lib/postgresql/data
+    environment:
+      - POSTGRES_DB=postgres
+      - POSTGRES_USER=postgres
+      - POSTGRES_PASSWORD=postgres
+
+  web:
+    build: .
+    command: python manage.py runserver 0.0.0.0:8000
+    volumes:
+      - .:/code
+    ports:
+      - "8000:8000"
+    environment:
+      - DB_HOST=db
+      - DB_NAME=postgres
+      - DB_USER=postgres
+      - DB_PASSWORD=postgres
+      - REDIS_HOST=redis
+      - DJANGO_SETTINGS_MODULE=neodb.dev
+    depends_on:
+      - db
+      - redis
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100755
index 00000000..5feb0354
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -o errexit  
+set -o pipefail  
+set -o nounset
+
+python manage.py collectstatic --noinput  
+python manage.py makemigrations users books movies games music sync mastodon management collection
+python manage.py makemigrations  
+python manage.py migrate users
+python manage.py migrate
+
+exec "$@"
diff --git a/docker/start.sh b/docker/start.sh
new file mode 100755
index 00000000..a54a76c7
--- /dev/null
+++ b/docker/start.sh
@@ -0,0 +1,36 @@
+#!/bin/bash  
+
+cd /app  
+
+if [ $# -eq 0 ]; then  
+    echo "Usage: start.sh <server|rq>"  
+    exit 1  
+fi  
+
+PROCESS_TYPE=$1  
+
+if [ "$PROCESS_TYPE" = "server" ]; then  
+    if [ "$DJANGO_DEBUG" = "true" ]; then  
+        gunicorn \  
+            --reload \  
+            --bind 0.0.0.0:8000 \  
+            --workers 2 \  
+            --worker-class eventlet \  
+            --log-level DEBUG \  
+            --access-logfile "-" \  
+            --error-logfile "-" \  
+            boofilsic.wsgi 
+    else  
+        gunicorn \  
+            --bind 0.0.0.0:8000 \  
+            --workers 2 \  
+            --worker-class eventlet \  
+            --log-level DEBUG \  
+            --access-logfile "-" \  
+            --error-logfile "-" \  
+            boofilsic.wsgi
+    fi  
+elif [ "$PROCESS_TYPE" = "rq" ]; then
+    rqworker --with-scheduler doufen export mastodon
+fi
+
diff --git a/games/admin.py b/games/admin.py
index fe72bd9a..36cc44fa 100644
--- a/games/admin.py
+++ b/games/admin.py
@@ -1,7 +1,8 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Game)
+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
index b74f62c9..a204c094 100644
--- a/games/apps.py
+++ b/games/apps.py
@@ -3,3 +3,8 @@ 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
index 5aa53330..e758f9fd 100644
--- a/games/forms.py
+++ b/games/forms.py
@@ -1,28 +1,24 @@
 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
+from .models import Game, GameMark, GameReview, GameMarkStatusTranslation
 from common.models import MarkStatusEnum
 from common.forms import *
 
 
 def GameMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在玩"),
-        MarkStatusEnum.WISH.value: _("想玩"),
-        MarkStatusEnum.COLLECT.value: _("玩过")
-    }
-    return trans_dict[status]
+    return GameMarkStatusTranslation[status]
 
 
 class GameForm(forms.ModelForm):
-    # id = forms.IntegerField(required=False, widget=forms.HiddenInput())
+    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',
@@ -66,11 +62,8 @@ class GameMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'game': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -85,14 +78,8 @@ class GameReviewForm(ReviewForm):
             'game',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'book': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'game': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/games/models.py b/games/models.py
index a2614f0a..fba9e639 100644
--- a/games/models.py
+++ b/games/models.py
@@ -1,17 +1,25 @@
 import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
 from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
-from boofilsic.settings import GAME_MEDIA_PATH_ROOT, DEFAULT_GAME_IMAGE
 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, GAME_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.GAME_MEDIA_PATH_ROOT)
 
 
 class Game(Entity):
@@ -53,7 +61,7 @@ class Game(Entity):
     )
 
     genre = postgres.ArrayField(
-        models.CharField(blank=True, default='', max_length=50),
+        models.CharField(blank=True, default='', max_length=200),
         null=True,
         blank=True,
         default=list,
@@ -61,23 +69,39 @@ class Game(Entity):
     )
 
     platform = postgres.ArrayField(
-        models.CharField(blank=True, default='', max_length=50),
+        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=DEFAULT_GAME_IMAGE, blank=True)
-
+    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 wish_url(self):
+        return reverse("games:wish", args=[self.id])
+
     def get_tags_manager(self):
         return self.game_tags
 
@@ -85,6 +109,14 @@ class Game(Entity):
     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(
@@ -96,6 +128,10 @@ class GameMark(Mark):
                 fields=['owner', 'game'], name='unique_game_mark')
         ]
 
+    @property
+    def translated_status(self):
+        return GameMarkStatusTranslation[self.status]
+
 
 class GameReview(Review):
     game = models.ForeignKey(
@@ -107,6 +143,14 @@ class GameReview(Review):
                 fields=['owner', 'game'], name='unique_game_review')
         ]
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + reverse("games:retrieve_review", args=[self.id])
+
+    @property
+    def item(self):
+        return self.game
+
 
 class GameTag(Tag):
     game = models.ForeignKey(
@@ -119,3 +163,7 @@ class GameTag(Tag):
             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
index 40a768e2..178089c4 100644
--- a/games/templates/games/create_update.html
+++ b/games/templates/games/create_update.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -22,9 +22,24 @@
 
             <section id="content" class="container">
                 <div class="grid">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'games:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'games:scrape' %}"
-                            class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                        {% comment %} <a href="{% url 'games:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -55,12 +70,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/games/templates/games/create_update_review.html b/games/templates/games/create_update_review.html
index 54391b6d..cc9f8a2c 100644
--- a/games/templates/games/create_update_review.html
+++ b/games/templates/games/create_update_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -94,7 +94,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -114,12 +114,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/games/templates/games/delete.html b/games/templates/games/delete.html
index 008a561b..a7a2adde 100644
--- a/games/templates/games/delete.html
+++ b/games/templates/games/delete.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除电影/剧集' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if game.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' game.last_editor.id %}">
+                                    <a href="{% url 'users:home' game.last_editor.mastodon_username %}">
                                         <span>{{ game.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/games/templates/games/delete_review.html b/games/templates/games/delete_review.html
index b24fc5d2..f39cdf62 100644
--- a/games/templates/games/delete_review.html
+++ b/games/templates/games/delete_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/games/templates/games/detail.html b/games/templates/games/detail.html
index e32ebf66..fed9eb7a 100644
--- a/games/templates/games/detail.html
+++ b/games/templates/games/detail.html
@@ -14,21 +14,20 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB游戏 - {{ game.title }}">
+    <meta property="og:title" content="{{ site_name }}游戏 - {{ game.title }}">
     <meta property="og:type" content="game">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ game.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ game.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ game.brief }}">
     
-    <title>{% trans 'NiceDB - 游戏详情' %} | {{ game.title }}</title>
+    <title>{{ site_name }} - {% trans '游戏详情' %} | {{ game.title }}</title>
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -54,11 +53,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if game.rating %}
+                                            {% if game.rating and game.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ game.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ game.rating }} </span>
+                                            <small>({{ game.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
 
@@ -72,7 +72,7 @@
                                             {% if game.other_title|length > 5 %}
                                             <a href="javascript:void(0);" id="otherTitleMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#otherTitleMore").click(function (e) {
+                                                $("#otherTitleMore").on('click', function (e) {
                                                     $("span.other_title:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -111,7 +111,15 @@
                                             {% trans '发行日期:' %}{{ game.release_date }}
                                             {% endif %}
                                         </div>
-                             
+                                        
+                                        {% if game.other_info %}                                  
+                                        {% for k, v in game.other_info.items %}
+                                        <div>
+                                            {{ k }}:{{ v  | urlize }}
+                                        </div>
+                                        {% endfor %}
+                                        {% endif %}
+                                        
                                     </div>
                                     <div class="entity-detail__fields">
                                         
@@ -123,17 +131,10 @@
                                             {% endif %}
                                         </div>
 
-                                        {% if game.other_info %}                                  
-                                        {% for k, v in game.other_info.items %}
-                                        <div>
-                                            {{ k }}:{{ v  | urlize }}
-                                        </div>
-                                        {% endfor %}
-                                        {% endif %}
-                                        
+
                                     
                                         {% if game.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.id %}">{{ game.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.mastodon_username %}">{{ game.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -178,43 +179,23 @@
                                 
                                 <h5 class="entity-marks__title">{% trans '这个游戏的标记' %}</h5>
                                 {% if mark_list_more %}
-                                <a href="{% url 'games:retrieve_mark_list' game.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'games:retrieve_mark_list' game.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'games:retrieve_mark_list' game.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=game %}    
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这个游戏的评论' %}</h5>
 
                                 {% if review_list_more %}
-                                <a href="{% url 'games:retrieve_review_list' game.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'games:retrieve_review_list' game.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
@@ -242,7 +223,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -254,7 +235,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -286,7 +267,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -315,6 +296,24 @@
 
                             {% endif %}
                         </div>
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'game' game.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
                                 
                     </div>
                 </div>
@@ -377,8 +376,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/games/templates/games/mark_list.html b/games/templates/games/mark_list.html
index 45071f89..6f0acffc 100644
--- a/games/templates/games/mark_list.html
+++ b/games/templates/games/mark_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ game.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ game.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,37 +35,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'games:retrieve' game.id %}">{{ game.title }}</a>{% trans ' 的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-        
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                    </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-                                        
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=game %}
                             </div>
                             <div class="pagination">
                             
@@ -149,12 +119,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
   
diff --git a/games/templates/games/review_detail.html b/games/templates/games/review_detail.html
index 1ffab003..6b0c357f 100644
--- a/games/templates/games/review_detail.html
+++ b/games/templates/games/review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB影评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}游戏评论 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ game.cover|thumb:'normal' }}">
+    <title>{{ site_name }}游戏评论 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -73,6 +74,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -134,16 +136,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/games/templates/games/review_list.html b/games/templates/games/review_list.html
index 10b01d43..8b05318d 100644
--- a/games/templates/games/review_list.html
+++ b/games/templates/games/review_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ game.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ game.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -41,8 +41,8 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
@@ -137,12 +137,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/games/templates/games/scrape.html b/games/templates/games/scrape.html
index 87f2efa8..aaaeb99b 100644
--- a/games/templates/games/scrape.html
+++ b/games/templates/games/scrape.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/games/urls.py b/games/urls.py
index ff984632..88ce1629 100644
--- a/games/urls.py
+++ b/games/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -8,9 +8,10 @@ urlpatterns = [
     path('<int:id>/', retrieve, name='retrieve'),
     path('update/<int:id>/', update, name='update'),
     path('delete/<int:id>/', delete, name='delete'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('mark/', create_update_mark, name='create_update_mark'),
-    path('<int:game_id>/mark/list/',
-         retrieve_mark_list, name='retrieve_mark_list'),
+    path('wish/<int:id>/', wish, name='wish'),
+    re_path('(?P<game_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
     path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
     path('<int:game_id>/review/create/', create_review, name='create_review'),
     path('review/update/<int:id>/', update_review, name='update_review'),
diff --git a/games/views.py b/games/views.py
index f69e4918..0f0fc166 100644
--- a/games/views.py
+++ b/games/views.py
@@ -2,21 +2,23 @@ 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
+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.api import check_visibility, post_toot, TootVisibilityEnum
-from mastodon.utils import rating_to_emoji
+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
+from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
 from common.models import SourceSiteEnum
 from .models import *
 from .forms import *
-from boofilsic.settings import MASTODON_TAGS
+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__)
@@ -87,6 +89,18 @@ def create(request):
         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':
@@ -98,6 +112,7 @@ def update(request, id):
             'games/create_update.html',
             {
                 'form': form,
+                'is_update': True,
                 'title': page_title,
                 'submit_url': reverse("games:update", args=[game.id]),
                 # provided for frontend js
@@ -127,6 +142,7 @@ def update(request, id):
                 'games/create_update.html',
                 {
                     'form': form,
+                    'is_update': True,
                     'title': page_title,
                     'submit_url': reverse("games:update", args=[game.id]),
                     # provided for frontend js
@@ -167,6 +183,7 @@ def retrieve(request, id):
         else:
             mark_form = GameMarkForm(initial={
                 'game': game,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -186,10 +203,8 @@ def retrieve(request, id):
             mark_list_more = None
             review_list_more = None
         else:
-            mark_list = GameMark.get_available(
-                game, request.user, request.session['oauth_token'])
-            review_list = GameReview.get_available(
-                game, request.user, request.session['oauth_token'])
+            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:
@@ -197,6 +212,7 @@ def retrieve(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(game=game)))
 
         # def strip_html_tags(text):
         #     import re
@@ -221,6 +237,7 @@ def retrieve(request, id):
                 'review_list_more': review_list_more,
                 'game_tag_list': game_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -265,12 +282,19 @@ def create_update_mark(request):
         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:
@@ -278,7 +302,7 @@ def create_update_mark(request):
             form = GameMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -306,28 +330,10 @@ def create_update_mark(request):
                 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("games:retrieve",
-                                                                args=[game.id])
-                words = GameMarkStatusTranslator(form.cleaned_data['status']) +\
-                    f"《{game.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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("games:retrieve", args=[form.instance.game.id]))
     else:
@@ -336,11 +342,30 @@ def create_update_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_mark_list(request, game_id):
+def wish(request, id):
+    if request.method == 'POST':
+        game = get_object_or_404(Game, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            '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, request.session['oauth_token'])
+        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)
@@ -401,23 +426,8 @@ def create_review(request, game_id):
             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("games:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.game.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("games:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -453,23 +463,8 @@ def update_review(request, id):
             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("games:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.game.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("games:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -504,11 +499,10 @@ def delete_review(request, id):
 
 
 @mastodon_request_included
-@login_required
 def retrieve_review(request, id):
     if request.method == 'GET':
         review = get_object_or_404(GameReview, pk=id)
-        if not check_visibility(review, request.session['oauth_token'], request.user):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -543,8 +537,7 @@ def retrieve_review(request, id):
 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, request.session['oauth_token'])
+        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)
diff --git a/management/models.py b/management/models.py
index 9816781e..c5bf2f64 100644
--- a/management/models.py
+++ b/management/models.py
@@ -1,7 +1,7 @@
 import re
 from django.db import models
 from django.shortcuts import reverse
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 from markdownx.models import MarkdownxField
 from markdown import markdown
 
diff --git a/management/templates/management/create_update.html b/management/templates/management/create_update.html
index 9ffd568c..692dc126 100644
--- a/management/templates/management/create_update.html
+++ b/management/templates/management/create_update.html
@@ -4,8 +4,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/skeleton.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
     <title>Create/Update Announcement</title>
 </head>
 <body>
diff --git a/management/templates/management/delete.html b/management/templates/management/delete.html
index 11f08209..d48b740d 100644
--- a/management/templates/management/delete.html
+++ b/management/templates/management/delete.html
@@ -4,8 +4,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/skeleton.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
     <title>Delete Announcement</title>
 </head>
 <body>
diff --git a/management/templates/management/detail.html b/management/templates/management/detail.html
index a7cf64e5..76150b1d 100644
--- a/management/templates/management/detail.html
+++ b/management/templates/management/detail.html
@@ -6,9 +6,9 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <title>NiceDB - {{ object.title }}</title>
+    <title>{{ site_name }} - {{ object.title }}</title>
 </head>
 
 <body>
diff --git a/management/templates/management/list.html b/management/templates/management/list.html
index c8199e1f..ab5a2b24 100644
--- a/management/templates/management/list.html
+++ b/management/templates/management/list.html
@@ -7,10 +7,10 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 
-    <title>{% trans 'NiceDB - 公告栏' %}</title>
+    <title>{{ site_name }} - {% trans '公告栏' %}</title>
 </head>
 
 <body>
diff --git a/mastodon/admin.py b/mastodon/admin.py
index b29c2a3c..e81bc74b 100644
--- a/mastodon/admin.py
+++ b/mastodon/admin.py
@@ -38,7 +38,7 @@ class MastodonApplicationModelAdmin(admin.ModelAdmin):
                         try:
                             response = create_app(request.POST.get('domain_name'))
                         except (Timeout, ConnectionError):
-                            request.POST['domain_name'] = _("长毛象请求超时。")
+                            request.POST['domain_name'] = _("联邦网络请求超时。")
                         except Exception as e:
                             request.POST['domain_name'] = str(e)
                         else:
diff --git a/mastodon/api.py b/mastodon/api.py
index 5e48c944..1919f099 100644
--- a/mastodon/api.py
+++ b/mastodon/api.py
@@ -2,10 +2,16 @@ import requests
 import string
 import random
 import functools
+import logging
 from django.core.exceptions import ObjectDoesNotExist
-from boofilsic.settings import MASTODON_TIMEOUT
-from boofilsic.settings import CLIENT_NAME, APP_WEBSITE, REDIRECT_URIS
-from .models import CrossSiteUserInfo
+from django.conf import settings
+from django.shortcuts import reverse
+from urllib.parse import quote
+from .models import CrossSiteUserInfo, MastodonApplication
+from mastodon.utils import rating_to_emoji
+
+
+logger = logging.getLogger(__name__)
 
 # See https://docs.joinmastodon.org/methods/accounts/
 
@@ -46,39 +52,76 @@ API_CREATE_APP = '/api/v1/apps'
 # GET
 API_SEARCH = '/api/v2/search'
 
+TWITTER_DOMAIN = 'twitter.com'
 
-get = functools.partial(requests.get, timeout=MASTODON_TIMEOUT)
-post = functools.partial(requests.post, timeout=MASTODON_TIMEOUT)
+TWITTER_API_ME = 'https://api.twitter.com/2/users/me'
+
+TWITTER_API_POST = 'https://api.twitter.com/2/tweets'
+
+TWITTER_API_TOKEN = 'https://api.twitter.com/2/oauth2/token'
+
+USER_AGENT = f"{settings.CLIENT_NAME}/1.0"
+
+get = functools.partial(requests.get, timeout=settings.MASTODON_TIMEOUT)
+post = functools.partial(requests.post, timeout=settings.MASTODON_TIMEOUT)
 
 
 # low level api below
-def get_relationships(site, id_list, token):
+def get_relationships(site, id_list, token):  # no longer in use
     url = 'https://' + site + API_GET_RELATIONSHIPS
     payload = {'id[]': id_list}
     headers = {
+        'User-Agent': USER_AGENT,
         'Authorization': f'Bearer {token}'
     }
-    response = get(url, headers=headers, data=payload)
+    response = get(url, headers=headers, params=payload)
     return response.json()
 
 
 def post_toot(site, content, visibility, token, local_only=False):
-    url = 'https://' + site + API_PUBLISH_TOOT
     headers = {
+        'User-Agent': USER_AGENT,
         'Authorization': f'Bearer {token}',
         'Idempotency-Key': random_string_generator(16)
     }
-    payload = {
-        'status': content,
-        'visibility': visibility,
-        'local_only': True,
-    }
-    if not local_only:
-        del payload['local_only']
-    response = post(url, headers=headers, data=payload)
+    if site == TWITTER_DOMAIN:
+        url = TWITTER_API_POST
+        payload = {
+            'text': content if len(content) <= 150 else content[0:150] + '...'
+        }
+        response = post(url, headers=headers, json=payload)
+    else:
+        url = 'https://' + site + API_PUBLISH_TOOT
+        payload = {
+            'status': content,
+            'visibility': visibility,
+        }
+        if local_only:
+            payload['local_only'] = True
+        try:
+            response = post(url, headers=headers, data=payload)
+            if response.status_code == 201:
+                response.status_code = 200
+            if response.status_code != 200:
+                logger.error(f"Error {url} {response.status_code}")
+        except Exception:
+            response = None
     return response
 
 
+def get_instance_info(domain_name):
+    if domain_name.lower().strip() == TWITTER_DOMAIN:
+        return TWITTER_DOMAIN, ''
+    try:
+        url = f'https://{domain_name}/api/v1/instance'
+        response = get(url, headers={'User-Agent': USER_AGENT})
+        j = response.json()
+        return j['uri'].lower().split('//')[-1].split('/')[0], j['version']
+    except Exception:
+        logger.error(f"Error {url}")
+        return domain_name, ''
+
+
 def create_app(domain_name):
     # naive protocal strip
     is_http = False
@@ -96,18 +139,13 @@ def create_app(domain_name):
         url = 'http://' + domain_name + API_CREATE_APP
 
     payload = {
-        'client_name': CLIENT_NAME,
-        'scopes': 'read write follow',
-        'redirect_uris': REDIRECT_URIS,
-        'website': APP_WEBSITE
+        'client_name': settings.CLIENT_NAME,
+        'scopes': settings.MASTODON_CLIENT_SCOPE,
+        'redirect_uris': settings.REDIRECT_URIS,
+        'website': settings.APP_WEBSITE
     }
 
-    from boofilsic.settings import DEBUG
-    if DEBUG:
-        payload['redirect_uris'] = 'http://localhost/users/OAuth2_login/\nurn:ietf:wg:oauth:2.0:oob'
-        payload['client_name'] = 'test_do_not_authorise'
-
-    response = post(url, data=payload)
+    response = post(url, data=payload, headers={'User-Agent': USER_AGENT})
     return response
 
 
@@ -116,26 +154,35 @@ def get_site_id(username, user_site, target_site, token):
     payload = {
         'limit': 1,
         'type': 'accounts',
+        'resolve': True,
         'q': f"{username}@{user_site}"
     }
     headers = {
+        'User-Agent': USER_AGENT,
         'Authorization': f'Bearer {token}'
     }
-    response = get(url, data=payload, headers=headers)
-    data = response.json()
-    if not data['accounts']:
+    response = get(url, params=payload, headers=headers)
+    try:
+        data = response.json()
+    except Exception:
+        logger.error(f"Error parsing JSON from {url}")
+        return None
+    if 'accounts' not in data:
+        return None
+    elif len(data['accounts']) == 0:  # target site may return empty if no cache of this user
+        return None
+    elif data['accounts'][0]['acct'] != f"{username}@{user_site}":  # or return another user with a similar id which needs to be skipped
         return None
     else:
         return data['accounts'][0]['id']
 
 
 # high level api below
-def get_relationship(request_user, target_user, token):
-    if request_user.mastodon_site == target_user.mastodon_site:
-        return get_relationships(request_user.mastodon_site, target_user.mastodon_id, token)
-    else:
-        cross_site_id = get_cross_site_id(target_user, request_user.mastodon_site, token)
-        return get_relationships(request_user.mastodon_site, [cross_site_id,], token)
+def get_relationship(request_user, target_user, useless_token=None):
+    return [{
+        'blocked_by': target_user.is_blocking(request_user),
+        'following': request_user.is_following(target_user),
+    }]
 
 
 def get_cross_site_id(target_user, target_site, token):
@@ -147,6 +194,8 @@ def get_cross_site_id(target_user, target_site, token):
     """
     if target_site == target_user.mastodon_site:
         return target_user.mastodon_id
+    if target_site == TWITTER_DOMAIN:
+        return None
 
     try:
         cross_site_info = CrossSiteUserInfo.objects.get(
@@ -157,6 +206,7 @@ def get_cross_site_id(target_user, target_site, token):
         cross_site_id = get_site_id(
             target_user.username, target_user.mastodon_site, target_site, token)
         if not cross_site_id:
+            logger.error(f'unable to find cross_site_id for {target_user} on {target_site}')
             return None
         cross_site_info = CrossSiteUserInfo.objects.create(
             uid=f"{target_user.username}@{target_user.mastodon_site}",
@@ -167,30 +217,251 @@ def get_cross_site_id(target_user, target_site, token):
     return cross_site_info.site_id
 
 
-def check_visibility(user_owned_entity, token, visitor):
-    """
-    check if given user can see the user owned entity
-    """
-    if not visitor == user_owned_entity.owner:
-        # mastodon request
-        relationship = get_relationship(visitor, user_owned_entity.owner, token)[0]
-        if relationship['blocked_by']:
-            return False
-        if not relationship['following'] and user_owned_entity.is_private:
-            return False
-        return True
-    else:
-        return True
-
-
 # utils below
 def random_string_generator(n):
     s = string.ascii_letters + string.punctuation + string.digits
     return ''.join(random.choice(s) for i in range(n))
 
 
+def verify_account(site, token):
+    if site == TWITTER_DOMAIN:
+        url = TWITTER_API_ME + '?user.fields=id,username,name,description,profile_image_url,created_at,protected'
+        try:
+            response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
+            if response.status_code != 200:
+                logger.error(f"Error {url} {response.status_code}")
+                return response.status_code, None
+            r = response.json()['data']
+            r['display_name'] = r['name']
+            r['note'] = r['description']
+            r['avatar'] = r['profile_image_url']
+            r['avatar_static'] = r['profile_image_url']
+            r['locked'] = r['protected']
+            r['url'] = f'https://{TWITTER_DOMAIN}/{r["username"]}'
+            return 200, r
+        except Exception:
+            return -1, None
+    url = 'https://' + site + API_VERIFY_ACCOUNT
+    try:
+        response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
+        return response.status_code, (response.json() if response.status_code == 200 else None)
+    except Exception:
+        return -1, None
+
+
+def get_related_acct_list(site, token, api):
+    if site == TWITTER_DOMAIN:
+        return []
+    url = 'https://' + site + api
+    results = []
+    while url:
+        response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
+        url = None
+        if response.status_code == 200:
+            results.extend(map(lambda u: (u['acct'] if u['acct'].find('@') != -1 else u['acct'] + '@' + site) if 'acct' in u else u, response.json()))
+            if 'Link' in response.headers:
+                for ls in response.headers['Link'].split(','):
+                    li = ls.strip().split(';')
+                    if li[1].strip() == 'rel="next"':
+                        url = li[0].strip().replace('>', '').replace('<', '')
+    return results
+
+
 class TootVisibilityEnum:
     PUBLIC = 'public'
     PRIVATE = 'private'
     DIRECT = 'direct'
     UNLISTED = 'unlisted'
+
+
+def get_mastodon_application(domain):
+    app = MastodonApplication.objects.filter(domain_name=domain).first()
+    if app is not None:
+        return app, ''
+    if domain == TWITTER_DOMAIN:
+        return None, 'Twitter未配置'
+    error_msg = None
+    try:
+        response = create_app(domain)
+    except (requests.exceptions.Timeout, ConnectionError):
+        error_msg = "联邦网络请求超时。"
+        logger.error(f'Error creating app for {domain}: Timeout')
+    except Exception as e:
+        error_msg = "联邦网络请求失败 " + str(e)
+        logger.error(f'Error creating app for {domain}: {e}')
+    else:
+        # fill the form with returned data
+        if response.status_code != 200:
+            error_msg = "实例连接错误,代码: " + str(response.status_code)
+            logger.error(f'Error creating app for {domain}: {response.status_code}')
+        else:
+            try:
+                data = response.json()
+            except Exception:
+                error_msg = "实例返回内容无法识别"
+                logger.error(f'Error creating app for {domain}: unable to parse response')
+            else:
+                if settings.MASTODON_ALLOW_ANY_SITE:
+                    app = MastodonApplication.objects.create(domain_name=domain, app_id=data['id'], client_id=data['client_id'],
+                    client_secret=data['client_secret'], vapid_key=data['vapid_key'] if 'vapid_key' in data else '')
+                else:
+                    error_msg = "不支持其它实例登录"
+                    logger.error(f'Disallowed to create app for {domain}')
+    return app, error_msg
+
+
+def get_mastodon_login_url(app, login_domain, version, request):
+    url = request.scheme + "://" + request.get_host() + reverse('users:OAuth2_login')
+    if login_domain == TWITTER_DOMAIN:
+        return f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={app.client_id}&redirect_uri={quote(url)}&scope={quote(settings.TWITTER_CLIENT_SCOPE)}&state=state&code_challenge=challenge&code_challenge_method=plain"
+    scope = settings.MASTODON_LEGACY_CLIENT_SCOPE if 'Pixelfed' in version else settings.MASTODON_CLIENT_SCOPE
+    return "https://" + login_domain + "/oauth/authorize?client_id=" + app.client_id + "&scope=" + quote(scope) + "&redirect_uri=" + url + "&response_type=code"
+
+
+def obtain_token(site, request, code):
+    """ Returns token if success else None. """
+    mast_app = MastodonApplication.objects.get(domain_name=site)
+    redirect_uri = request.scheme + "://" + request.get_host() + reverse('users:OAuth2_login')
+    payload = {
+        'client_id': mast_app.client_id,
+        'client_secret': mast_app.client_secret,
+        'redirect_uri': redirect_uri,
+        'grant_type': 'authorization_code',
+        'code': code
+    }
+    headers = {'User-Agent': USER_AGENT}
+    auth = None
+    if mast_app.is_proxy:
+        url = 'https://' + mast_app.proxy_to + API_OBTAIN_TOKEN
+    elif site == TWITTER_DOMAIN:
+        url = TWITTER_API_TOKEN
+        auth = (mast_app.client_id, mast_app.client_secret)
+        del payload['client_secret']
+        payload['code_verifier'] = 'challenge'
+    else:
+        url = 'https://' + mast_app.domain_name + API_OBTAIN_TOKEN
+    try:
+        response = post(url, data=payload, headers=headers, auth=auth)
+        # {"token_type":"bearer","expires_in":7200,"access_token":"VGpkOEZGR3FQRDJ5NkZ0dmYyYWIwS0dqeHpvTnk4eXp0NV9nWDJ2TEpmM1ZTOjE2NDg3ODMxNTU4Mzc6MToxOmF0OjE","scope":"block.read follows.read offline.access tweet.write users.read mute.read","refresh_token":"b1pXbGEzeUF1WE5yZHJOWmxTeWpvMTBrQmZPd0czLU0tQndZQTUyU3FwRDVIOjE2NDg3ODMxNTU4Mzg6MToxOnJ0OjE"}
+        if response.status_code != 200:
+            logger.error(f"Error {url} {response.status_code}")
+            return None, None
+    except Exception as e:
+        logger.error(f"Error {url} {e}")
+        return None, None
+    data = response.json()
+    return data.get('access_token'), data.get('refresh_token', '')
+
+
+def refresh_access_token(site, refresh_token):
+    if site != TWITTER_DOMAIN:
+        return None
+    mast_app = MastodonApplication.objects.get(domain_name=site)
+    url = TWITTER_API_TOKEN
+    payload = {
+        'client_id': mast_app.client_id,
+        'refresh_token': refresh_token,
+        'grant_type': 'refresh_token',
+    }
+    headers = {'User-Agent': USER_AGENT}
+    auth = (mast_app.client_id, mast_app.client_secret)
+    response = post(url, data=payload, headers=headers, auth=auth)
+    if response.status_code != 200:
+        logger.error(f"Error {url} {response.status_code}")
+        return None
+    data = response.json()
+    return data.get('access_token')
+
+
+def revoke_token(site, token):
+    mast_app = MastodonApplication.objects.get(domain_name=site)
+
+    payload = {
+        'client_id': mast_app.client_id,
+        'client_secret': mast_app.client_secret,
+        'token': token
+    }
+
+    if mast_app.is_proxy:
+        url = 'https://' + mast_app.proxy_to + API_REVOKE_TOKEN
+    else:
+        url = 'https://' + site + API_REVOKE_TOKEN
+    post(url, data=payload, headers={'User-Agent': USER_AGENT})
+
+
+def share_mark(mark):
+    user = mark.owner
+    if mark.visibility == 2:
+        visibility = TootVisibilityEnum.DIRECT
+    elif mark.visibility == 1:
+        visibility = TootVisibilityEnum.PRIVATE
+    elif user.get_preference().mastodon_publish_public:
+        visibility = TootVisibilityEnum.PUBLIC
+    else:
+        visibility = TootVisibilityEnum.UNLISTED
+    tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(mark.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
+    stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode)
+    content = f"{mark.translated_status}《{mark.item.title}》{stars}\n{mark.item.url}\n{mark.text}{tags}"
+    response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
+    if response and response.status_code in [200, 201]:
+        j = response.json()
+        if 'url' in j:
+            mark.shared_link = j['url']
+        elif 'data' in j:
+            mark.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
+        if mark.shared_link:
+            mark.save(update_fields=['shared_link'])
+        return True
+    else:
+        return False
+
+
+def share_review(review):
+    user = review.owner
+    if review.visibility == 2:
+        visibility = TootVisibilityEnum.DIRECT
+    elif review.visibility == 1:
+        visibility = TootVisibilityEnum.PRIVATE
+    elif user.get_preference().mastodon_publish_public:
+        visibility = TootVisibilityEnum.PUBLIC
+    else:
+        visibility = TootVisibilityEnum.UNLISTED
+    tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(review.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
+    content = f"发布了关于《{review.item.title}》的评论\n{review.url}\n{review.title}{tags}"
+    response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
+    if response and response.status_code in [200, 201]:
+        j = response.json()
+        if 'url' in j:
+            review.shared_link = j['url']
+        elif 'data' in j:
+            review.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
+        if review.shared_link:
+            review.save(update_fields=['shared_link'])
+        return True
+    else:
+        return False
+
+
+def share_collection(collection, comment, user, visibility_no):
+    if visibility_no == 2:
+        visibility = TootVisibilityEnum.DIRECT
+    elif visibility_no == 1:
+        visibility = TootVisibilityEnum.PRIVATE
+    elif user.get_preference().mastodon_publish_public:
+        visibility = TootVisibilityEnum.PUBLIC
+    else:
+        visibility = TootVisibilityEnum.UNLISTED
+    tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', '收藏单') if user.get_preference().mastodon_append_tag else ''
+    content = f"分享收藏单《{collection.title}》\n{collection.url}\n{comment}{tags}"
+    response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
+    if response and response.status_code in [200, 201]:
+        j = response.json()
+        if 'url' in j:
+            shared_link = j['url']
+        elif 'data' in j:
+            shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
+        if shared_link:
+            pass
+        return True
+    else:
+        return False
diff --git a/mastodon/auth.py b/mastodon/auth.py
index 29a691e8..02d7703f 100644
--- a/mastodon/auth.py
+++ b/mastodon/auth.py
@@ -1,74 +1,5 @@
 from django.contrib.auth.backends import ModelBackend, UserModel
-from django.shortcuts import reverse
-from .api import *
-from .models import MastodonApplication
-
-
-def obtain_token(site, request, code):
-    """ Returns token if success else None. """
-    mast_app = MastodonApplication.objects.get(domain_name=site)
-    payload = {
-        'client_id': mast_app.client_id,
-        'client_secret': mast_app.client_secret,
-        'redirect_uri': f"https://{request.get_host()}{reverse('users:OAuth2_login')}",
-        'grant_type': 'authorization_code',
-        'code': code,
-        'scope': 'read write'
-    }
-    from boofilsic.settings import DEBUG
-    if DEBUG:
-        payload['redirect_uri']= f"http://{request.get_host()}{reverse('users:OAuth2_login')}",
-    if mast_app.is_proxy:
-        url = 'https://' + mast_app.proxy_to + API_OBTAIN_TOKEN
-    else:
-        url = 'https://' + mast_app.domain_name + API_OBTAIN_TOKEN
-    response = post(url, data=payload)
-    if response.status_code != 200:
-        return
-    data = response.json()
-    return data.get('access_token')
-
-
-def get_user_data(site, token):
-    url = 'https://' + site + API_VERIFY_ACCOUNT
-    headers = {
-        'Authorization': f'Bearer {token}'
-    }
-    response = get(url, headers=headers)
-    if response.status_code != 200:
-        return None
-    return response.json()
-
-
-def revoke_token(site, token):
-    mast_app = MastodonApplication.objects.get(domain_name=site)
-
-    payload = {
-        'client_id': mast_app.client_id,
-        'client_secret': mast_app.client_secret,
-        'scope': token
-    }
-
-    if mast_app.is_proxy:
-        url = 'https://' + mast_app.proxy_to + API_REVOKE_TOKEN
-    else:
-        url = 'https://' + site + API_REVOKE_TOKEN
-    response = post(url, data=payload)
-
-
-def verify_token(site, token):
-    """ Check if the token is valid and is of local instance. """
-    url = 'https://' + site + API_VERIFY_ACCOUNT
-    headers = {
-        'Authorization': f'Bearer {token}'
-    }
-    response = get(url, headers=headers)
-    if response.status_code == 200:
-        res_data = response.json()
-        # check if is local instance user
-        if res_data['acct'] == res_data['username']:
-            return True
-    return False
+from .api import verify_account
 
 
 class OAuth2Backend(ModelBackend):
@@ -76,22 +7,23 @@ class OAuth2Backend(ModelBackend):
     # "authenticate() should check the credentials it gets and returns
     #  a user object that matches those credentials."
     # arg request is an interface specification, not used in this implementation
-    def authenticate(self, request, token=None, username=None, site=None,  **kwargs):
+
+    def authenticate(self, request, token=None, username=None, site=None, **kwargs):
         """ when username is provided, assume that token is newly obtained and valid """
         if token is None or site is None:
             return
 
         if username is None:
-            user_data = get_user_data(site, token)
-            if user_data:
-                username = user_data['username']
+            code, user_data = verify_account(site, token)
+            if code == 200:
+                userid = user_data['id']
             else:
                 # aquiring user data fail means token is invalid thus auth fail
                 return None
 
         # when username is provided, assume that token is newly obtained and valid
         try:
-            user = UserModel._default_manager.get_by_natural_key(user_data['username'])
+            user = UserModel._default_manager.get(mastodon_id=userid, mastodon_site=site)
         except UserModel.DoesNotExist:
             return None
         else:
diff --git a/mastodon/decorators.py b/mastodon/decorators.py
index 3f2ec5cd..ef7a31d1 100644
--- a/mastodon/decorators.py
+++ b/mastodon/decorators.py
@@ -16,7 +16,7 @@ def mastodon_request_included(func):
                 args[0],
                 'common/error.html',
                 {
-                    'msg': _("长毛象请求超时叻_(´ཀ`」 ∠)__ ")
+                    'msg': _("联邦网络请求超时叻_(´ཀ`」 ∠)__ ")
                 }
             )
     return wrapper
diff --git a/mastodon/management/commands/wrong_sites.py b/mastodon/management/commands/wrong_sites.py
new file mode 100644
index 00000000..f985a1e7
--- /dev/null
+++ b/mastodon/management/commands/wrong_sites.py
@@ -0,0 +1,21 @@
+from django.core.management.base import BaseCommand
+from mastodon.models import MastodonApplication
+from django.conf import settings
+from mastodon.api import get_instance_info
+from users.models import User
+
+
+class Command(BaseCommand):
+    help = 'Find wrong sites'
+
+    def handle(self, *args, **options):
+        for site in MastodonApplication.objects.all():
+            d = site.domain_name
+            login_domain = d.strip().lower().split('//')[-1].split('/')[0].split('@')[-1]
+            domain, version = get_instance_info(login_domain)
+            if d != domain:
+                print(f'{d} should be {domain}')
+                for u in User.objects.filter(mastodon_site=d, is_active=True):
+                    u.mastodon_site = domain
+                    print(f'fixing {u}')
+                    u.save()
diff --git a/mastodon/models.py b/mastodon/models.py
index b6ac1207..49346810 100644
--- a/mastodon/models.py
+++ b/mastodon/models.py
@@ -1,14 +1,16 @@
 from django.db import models
 from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 
 
 class MastodonApplication(models.Model):
     domain_name = models.CharField(_('site domain name'), max_length=100, unique=True)
-    app_id = models.PositiveIntegerField(_('in-site app id'))
+    app_id = models.PositiveIntegerField(_('in-site app id'))  # TODO Remove? bc 1) it seems useless 2) GoToSocial returns a hash text id
     client_id = models.CharField(_('client id'), max_length=100)
     client_secret = models.CharField(_('client secret'), max_length=100)
-    vapid_key = models.CharField(_('vapid key'), max_length=200)
+    vapid_key = models.CharField(_('vapid key'), max_length=200, null=True, blank=True)
+    star_mode = models.PositiveIntegerField(_('0: custom emoji; 1: unicode moon; 2: text'), blank=False, default=0)
+    max_status_len = models.PositiveIntegerField(_('max toot len'), blank=False, default=500)
 
     is_proxy = models.BooleanField(default=False, blank=True)
     proxy_to = models.CharField(max_length=100, blank=True, default='')
@@ -27,7 +29,7 @@ class CrossSiteUserInfo(models.Model):
     # target site domain name
     target_site = models.CharField(_("target site domain name"), max_length=100)
     # target site id
-    site_id = models.PositiveIntegerField()
+    site_id = models.CharField(max_length=100, blank=False)
 
     class Meta:
         constraints = [
diff --git a/mastodon/utils.py b/mastodon/utils.py
index 7da7f94e..8bada43a 100644
--- a/mastodon/utils.py
+++ b/mastodon/utils.py
@@ -1,14 +1,17 @@
-from boofilsic.settings import STAR_EMPTY, STAR_HALF, STAR_SOLID
+from django.conf import settings
 
 
-def rating_to_emoji(score):
+def rating_to_emoji(score, star_mode = 0):
     """ convert score to mastodon star emoji code """
     if score is None or score == '' or score == 0:
         return ''
     solid_stars = score // 2
     half_star = int(bool(score % 2))
     empty_stars = 5 - solid_stars if not half_star else 5 - solid_stars - 1
-    emoji_code = STAR_SOLID * solid_stars + STAR_HALF * half_star + STAR_EMPTY * empty_stars
+    if star_mode == 1:
+        emoji_code = "🌕" * solid_stars + "🌗" * half_star + "🌑" * empty_stars
+    else:
+        emoji_code = settings.STAR_SOLID * solid_stars + settings.STAR_HALF * half_star + settings.STAR_EMPTY * empty_stars
     emoji_code = emoji_code.replace("::", ": :")
     emoji_code = ' ' + emoji_code + ' '
     return emoji_code
\ No newline at end of file
diff --git a/movies/admin.py b/movies/admin.py
index ca4d1b0e..bb2b1a54 100644
--- a/movies/admin.py
+++ b/movies/admin.py
@@ -1,7 +1,8 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Movie)
+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
index bda16f08..3b025da4 100644
--- a/movies/apps.py
+++ b/movies/apps.py
@@ -3,3 +3,8 @@ 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
index aa320bd5..587d92e9 100644
--- a/movies/forms.py
+++ b/movies/forms.py
@@ -1,18 +1,13 @@
 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
+from .models import Movie, MovieMark, MovieReview, MovieGenreEnum, MovieMarkStatusTranslation
 from common.models import MarkStatusEnum
 from common.forms import *
 
 
 def MovieMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在看"),
-        MarkStatusEnum.WISH.value: _("想看"),
-        MarkStatusEnum.COLLECT.value: _("看过")
-    }
-    return trans_dict[status]
+    return MovieMarkStatusTranslation[status]
 
 
 class MovieForm(forms.ModelForm):
@@ -119,11 +114,8 @@ class MovieMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'movie': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -138,14 +130,8 @@ class MovieReviewForm(ReviewForm):
             'movie',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'book': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'movie': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/movies/management/commands/fix-movie-poster.py b/movies/management/commands/fix-movie-poster.py
new file mode 100644
index 00000000..b8a35f85
--- /dev/null
+++ b/movies/management/commands/fix-movie-poster.py
@@ -0,0 +1,203 @@
+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('不存在[^<]+</title>', 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 = '<html />'
+        # 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
index be6d4ba3..ae3a9990 100644
--- a/movies/models.py
+++ b/movies/models.py
@@ -1,17 +1,27 @@
 import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, MarkStatusEnum
 from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
-from boofilsic.settings import MOVIE_MEDIA_PATH_ROOT, DEFAULT_MOVIE_IMAGE
 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, MOVIE_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.MOVIE_MEDIA_PATH_ROOT)
 
 
 class MovieGenreEnum(models.TextChoices):
@@ -47,6 +57,10 @@ class MovieGenreEnum(models.TextChoices):
     REALITY_TV = 'Reality-TV', _('真人秀')
     FAMILY = 'Family', _('家庭')
     TALK_SHOW = 'Talk-Show', _('脱口秀')
+    NEWS = 'News', _('新闻')
+    SOAP = 'Soap', _('肥皂剧')
+    TV_MOVIE = 'TV Movie', _('电视电影')
+    THEATRE = 'Theatre', _('舞台艺术')
     OTHER = 'Other', _('其他')
 
 
@@ -58,13 +72,13 @@ class Movie(Entity):
     Can either be movie or series.
     '''
     # widely recognized name, usually in Chinese
-    title = models.CharField(_("title"), max_length=200)
+    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=200)
+        _("original title"), blank=True, default='', max_length=500)
     other_title = postgres.ArrayField(
         models.CharField(_("other title"), blank=True,
-                         default='', max_length=300),
+                         default='', max_length=500),
         null=True,
         blank=True,
         default=list,
@@ -73,21 +87,21 @@ class Movie(Entity):
         blank=True, max_length=10, null=False, db_index=True, default='')
     director = postgres.ArrayField(
         models.CharField(_("director"), blank=True,
-                         default='', max_length=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
     )
     playwright = postgres.ArrayField(
         models.CharField(_("playwright"), blank=True,
-                         default='', max_length=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
     )
     actor = postgres.ArrayField(
         models.CharField(_("actor"), blank=True,
-                         default='', max_length=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
@@ -112,7 +126,7 @@ class Movie(Entity):
         default=list,
     )
     site = models.URLField(_('site url'), blank=True, default='', max_length=200)
-    
+
     # country or region
     area = postgres.ArrayField(
         models.CharField(
@@ -140,7 +154,7 @@ class Movie(Entity):
     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=DEFAULT_MOVIE_IMAGE, blank=True)
+    cover = models.ImageField(_("poster"), upload_to=movie_cover_path, default=settings.DEFAULT_MOVIE_IMAGE, blank=True)
 
     ############################################
     # exclusive fields to series
@@ -157,27 +171,67 @@ class Movie(Entity):
     ############################################
     is_series = models.BooleanField(default=False)
 
+    history = HistoricalRecords()
 
     def __str__(self):
         if self.year:
-            return self.title + f"({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:
@@ -185,23 +239,45 @@ class Movie(Entity):
         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 settings.APP_WEBSITE + 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)
@@ -211,3 +287,7 @@ class MovieTag(Tag):
             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
index f55c026b..497c9bce 100644
--- a/movies/templates/movies/create_update.html
+++ b/movies/templates/movies/create_update.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -21,10 +21,25 @@
             {% include "partial/_navbar.html" %}
 
             <section id="content" class="container">
-                <div class="grid">
+                <div class="grid" class="single-section-wrapper">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'movies:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'movies:scrape' %}"
-                            class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                   {% comment %} <a href="{% url 'movies:scrape' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -53,12 +68,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/movies/templates/movies/create_update_review.html b/movies/templates/movies/create_update_review.html
index 1f4e5b34..87046277 100644
--- a/movies/templates/movies/create_update_review.html
+++ b/movies/templates/movies/create_update_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -61,7 +61,7 @@
                                 {% if movie.director|length > 5 %}
                                 <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                 <script>
-                                    $("#directorMore").click(function (e) {
+                                    $("#directorMore").on('click', function (e) {
                                         $("span.director:not(:visible)").each(function (e) {
                                             $(this).parent().removeAttr('style');
                                         });
@@ -89,7 +89,7 @@
                                     {% if movie.actor|length > 5 %}
                                     <a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
                                     <script>
-                                        $("#actorMore").click(function (e) {
+                                        $("#actorMore").on('click', function (e) {
                                             $("span.actor:not(:visible)").each(function (e) {
                                                 $(this).parent().removeAttr('style');
                                             });
@@ -138,7 +138,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -158,12 +158,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/movies/templates/movies/delete.html b/movies/templates/movies/delete.html
index 5aa651ac..94dfb2ce 100644
--- a/movies/templates/movies/delete.html
+++ b/movies/templates/movies/delete.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除电影/剧集' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -62,7 +62,7 @@
                                 {% if movie.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' movie.last_editor.id %}">
+                                    <a href="{% url 'users:home' movie.last_editor.mastodon_username %}">
                                         <span>{{ movie.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -96,12 +96,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/movies/templates/movies/delete_review.html b/movies/templates/movies/delete_review.html
index a16e6c49..db3d3fe0 100644
--- a/movies/templates/movies/delete_review.html
+++ b/movies/templates/movies/delete_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html
index 8dbb4a09..72788ad0 100644
--- a/movies/templates/movies/detail.html
+++ b/movies/templates/movies/detail.html
@@ -14,12 +14,12 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB电影 - {{ movie.title }}">
+    <meta property="og:title" content="{{ site_name }}电影 - {{ movie.title }}">
     <meta property="og:type" content="video.movie">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ movie.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ movie.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ movie.brief }}">
     <!-- 
     video:actor - profile array - Actors in the movie.
     video:actor:role - string - The role they played.
@@ -31,17 +31,15 @@
     -->
     
     {% if movie.is_series %}
-    <title>{% trans 'NiceDB - 剧集详情' %} | {{ movie.title }}</title>
+    <title>{{ site_name }} - {% trans '剧集详情' %} | {{ movie.title }}</title>
     {% else %}
-    <title>{% trans 'NiceDB - 电影详情' %} | {{ movie.title }}</title>
+    <title>{{ site_name }} - {% trans '电影详情' %} | {{ movie.title }}</title>
     {% endif %}
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -78,11 +76,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if movie.rating %}
+                                            {% if movie.rating and movie.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ movie.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ movie.rating }} </span>
+                                            <small>({{ movie.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if movie.imdb_code %}
@@ -99,7 +98,7 @@
                                             {% if movie.director|length > 5 %}
                                             <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#directorMore").click(function (e) {
+                                                $("#directorMore").on('click', function (e) {
                                                     $("span.director:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -118,7 +117,7 @@
                                             {% if movie.playwright|length > 5 %}
                                             <a href="javascript:void(0);" id="playwrightMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#playwrightMore").click(function (e) {
+                                                $("#playwrightMore").on('click', function (e) {
                                                     $("span.playwright:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -138,7 +137,7 @@
                                             {% if movie.actor|length > 5 %}
                                                 <a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
                                                 <script>
-                                                    $("#actorMore").click(function(e) {
+                                                    $("#actorMore").on('click', function(e) {
                                                         $("span.actor:not(:visible)").each(function(e){
                                                             $(this).parent().removeAttr('style');
                                                         });
@@ -197,7 +196,7 @@
                                         
                                     
                                         {% if movie.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.id %}">{{ movie.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.mastodon_username %}">{{ movie.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -249,30 +248,10 @@
                                 <h5 class="entity-marks__title">{% trans '这部电影的标记' %}</h5>
                                 {% endif %}
                                 {% if mark_list_more %}
-                                <a href="{% url 'movies:retrieve_mark_list' movie.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'movies:retrieve_mark_list' movie.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'movies:retrieve_mark_list' movie.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=movie %}    
                             </div>
                             <div class="entity-reviews">
                                 {% if movie.is_series %}
@@ -282,17 +261,21 @@
                                 {% endif %}
 
                                 {% if review_list_more %}
-                                <a href="{% url 'movies:retrieve_review_list' movie.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'movies:retrieve_review_list' movie.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
+                                    {% if others_review.movie != movie %}
+                                    <span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'movies:retrieve' others_review.movie.id %}">{{ others_review.movie.get_source_site_display }}</a></span>
+                                    {% endif %}
+
                                     <span class="entity-reviews__review-title"> <a href="{% url 'movies:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
                                     <span>{{ others_review.get_plain_content | truncate:100 }}</span>
                                 </li>
@@ -317,7 +300,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -329,7 +312,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -365,7 +348,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -394,7 +377,42 @@
 
                             {% endif %}
                         </div>
-                                
+
+                        {% if movie.get_related_movies.count > 0 %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关条目' %}</div>
+                                <div >
+                                    {% for m in movie.get_related_movies %}
+                                    <p>
+                                        <a href="{% url 'movies:retrieve' m.id %}">{{ m.title }}</a>
+                                        {% if movie.source_site != m.source_site %}
+                                        <span class="source-label source-label__{{ m.source_site }}">{{ m.get_source_site_display }}</span>
+                                        {% endif %}
+                                    </p>
+                                    {% endfor %}
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'movie' movie.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
                     </div>
                 </div>
             </section>
@@ -464,8 +482,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/movies/templates/movies/mark_list.html b/movies/templates/movies/mark_list.html
index 23e57b28..27cc0e1e 100644
--- a/movies/templates/movies/mark_list.html
+++ b/movies/templates/movies/mark_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ movie.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ movie.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,37 +35,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'movies:retrieve' movie.id %}">{{ movie.title }}</a>{% trans ' 的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-        
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                    </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-                                        
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=movie %}
                             </div>
                             <div class="pagination">
                             
@@ -129,7 +99,7 @@
                                 {% if movie.director|length > 5 %}
                                 <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                 <script>
-                                    $("#directorMore").click(function (e) {
+                                    $("#directorMore").on('click', function (e) {
                                         $("span.director:not(:visible)").each(function (e) {
                                             $(this).parent().removeAttr('style');
                                         });
@@ -168,12 +138,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
   
diff --git a/movies/templates/movies/review_detail.html b/movies/templates/movies/review_detail.html
index b52c7b54..4a83227e 100644
--- a/movies/templates/movies/review_detail.html
+++ b/movies/templates/movies/review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB影评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}影评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ movie.cover|thumb:'normal' }}">
+    <title>{{ site_name }}影评 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -73,6 +74,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -108,7 +110,7 @@
                                     {% if movie.director|length > 5 %}
                                     <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                     <script>
-                                        $("#directorMore").click(function (e) {
+                                        $("#directorMore").on('click', function (e) {
                                             $("span.director:not(:visible)").each(function (e) {
                                                 $(this).parent().removeAttr('style');
                                             });
@@ -148,16 +150,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/movies/templates/movies/review_list.html b/movies/templates/movies/review_list.html
index ed32cfe0..daedf296 100644
--- a/movies/templates/movies/review_list.html
+++ b/movies/templates/movies/review_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ movie.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ movie.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -41,11 +41,14 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
+                                            {% if review.movie != movie %}
+                                            <span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'movies:retrieve' review.movie.id %}">{{ review.movie.get_source_site_display }}</a></span>
+                                            {% endif %}
                                         
         
                                         <span href="{% url 'movies:retrieve_review' review.id %}" class="entity-reviews__review-title"><a href="{% url 'movies:retrieve_review' review.id %}">{{ review.title }}</a></span>
@@ -116,7 +119,7 @@
                                     {% if movie.director|length > 5 %}
                                     <a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
                                     <script>
-                                        $("#directorMore").click(function (e) {
+                                        $("#directorMore").on('click', function (e) {
                                             $("span.director:not(:visible)").each(function (e) {
                                                 $(this).parent().removeAttr('style');
                                             });
@@ -156,12 +159,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/movies/templates/movies/scrape.html b/movies/templates/movies/scrape.html
index 91cd90c9..0387cc10 100644
--- a/movies/templates/movies/scrape.html
+++ b/movies/templates/movies/scrape.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/movies/urls.py b/movies/urls.py
index 9f359e9e..0495a9f0 100644
--- a/movies/urls.py
+++ b/movies/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -8,8 +8,10 @@ urlpatterns = [
     path('<int:id>/', retrieve, name='retrieve'),
     path('update/<int:id>/', update, name='update'),
     path('delete/<int:id>/', delete, name='delete'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('mark/', create_update_mark, name='create_update_mark'),
-    path('<int:movie_id>/mark/list/', retrieve_mark_list, name='retrieve_mark_list'),
+    path('wish/<int:id>/', wish, name='wish'),
+    re_path('(?P<movie_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_mark_list, name='retrieve_mark_list'),
     path('mark/delete/<int:id>/', delete_mark, name='delete_mark'),
     path('<int:movie_id>/review/create/', create_review, name='create_review'),
     path('review/update/<int:id>/', update_review, name='update_review'),
diff --git a/movies/views.py b/movies/views.py
index f63cdeef..d4fc21f5 100644
--- a/movies/views.py
+++ b/movies/views.py
@@ -2,21 +2,23 @@ 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
+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.api import check_visibility, post_toot, TootVisibilityEnum
-from mastodon.utils import rating_to_emoji
+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
+from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
 from common.models import SourceSiteEnum
 from .models import *
 from .forms import *
-from boofilsic.settings import MASTODON_TAGS
+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__)
@@ -87,6 +89,18 @@ def create(request):
         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':
@@ -98,6 +112,7 @@ def update(request, id):
             'movies/create_update.html',
             {
                 'form': form,
+                'is_update': True,
                 'title': page_title,
                 'submit_url': reverse("movies:update", args=[movie.id]),
                 # provided for frontend js
@@ -127,6 +142,7 @@ def update(request, id):
                 'movies/create_update.html',
                 {
                     'form': form,
+                    'is_update': True,
                     'title': page_title,
                     'submit_url': reverse("movies:update", args=[movie.id]),
                     # provided for frontend js
@@ -167,6 +183,7 @@ def retrieve(request, id):
         else:
             mark_form = MovieMarkForm(initial={
                 'movie': movie,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -185,10 +202,8 @@ def retrieve(request, id):
             mark_list_more = None
             review_list_more = None
         else:
-            mark_list = MovieMark.get_available(
-                movie, request.user, request.session['oauth_token'])
-            review_list = MovieReview.get_available(
-                movie, request.user, request.session['oauth_token'])
+            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:
@@ -196,6 +211,7 @@ def retrieve(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(movie=movie)))
 
         # def strip_html_tags(text):
         #     import re
@@ -220,6 +236,7 @@ def retrieve(request, id):
                 'review_list_more': review_list_more,
                 'movie_tag_list': movie_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -264,12 +281,19 @@ def create_update_mark(request):
         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:
@@ -277,7 +301,7 @@ def create_update_mark(request):
             form = MovieMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -305,28 +329,10 @@ def create_update_mark(request):
                 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("movies:retrieve",
-                                                                args=[movie.id])
-                words = MovieMarkStatusTranslator(form.cleaned_data['status']) +\
-                    f"《{movie.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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("movies:retrieve", args=[form.instance.movie.id]))
     else:
@@ -335,11 +341,30 @@ def create_update_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_mark_list(request, movie_id):
+def wish(request, id):
+    if request.method == 'POST':
+        movie = get_object_or_404(Movie, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            '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(
-            movie, request.user, request.session['oauth_token'])
+        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)
@@ -400,23 +425,8 @@ def create_review(request, movie_id):
             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("movies:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.movie.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("movies:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -452,23 +462,8 @@ def update_review(request, id):
             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("movies:retrieve_review",
-                                                                args=[form.instance.id])
-                words = "发布了关于" + f"《{form.instance.movie.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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("movies:retrieve_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -503,11 +498,10 @@ def delete_review(request, id):
 
 
 @mastodon_request_included
-@login_required
 def retrieve_review(request, id):
     if request.method == 'GET':
         review = get_object_or_404(MovieReview, pk=id)
-        if not check_visibility(review, request.session['oauth_token'], request.user):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -542,8 +536,7 @@ def retrieve_review(request, id):
 def retrieve_review_list(request, movie_id):
     if request.method == 'GET':
         movie = get_object_or_404(Movie, pk=movie_id)
-        queryset = MovieReview.get_available(
-            movie, request.user, request.session['oauth_token'])
+        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)
diff --git a/music/admin.py b/music/admin.py
index eb770458..33eb056d 100644
--- a/music/admin.py
+++ b/music/admin.py
@@ -1,11 +1,12 @@
 from django.contrib import admin
 from .models import *
+from simple_history.admin import SimpleHistoryAdmin
 
-admin.site.register(Song)
+admin.site.register(Song, SimpleHistoryAdmin)
 admin.site.register(SongMark)
 admin.site.register(SongReview)
 admin.site.register(SongTag)
-admin.site.register(Album)
+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
index d909c7fb..6fb97b37 100644
--- a/music/apps.py
+++ b/music/apps.py
@@ -3,3 +3,9 @@ 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
index 3eec5646..9e487592 100644
--- a/music/forms.py
+++ b/music/forms.py
@@ -7,12 +7,7 @@ from common.forms import *
 
 
 def MusicMarkStatusTranslator(status):
-    trans_dict = {
-        MarkStatusEnum.DO.value: _("在听"),
-        MarkStatusEnum.WISH.value: _("想听"),
-        MarkStatusEnum.COLLECT.value: _("听过")
-    }
-    return trans_dict[status]
+    return MusicMarkStatusTranslation[status]
 
 
 class SongForm(forms.ModelForm):
@@ -65,11 +60,8 @@ class SongMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'song': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -84,14 +76,8 @@ class SongReviewForm(ReviewForm):
             'song',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'song': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'song': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -148,11 +134,8 @@ class AlbumMarkForm(MarkForm):
             'status',
             'rating',
             'text',
-            'is_private',
+            'visibility',
         ]
-        labels = {
-            'rating': _("评分"),
-        }
         widgets = {
             'album': forms.TextInput(attrs={"hidden": ""}),
         }
@@ -167,14 +150,8 @@ class AlbumReviewForm(ReviewForm):
             'album',
             'title',
             'content',
-            'is_private'
+            'visibility'
         ]
-        labels = {
-            'album': "",
-            'title': _("标题"),
-            'content': _("正文"),
-            'share_to_mastodon': _("分享到长毛象")
-        }
         widgets = {
             'album': forms.TextInput(attrs={"hidden": ""}),
         }
diff --git a/music/management/commands/fix-album-cover.py b/music/management/commands/fix-album-cover.py
new file mode 100644
index 00000000..9b9f4d3e
--- /dev/null
+++ b/music/management/commands/fix-album-cover.py
@@ -0,0 +1,199 @@
+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('不存在[^<]+</title>', 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.scraperapi.com?api_key={settings.SCRAPERAPI_KEY}&url={url}', 60)
+                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 = '<html />'
+        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}'
+            # 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
index c738db5b..131f9a14 100644
--- a/music/models.py
+++ b/music/models.py
@@ -1,21 +1,29 @@
 import uuid
 import django.contrib.postgres.fields as postgres
-from django.utils.translation import ugettext_lazy as _
+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
+from common.models import Entity, Mark, Review, Tag, SourceSiteEnum, MarkStatusEnum
 from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
-from boofilsic.settings import SONG_MEDIA_PATH_ROOT, DEFAULT_SONG_IMAGE, ALBUM_MEDIA_PATH_ROOT, DEFAULT_ALBUM_IMAGE
 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, SONG_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.SONG_MEDIA_PATH_ROOT)
 
 
 def album_cover_path(instance, filename):
-    return GenerateDateUUIDMediaFilePath(instance, filename, ALBUM_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.ALBUM_MEDIA_PATH_ROOT)
 
 
 class Album(Entity):
@@ -23,11 +31,11 @@ class Album(Entity):
     release_date = models.DateField(
         _('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
     cover = models.ImageField(
-        _("封面"), upload_to=album_cover_path, default=DEFAULT_ALBUM_IMAGE, blank=True)
+        _("封面"), 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=100),
+                         default='', max_length=200),
         null=True,
         blank=True,
         default=list,
@@ -45,12 +53,36 @@ class Album(Entity):
     )
     track_list = models.TextField(_("曲目"), blank=True, default="")
 
+    history = HistoricalRecords()
+
     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
 
@@ -58,6 +90,14 @@ class Album(Entity):
     def verbose_category_name(self):
         return _("专辑")
 
+    @property
+    def mark_class(self):
+        return AlbumMark
+
+    @property
+    def tag_class(self):
+        return AlbumTag
+
 
 class Song(Entity):
     '''
@@ -70,7 +110,7 @@ class Song(Entity):
     # duration in ms
     duration = models.PositiveIntegerField(_("时长"), null=True, blank=True)
     cover = models.ImageField(
-        _("封面"), upload_to=song_cover_path, default=DEFAULT_SONG_IMAGE, blank=True)
+        _("封面"), upload_to=song_cover_path, default=settings.DEFAULT_SONG_IMAGE, blank=True)
     artist = postgres.ArrayField(
         models.CharField(blank=True,
                          default='', max_length=100),
@@ -84,19 +124,46 @@ class Song(Entity):
     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)
@@ -107,6 +174,10 @@ class SongMark(Mark):
                 fields=['owner', 'song'], name='unique_song_mark')
         ]
 
+    @property
+    def translated_status(self):
+        return MusicMarkStatusTranslation[self.status]
+
 
 class SongReview(Review):
     song = models.ForeignKey(
@@ -118,6 +189,14 @@ class SongReview(Review):
                 fields=['owner', 'song'], name='unique_song_review')
         ]
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + reverse("music:retrieve_song_review", args=[self.id])
+
+    @property
+    def item(self):
+        return self.song
+
 
 class SongTag(Tag):
     song = models.ForeignKey(
@@ -131,6 +210,10 @@ class SongTag(Tag):
                 fields=['content', 'mark'], name="unique_songmark_tag")
         ]
 
+    @property
+    def item(self):
+        return self.song
+
 
 class AlbumMark(Mark):
     album = models.ForeignKey(
@@ -142,6 +225,10 @@ class AlbumMark(Mark):
                 fields=['owner', 'album'], name='unique_album_mark')
         ]
 
+    @property
+    def translated_status(self):
+        return MusicMarkStatusTranslation[self.status]
+
 
 class AlbumReview(Review):
     album = models.ForeignKey(
@@ -153,6 +240,14 @@ class AlbumReview(Review):
                 fields=['owner', 'album'], name='unique_album_review')
         ]
 
+    @property
+    def url(self):
+        return settings.APP_WEBSITE + reverse("music:retrieve_album_review", args=[self.id])
+
+    @property
+    def item(self):
+        return self.album
+
 
 class AlbumTag(Tag):
     album = models.ForeignKey(
@@ -165,3 +260,7 @@ class AlbumTag(Tag):
             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
index 68fe1143..7de3d0ae 100644
--- a/music/templates/music/album_detail.html
+++ b/music/templates/music/album_detail.html
@@ -6,6 +6,7 @@
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load strip_scheme %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -13,21 +14,19 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB音乐 - {{ album.title }}">
+    <meta property="og:title" content="{{ site_name }}音乐 - {{ album.title }}">
     <meta property="og:type" content="music.album">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ album.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ album.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ album.brief }}">
 
-    <title>{% trans 'NiceDB - 音乐详情' %} | {{ album.title }}</title>
+    <title>{{ site_name }} - {% trans '音乐详情' %} | {{ album.title }}</title>
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -55,11 +54,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if album.rating %}
+                                            {% if album.rating and album.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ album.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ album.rating }} </span>
+                                            <small>({{ album.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if album.artist %}{% trans '艺术家:' %}
@@ -72,7 +72,7 @@
                                             {% if album.artist|length > 5 %}
                                             <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#artistMore").click(function (e) {
+                                                $("#artistMore").on('click', function (e) {
                                                     $("span.artist:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -91,7 +91,7 @@
                                             {% if album.company|length > 5 %}
                                             <a href="javascript:void(0);" id="companyMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#companyMore").click(function (e) {
+                                                $("#companyMore").on('click', function (e) {
                                                     $("span.company:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -126,7 +126,7 @@
                                         
                                     
                                         {% if album.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.id %}">{{ album.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.mastodon_username %}">{{ album.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -213,43 +213,23 @@
                                 
                                 <h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
                                 {% if mark_list_more %}
-                                <a href="{% url 'music:retrieve_album_mark_list' album.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'music:retrieve_album_mark_list' album.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'music:retrieve_album_mark_list' album.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=album %}    
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
 
                                 {% if review_list_more %}
-                                <a href="{% url 'music:retrieve_album_review_list' album.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'music:retrieve_album_review_list' album.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
@@ -277,7 +257,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -289,7 +269,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -320,7 +300,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -349,7 +329,28 @@
 
                             {% endif %}
                         </div>
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'album' album.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
                                 
+                        {% if album.get_embed_link %}
+                        <iframe src="{{ album.get_embed_link }}"  height="320" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
+                        {% endif %}
                     </div>
                 </div>
             </section>
@@ -411,8 +412,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/music/templates/music/album_mark_list.html b/music/templates/music/album_mark_list.html
index cc9c7821..49e4b670 100644
--- a/music/templates/music/album_mark_list.html
+++ b/music/templates/music/album_mark_list.html
@@ -14,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ album.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ album.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -35,38 +35,7 @@
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
                                     <a href="{% url 'music:retrieve_album' album.id %}">{{ album.title }}</a>{% trans '的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-
-                                    {% for mark in marks %}
-
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
-                                                viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=album %}
                             </div>
                             <div class="pagination">
 
@@ -124,7 +93,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -154,12 +123,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/album_review_detail.html b/music/templates/music/album_review_detail.html
index fb3b7b5a..3e6e8fdd 100644
--- a/music/templates/music/album_review_detail.html
+++ b/music/templates/music/album_review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}乐评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ album.cover|thumb:'normal' }}">
+    <title>{{ site_name }}乐评 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -77,6 +78,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
 
@@ -109,7 +111,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -139,16 +141,8 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/music/templates/music/album_review_list.html b/music/templates/music/album_review_list.html
index a4e4f04a..40ce5b6d 100644
--- a/music/templates/music/album_review_list.html
+++ b/music/templates/music/album_review_list.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ album.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ album.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -40,9 +40,9 @@
 
                                     <li class="entity-reviews__review entity-reviews__review--wider">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                        {% if review.is_private %}
+                                        {% if review.visibility > 0 %}
                                         <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                                 viewBox="0 0 20 20">
                                                     <path
@@ -120,7 +120,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -150,12 +150,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/create_update_album.html b/music/templates/music/create_update_album.html
index 841eab79..111c60a1 100644
--- a/music/templates/music/create_update_album.html
+++ b/music/templates/music/create_update_album.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -22,9 +22,24 @@
 
             <section id="content" class="container">
                 <div class="grid">
+                    {% if is_update and form.source_site.value != 'in-site' %}
+                    <div style="float:right;padding-left:16px">
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '源网站' %}: <a href="{{ form.source_url.value }}">{{ form.source_site.value }}</a></div>
+                                <div class="action-panel__button-group">
+                                    <form method="post" action="{% url 'music:rescrape' form.id.value %}">
+                                        {% csrf_token %}
+                                        <input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
+                                    </form>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    {% endif %}
+
                     <div class="single-section-wrapper" id="main">
-                        <a href="{% url 'music:scrape_album' %}"
-                            class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a>
+                        {% comment %} <a href="{% url 'music:scrape_album' %}" class="single-section-wrapper__link single-section-wrapper__link--secondary">{% trans '>>> 试试一键剽取~ <<<' %}</a> {% endcomment %}
                         <form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
                             {% csrf_token %}
                             {{ form.media }}
@@ -54,12 +69,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/music/templates/music/create_update_album_review.html b/music/templates/music/create_update_album_review.html
index db1f11f8..440bce52 100644
--- a/music/templates/music/create_update_album_review.html
+++ b/music/templates/music/create_update_album_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -52,7 +52,7 @@
                                         {% if album.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -96,7 +96,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -116,12 +116,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html
index a694c891..4a4e8b49 100644
--- a/music/templates/music/create_update_song.html
+++ b/music/templates/music/create_update_song.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
@@ -57,12 +57,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
         // mark required
diff --git a/music/templates/music/create_update_song_review.html b/music/templates/music/create_update_song_review.html
index 03e8271f..ec74d0bb 100644
--- a/music/templates/music/create_update_song_review.html
+++ b/music/templates/music/create_update_song_review.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -52,7 +52,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -100,7 +100,7 @@
                                 <div class="review-form__option">
                                     <div class="review-form__visibility-radio">
 
-                                        {{ form.is_private.label }}{{ form.is_private }}
+                                        {{ form.visibility.label }}{{ form.visibility }}
                                     </div>
                                     <div class="review-form__share-checkbox">
                                         {{ form.share_to_mastodon }}{{ form.share_to_mastodon.label }}
@@ -120,12 +120,6 @@
         {% include "partial/_footer.html" %}
     </div>
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/delete_album.html b/music/templates/music/delete_album.html
index bbc15964..8539d736 100644
--- a/music/templates/music/delete_album.html
+++ b/music/templates/music/delete_album.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除音乐' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除音乐' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if album.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' album.last_editor.id %}">
+                                    <a href="{% url 'users:home' album.last_editor.mastodon_username %}">
                                         <span>{{ album.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html
index 7deaaa49..b67d065e 100644
--- a/music/templates/music/delete_album_review.html
+++ b/music/templates/music/delete_album_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/delete_song.html b/music/templates/music/delete_song.html
index 4d3825af..1be0112c 100644
--- a/music/templates/music/delete_song.html
+++ b/music/templates/music/delete_song.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除音乐' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除音乐' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -55,7 +55,7 @@
                                 {% if song.last_editor %}
                                 <div>
                                     {% trans '最近编辑者:' %}
-                                    <a href="{% url 'users:home' song.last_editor.id %}">
+                                    <a href="{% url 'users:home' song.last_editor.mastodon_username %}">
                                         <span>{{ song.last_editor | default:"" }}</span>
                                     </a>
                                 </div>
@@ -89,12 +89,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html
index 9e515b02..d2d610f3 100644
--- a/music/templates/music/delete_song_review.html
+++ b/music/templates/music/delete_song_review.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 删除评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '删除评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
@@ -35,7 +35,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
                                         viewBox="0 0 20 20">
                                             <path
@@ -46,7 +46,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
 
-                                        <a href="{% url 'users:home' review.owner.id %}"
+                                        <a href="{% url 'users:home' review.owner.mastodon_username %}"
                                             class="review-head__owner-link">{{ review.owner.username }}</a>
 
                                         {% if mark %}
@@ -89,12 +89,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html
index 8079aa9e..b4fc9aba 100644
--- a/music/templates/music/scrape_album.html
+++ b/music/templates/music/scrape_album.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/templates/music/scrape_song.html b/music/templates/music/scrape_song.html
index e2cd4730..2ffa1b3b 100644
--- a/music/templates/music/scrape_song.html
+++ b/music/templates/music/scrape_song.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 从豆瓣获取数据' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html
index 6aa9b87b..48a8c7be 100644
--- a/music/templates/music/song_detail.html
+++ b/music/templates/music/song_detail.html
@@ -6,6 +6,7 @@
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load strip_scheme %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -13,21 +14,19 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB音乐 - {{ song.title }}">
+    <meta property="og:title" content="{{ site_name }}音乐 - {{ song.title }}">
     <meta property="og:type" content="music.song">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ song.cover.url }}">
-    <meta property="og:site_name" content="NiceDB">
-    <meta property="og:description"content="{{ song.brief }}">
+    <meta property="og:site_name" content="{{ site_name }}">
+    <meta property="og:description" content="{{ song.brief }}">
 
-    <title>{% trans 'NiceDB - 音乐详情' %} | {{ song.title }}</title>
+    <title>{{ site_name }} - {% trans '音乐详情' %} | {{ song.title }}</title>
         
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/detail.js' %}"></script>    
-    <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-
 </head>
 
 <body>
@@ -54,11 +53,12 @@
                                     
                                     <div class="entity-detail__fields">
                                         <div class="entity-detail__rating">
-                                            {% if song.rating %}
+                                            {% if song.rating and song.rating_number >= 5 %}
                                             <span class="entity-detail__rating-star rating-star" data-rating-score="{{ song.rating | floatformat:"0" }}"></span>
                                             <span class="entity-detail__rating-score"> {{ song.rating }} </span>
+                                            <small>({{ song.rating_number }}人评分)</small>
                                             {% else %}
-                                            <span> {% trans '评分:暂无评分' %}</span>
+                                            <span> {% trans '评分:评分人数不足' %}</span>
                                             {% endif %}
                                         </div>
                                         <div>{% if song.artist %}{% trans '艺术家:' %}
@@ -71,7 +71,7 @@
                                             {% if song.artist|length > 5 %}
                                             <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                             <script>
-                                                $("#artistMore").click(function (e) {
+                                                $("#artistMore").on('click', function (e) {
                                                     $("span.artist:not(:visible)").each(function (e) {
                                                         $(this).parent().removeAttr('style');
                                                     });
@@ -114,7 +114,7 @@
                                         
                                     
                                         {% if song.last_editor %}
-                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.id %}">{{ song.last_editor | default:"" }}</a></div>
+                                        <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.mastodon_username %}">{{ song.last_editor | default:"" }}</a></div>
                                         {% endif %}
 
                                         <div>
@@ -159,43 +159,23 @@
                                 
                                 <h5 class="entity-marks__title">{% trans '这部作品的标记' %}</h5>
                                 {% if mark_list_more %}
-                                <a href="{% url 'music:retrieve_song_mark_list' song.id %}" class="entity-marks__more-link">{% trans '更多' %}</a>
-                                {% endif %}
-                                {% if mark_list %}
-                                <ul class="entity-marks__mark-list">                                    
-                                {% for others_mark in mark_list %}
-                                <li class="entity-marks__mark">
-                                    <a href="{% url 'users:home' others_mark.owner.id %}" class="entity-marks__owner-link">{{ others_mark.owner.username }}</a>
-                                    <span>{{ others_mark.get_status_display }}</span>
-                                    {% if others_mark.rating %}
-                                    <span class="entity-marks__rating-star rating-star" data-rating-score="{{ others_mark.rating | floatformat:"0" }}"></span>
-                                    {% endif %}
-                                    {% if others_mark.is_private %}
-                                    <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
-                                    {% endif %}
-                                    <span class="entity-marks__mark-time">{{ others_mark.edited_time }}</span>
-                                    {% if others_mark.text %}
-                                    <p class="entity-marks__mark-content">{{ others_mark.text }}</p>
-                                    {% endif %}
-                                </li>
-                                {% endfor %}
-                                </ul>
-                                {% else %}
-                                <div>{% trans '暂无标记' %}</div>    
+                                <a href="{% url 'music:retrieve_song_mark_list' song.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
                                 {% endif %}
+                                <a href="{% url 'music:retrieve_song_mark_list' song.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
+                                {% include "partial/mark_list.html" with mark_list=mark_list current_item=song %}    
                             </div>
                             <div class="entity-reviews">
                                 <h5 class="entity-reviews__title">{% trans '这部作品的评论' %}</h5>
 
                                 {% if review_list_more %}
-                                <a href="{% url 'music:retrieve_song_review_list' song.id %}" class="entity-reviews__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'music:retrieve_song_review_list' song.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
                                 {% endif %}
                                 {% if review_list %}
                                 <ul class="entity-reviews__review-list">
                                 {% for others_review in review_list %}
                                 <li class="entity-reviews__review">
-                                    <a href="{% url 'users:home' others_review.owner.id %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
-                                    {% if others_review.is_private %}
+                                    <a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
+                                    {% if others_review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                     {% endif %}
                                     <span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
@@ -223,7 +203,7 @@
                                     <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
                                     {% endif %}
                                 {% endif %}
-                                {% if mark.is_private %}
+                                {% if mark.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                 {% endif %}                                        
                                 <span class="mark-panel__actions">
@@ -235,7 +215,7 @@
                                 </span>
                                 <div class="mark-panel__clear"></div>
 
-                                <div class="mark-panel__time">{{ mark.edited_time }}</div>
+                                <div class="mark-panel__time">{{ mark.created_time }}</div>
 
                                 {% if mark.text %}
                                 <p class="mark-panel__text">{{ mark.text }}</p>
@@ -266,7 +246,7 @@
                             <div class="review-panel">
    
                                 <span class="review-panel__label">{% trans '我的评论' %}</span>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                     <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>                                      
                                 {% endif %}
 
@@ -295,7 +275,28 @@
 
                             {% endif %}
                         </div>
-                                
+
+                        {% if collection_list %}
+                        <div class="aside-section-wrapper">
+                            <div class="action-panel">
+                                <div class="action-panel__label">{% trans '相关收藏单' %}</div>
+                                <div >
+                                    {% for c in collection_list %}
+                                    <p>
+                                        <a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
+                                    </p>
+                                    {% endfor %}
+                                    <div class="action-panel__button-group action-panel__button-group--center">
+                                        <button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'song' song.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        {% if song.source_site == "spotify" %}
+                        <iframe src="{{ song.get_embed_link }}"  height="80" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
+                        {% endif %}
                     </div>
                 </div>
             </section>
@@ -357,8 +358,8 @@
                     
                     <div class="mark-modal__option">
                         <div class="mark-modal__visibility-radio">
-                            <span>{{ mark_form.is_private.label }}:</span>
-                            {{ mark_form.is_private }}
+                            <span>{{ mark_form.visibility.label }}:</span>
+                            {{ mark_form.visibility }}
                         </div>
                         <div class="mark-modal__share-checkbox">
                             {{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
diff --git a/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html
index 6314df96..a5fcf18b 100644
--- a/music/templates/music/song_mark_list.html
+++ b/music/templates/music/song_mark_list.html
@@ -6,6 +6,7 @@
 {% load mastodon %}
 {% load oauth_token %}
 {% load truncate %}
+{% load highlight %}
 {% load thumb %}
 <!DOCTYPE html>
 <html lang="en">
@@ -13,8 +14,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的标记' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ song.title }}{% trans '的标记' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
@@ -32,41 +33,9 @@
                         <div class="main-section-wrapper">
                             <div class="entity-marks">
                                 <h5 class="entity-marks__title entity-marks__title--stand-alone">
-                                    <a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans '
-                                    的标记' %}
+                                    <a href="{% url 'music:retrieve_song' song.id %}">{{ song.title }}</a>{% trans '的标记' %}
                                 </h5>
-                                <ul class="entity-marks__mark-list">
-
-                                    {% for mark in marks %}
-
-                                    <li class="entity-marks__mark entity-marks__mark--wider">
-                                        <a href="{% url 'users:home' mark.owner.id %}"
-                                            class="entity-marks__owner-link">{{ mark.owner.username }}</a>
-                                        <span>{{ mark.get_status_display }}</span>
-                                        {% if mark.rating %}
-                                        <span class="entity-marks__rating-star rating-star"
-                                            data-rating-score="{{ mark.rating | floatformat:" 0" }}"></span>
-                                        {% endif %}
-                                        {% if mark.is_private %}
-                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
-                                                viewBox="0 0 20 20">
-                                                    <path
-                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                </svg></span>
-                                        {% endif %}
-                                        <span class="entity-marks__mark-time">{{ mark.edited_time }}</span>
-                                        {% if mark.text %}
-                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                        {% endif %}
-                                    </li>
-
-                                    {% empty %}
-                                    <div>
-                                        {% trans '无结果' %}
-                                    </div>
-                                    {% endfor %}
-
-                                </ul>
+                                {% include "partial/mark_list.html" with mark_list=marks current_item=song %}
                             </div>
                             <div class="pagination">
 
@@ -110,8 +79,7 @@
                                             {{ song.title }}
                                         </a>
                                         <a href="{{ song.source_url }}"><span
-                                                class="source-label source-label__{{ song.source_site }}">{{
-                                                song.get_source_site_display }}</span></a>
+                                                class="source-label source-label__{{ song.source_site }}">{{song.get_source_site_display }}</span></a>
                                     </h5>
 
                                     <div>{% if song.artist %}{% trans '艺术家:' %}
@@ -124,7 +92,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -159,12 +127,6 @@
     </div>
 
 
-    {% comment %}
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon request.user.mastodon_site %}</div>
-    <!--current user mastodon id-->
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
 
     <script>
 
diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html
index ee7c2704..e261fa91 100644
--- a/music/templates/music/song_review_detail.html
+++ b/music/templates/music/song_review_detail.html
@@ -13,17 +13,18 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="NiceDB乐评 - {{ review.title }}">
+    <meta property="og:title" content="{{ site_name }}乐评 - {{ review.title }}">
     <meta property="og:type" content="article">
     <meta property="og:article:author" content="{{ review.owner.username }}">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
-    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.svg' %}">
-    <title>{% trans 'NiceDB - 评论详情' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <meta property="og:image" content="{{ song.cover|thumb:'normal' }}">
+    <title>{{ site_name }}乐评 - {{ review.title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
 </head>
 
 <body>
@@ -39,7 +40,7 @@
                                 <h5 class="review-head__title">
                                     {{ review.title }}
                                 </h5>
-                                {% if review.is_private %}
+                                {% if review.visibility > 0 %}
                                 <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                             <path
                                                 d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
@@ -48,7 +49,7 @@
                                 <div class="review-head__body">
                                     <div class="review-head__info">
                                       
-                                            <a href="{% url 'users:home' review.owner.id %}" class="review-head__owner-link">{{ review.owner.username }}</a>
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="review-head__owner-link">{{ review.owner.username }}</a>
                                             
                                             {% if mark %}    
         
@@ -73,6 +74,7 @@
                                     {{ form.content }}
                                 </div>
                                 {{ form.media }}
+                                {% csrf_token %}
                             </div>
                         </div>
                         
@@ -101,7 +103,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -135,16 +137,8 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
-
-
         $(".markdownx textarea").hide();
     </script>
 </body>
diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html
index b8078275..5926eba6 100644
--- a/music/templates/music/song_review_list.html
+++ b/music/templates/music/song_review_list.html
@@ -13,8 +13,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ song.title }}{% trans '的评论' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {{ song.title }}{% trans '的评论' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
@@ -40,8 +40,8 @@
                                         
                                     <li class="entity-reviews__review entity-reviews__review--wider">
                                         
-                                            <a href="{% url 'users:home' review.owner.id %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
-                                            {% if review.is_private %}
+                                            <a href="{% url 'users:home' review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ review.owner.username }}</a>
+                                            {% if review.visibility > 0 %}
                                             <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
                                             {% endif %}
                                             <span class="entity-reviews__review-time">{{ review.edited_time }}</span>
@@ -108,7 +108,7 @@
                                         {% if song.artist|length > 5 %}
                                         <a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
                                         <script>
-                                            $("#artistMore").click(function (e) {
+                                            $("#artistMore").on('click', function (e) {
                                                 $("span.artist:not(:visible)").each(function (e) {
                                                     $(this).parent().removeAttr('style');
                                                 });
@@ -142,12 +142,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/music/urls.py b/music/urls.py
index 4acab46d..09b180ca 100644
--- a/music/urls.py
+++ b/music/urls.py
@@ -1,4 +1,4 @@
-from django.urls import path
+from django.urls import path, re_path
 from .views import *
 
 
@@ -9,6 +9,7 @@ urlpatterns = [
     path('song/update/<int:id>/', update_song, name='update_song'),
     path('song/delete/<int:id>/', delete_song, name='delete_song'),
     path('song/mark/', create_update_song_mark, name='create_update_song_mark'),
+    path('song/wish/<int:id>/', wish_song, name='wish_song'),
     path('song/<int:song_id>/mark/list/',
          retrieve_song_mark_list, name='retrieve_song_mark_list'),
     path('song/mark/delete/<int:id>/', delete_song_mark, name='delete_song_mark'),
@@ -16,18 +17,18 @@ urlpatterns = [
     path('song/review/update/<int:id>/', update_song_review, name='update_song_review'),
     path('song/review/delete/<int:id>/', delete_song_review, name='delete_song_review'),
     path('song/review/<int:id>/', retrieve_song_review, name='retrieve_song_review'),
-    path('song/<int:song_id>/review/list/',
-         retrieve_song_review_list, name='retrieve_song_review_list'),
+    re_path('song/(?P<song_id>[0-9]+)/mark/list/(?:(?P<following_only>\\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/<int:id>/', retrieve_album, name='retrieve_album'),
     path('album/update/<int:id>/', update_album, name='update_album'),
     path('album/delete/<int:id>/', delete_album, name='delete_album'),
+    path('rescrape/<int:id>/', rescrape, name='rescrape'),
     path('album/mark/', create_update_album_mark, name='create_update_album_mark'),
-    path('album/<int:album_id>/mark/list/',
-         retrieve_album_mark_list, name='retrieve_album_mark_list'),
+    path('album/wish/<int:id>/', wish_album, name='wish_album'),
+    re_path('album/(?P<album_id>[0-9]+)/mark/list/(?:(?P<following_only>\\d+))?', retrieve_album_mark_list, name='retrieve_album_mark_list'),
     path('album/mark/delete/<int:id>/', delete_album_mark, name='delete_album_mark'),
     path('album/<int:album_id>/review/create/', create_album_review, name='create_album_review'),
     path('album/review/update/<int:id>/', update_album_review, name='update_album_review'),
diff --git a/music/views.py b/music/views.py
index 993dae35..af37c2f2 100644
--- a/music/views.py
+++ b/music/views.py
@@ -1,24 +1,24 @@
-# 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.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
 from common.utils import PageLinksGenerator
-from mastodon.utils import rating_to_emoji
-from mastodon.api import check_visibility, post_toot, TootVisibilityEnum
+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
+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__)
@@ -100,6 +100,7 @@ def update_song(request, id):
             '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
@@ -129,6 +130,7 @@ def update_song(request, id):
                 '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
@@ -187,6 +189,7 @@ def retrieve_song(request, id):
         else:
             mark_form = SongMarkForm(initial={
                 'song': song,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -206,10 +209,8 @@ def retrieve_song(request, id):
             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 = 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:
@@ -217,6 +218,7 @@ def retrieve_song(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(song=song)))
 
         # def strip_html_tags(text):
         #     import re
@@ -241,6 +243,7 @@ def retrieve_song(request, id):
                 'review_list_more': review_list_more,
                 'song_tag_list': song_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -285,12 +288,19 @@ def create_update_song_mark(request):
         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:
@@ -298,7 +308,7 @@ def create_update_song_mark(request):
             form = SongMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -326,28 +336,10 @@ def create_update_song_mark(request):
                 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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("music:retrieve_song", args=[form.instance.song.id]))
     else:
@@ -356,11 +348,30 @@ def create_update_song_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_song_mark_list(request, song_id):
+def wish_song(request, id):
+    if request.method == 'POST':
+        song = get_object_or_404(Song, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            'song': song,
+        }
+        try:
+            SongMark.objects.create(**params)
+        except Exception:
+            pass
+        return HttpResponse("✔️")
+    else:
+        return HttpResponseBadRequest("invalid method")
+
+
+@mastodon_request_included
+@login_required
+def retrieve_song_mark_list(request, song_id, following_only=False):
     if request.method == 'GET':
         song = get_object_or_404(Song, pk=song_id)
-        queryset = SongMark.get_available(
-            song, request.user, request.session['oauth_token'])
+        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)
@@ -421,23 +432,8 @@ def create_song_review(request, song_id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_song_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -473,23 +469,8 @@ def update_song_review(request, id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_song_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -524,11 +505,10 @@ def delete_song_review(request, id):
 
 
 @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):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -563,8 +543,7 @@ def retrieve_song_review(request, id):
 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'])
+        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)
@@ -661,6 +640,18 @@ def create_album(request):
         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':
@@ -672,6 +663,7 @@ def update_album(request, id):
             '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
@@ -701,6 +693,7 @@ def update_album(request, id):
                 '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
@@ -758,6 +751,7 @@ def retrieve_album(request, id):
         else:
             mark_form = AlbumMarkForm(initial={
                 'album': album,
+                'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0,
                 'tags': mark_tags
             })
 
@@ -777,10 +771,8 @@ def retrieve_album(request, id):
             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 = 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:
@@ -788,6 +780,7 @@ def retrieve_album(request, id):
             review_list_more = True if len(
                 review_list) > REVIEW_NUMBER else False
             review_list = review_list[:REVIEW_NUMBER]
+        collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(album=album)))
 
         # def strip_html_tags(text):
         #     import re
@@ -812,6 +805,7 @@ def retrieve_album(request, id):
                 'review_list_more': review_list_more,
                 'album_tag_list': album_tag_list,
                 'mark_tags': mark_tags,
+                'collection_list': collection_list,
             }
         )
     else:
@@ -856,12 +850,19 @@ def create_update_album_mark(request):
         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:
@@ -869,7 +870,7 @@ def create_update_album_mark(request):
             form = AlbumMarkForm(request.POST)
 
         if form.is_valid():
-            if form.instance.status == MarkStatusEnum.WISH.value:
+            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
@@ -897,28 +898,10 @@ def create_update_album_mark(request):
                 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")
+                if not share_mark(form.instance):
+                    return go_relogin(request)
         else:
-            return HttpResponseBadRequest("invalid form data")
+            return HttpResponseBadRequest(f"invalid form data {form.errors}")
 
         return redirect(reverse("music:retrieve_album", args=[form.instance.album.id]))
     else:
@@ -927,11 +910,30 @@ def create_update_album_mark(request):
 
 @mastodon_request_included
 @login_required
-def retrieve_album_mark_list(request, album_id):
+def wish_album(request, id):
+    if request.method == 'POST':
+        album = get_object_or_404(Album, pk=id)
+        params = {
+            'owner': request.user,
+            'status': MarkStatusEnum.WISH,
+            'visibility': 0,
+            'album': album,
+        }
+        try:
+            AlbumMark.objects.create(**params)
+        except Exception:
+            pass
+        return HttpResponse("✔️")
+    else:
+        return HttpResponseBadRequest("invalid method")
+
+
+@mastodon_request_included
+@login_required
+def retrieve_album_mark_list(request, album_id, following_only=False):
     if request.method == 'GET':
         album = get_object_or_404(Album, pk=album_id)
-        queryset = AlbumMark.get_available(
-            album, request.user, request.session['oauth_token'])
+        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)
@@ -992,23 +994,8 @@ def create_album_review(request, album_id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_album_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -1044,23 +1031,8 @@ def update_album_review(request, id):
             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")
+                if not share_review(form.instance):
+                    return go_relogin(request)
             return redirect(reverse("music:retrieve_album_review", args=[form.instance.id]))
         else:
             return HttpResponseBadRequest()
@@ -1095,11 +1067,10 @@ def delete_album_review(request, id):
 
 
 @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):
+        if not review.is_visible_to(request.user):
             msg = _("你没有访问这个页面的权限😥")
             return render(
                 request,
@@ -1134,8 +1105,7 @@ def retrieve_album_review(request, id):
 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'])
+        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)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..d3177ab4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,26 @@
+dateparser
+django~=3.2.14
+django-hstore
+django-markdownx @ git+https://github.com/alphatownsman/django-markdownx.git@e69480c64ad9c5d0499f4a8625da78cf2bb7691b
+django-sass
+django-rq
+django-simple-history
+django-hijack
+django-user-messages
+django-slack
+meilisearch
+easy-thumbnails
+lxml
+openpyxl
+psycopg2
+requests
+filetype
+setproctitle
+tqdm
+opencc
+dnspython
+typesense
+markdownify
+sentry-sdk
+gitpython
+igdb-api-v4
diff --git a/sync/apps.py b/sync/apps.py
index 6e952eab..84c86ccf 100644
--- a/sync/apps.py
+++ b/sync/apps.py
@@ -1,9 +1,6 @@
 from django.apps import AppConfig
+from django.conf import settings
 
 
 class SyncConfig(AppConfig):
     name = 'sync'
-
-    def ready(self):
-        from sync.jobs import sync_task_manager
-        sync_task_manager.start()
\ No newline at end of file
diff --git a/sync/jobs.py b/sync/jobs.py
index 0b43c902..e2ce4759 100644
--- a/sync/jobs.py
+++ b/sync/jobs.py
@@ -1,12 +1,8 @@
 import logging
 import pytz
-import signal
-import sys
-import queue
-import threading
-import time
 from dataclasses import dataclass
 from datetime import datetime
+from django.conf import settings
 from django.utils import timezone
 from django.core.exceptions import ObjectDoesNotExist
 from openpyxl import load_workbook
@@ -18,69 +14,17 @@ from common.scraper import DoubanAlbumScraper, DoubanBookScraper, DoubanGameScra
 from common.models import MarkStatusEnum
 from .models import SyncTask
 
-__all__ = ['sync_task_manager']
 
 logger = logging.getLogger(__name__)
 
 
-class SyncTaskManger:
+def __import_should_stop():
+    # TODO: using queue.connection.set(job.key + b':should_stop', 1, ex=30) on the caller side and connection.get(job.key + b':should_stop') on the worker side.
+    pass
 
-    # in seconds
-    __CHECK_NEW_TASK_TIME_INTERVAL = 0.05
-    MAX_WORKERS = 256
 
-    def __init__(self):
-        self.__task_queue = queue.Queue(0)
-        self.__stop_event = threading.Event()
-        self.__worker_threads = []
-
-    def __listen_for_new_task(self):
-        while not self.__stop_event.is_set():
-            time.sleep(self.__CHECK_NEW_TASK_TIME_INTERVAL)
-            while not self.__task_queue.empty() and not self.is_full():
-                task = self.__task_queue.get_nowait()
-                self.__start_new_worker(task)
-
-    def __start_new_worker(self, task):
-        new_worker = threading.Thread(
-            target=sync_doufen_job, args=[task, self.is_stopped], daemon=True
-        )
-        self.__worker_threads.append(new_worker)
-        new_worker.start()
-
-    def __enqueue_existing_tasks(self):
-        for task in SyncTask.objects.filter(is_finished=False):
-            self.__task_queue.put_nowait(task)
-
-    def is_full(self):
-        return len(self.__worker_threads) >= self.MAX_WORKERS
-
-    def add_task(self, task):
-        self.__task_queue.put_nowait(task)
-
-    def stop(self, signum, frame):
-        print('rceived signal ', signum)
-        logger.info(f'rceived signal {signum}')
-
-        self.__stop_event.set()
-        # for worker_thread in self.__worker_threads:
-        #     worker_thread.join()
-
-        print("stopped")
-        logger.info(f'stopped')
-
-    def is_stopped(self):
-        return self.__stop_event.is_set()
-
-    def start(self):
-        self.__enqueue_existing_tasks()  # enqueue
-
-        listen_new_task_thread = threading.Thread(
-            target=self.__listen_for_new_task, daemon=True)
-
-        self.__worker_threads.append(listen_new_task_thread)
-
-        listen_new_task_thread.start()
+def import_doufen_task(synctask):
+    sync_doufen_job(synctask, __import_should_stop)
 
 
 class DoufenParser:
@@ -96,7 +40,7 @@ class DoufenParser:
         self.__file_path = task.file.path
         self.__progress_sheet, self.__progress_row = task.get_breakpoint()
         self.__is_new_task = True
-        if not self.__progress_sheet is None:
+        if self.__progress_sheet is not None:
             self.__is_new_task = False
         if self.__progress_row is None:
             self.__progress_row = 2
@@ -157,10 +101,14 @@ class DoufenParser:
 
         is_first_sheet = True
         for mapping in item_classes_mappings:
+            if mapping['sheet'] not in self.__wb:
+                print(f"Sheet not found: {mapping['sheet']}")
+                continue
             ws = self.__wb[mapping['sheet']]
 
+            max_row = ws.max_row
             # empty sheet
-            if ws.max_row <= 1:
+            if max_row <= 1:
                 continue
 
             # decide starting position
@@ -169,29 +117,26 @@ class DoufenParser:
                 start_row_index = self.__progress_row
 
             # parse data
-            for i in range(start_row_index, ws.max_row + 1):
-                # url definitely exists
-                url = ws.cell(row=i, column=self.URL_INDEX).value
-
-                tags = ws.cell(row=i, column=self.TAG_INDEX).value
-                tags = tags.split(',') if tags else None
-
-                time = ws.cell(row=i, column=self.TIME_INDEX).value
-                if time:
+            tz = pytz.timezone('Asia/Shanghai')
+            i = start_row_index
+            for row in ws.iter_rows(min_row=start_row_index, max_row=max_row, values_only=True):
+                cells = [cell for cell in row]
+                url = cells[self.URL_INDEX - 1]
+                tags = cells[self.TAG_INDEX - 1]
+                tags = list(set(tags.lower().split(','))) if tags else None
+                time = cells[self.TIME_INDEX - 1]
+                if time and type(time) == str:
                     time = datetime.strptime(time, "%Y-%m-%d %H:%M:%S")
-                    tz = pytz.timezone('Asia/Shanghai')
+                    time = time.replace(tzinfo=tz)
+                elif time and type(time) == datetime:
                     time = time.replace(tzinfo=tz)
                 else:
                     time = None
-
-                content = ws.cell(row=i, column=self.CONTENT_INDEX).value
+                content = cells[self.CONTENT_INDEX - 1]
                 if not content:
                     content = ""
-
-                rating = ws.cell(row=i, column=self.RATING_INDEX).value
+                rating = cells[self.RATING_INDEX - 1]
                 rating = int(rating) * 2 if rating else None
-
-                # store result
                 self.items.append({
                     'data': DoufenRowData(url, tags, time, content, rating),
                     'entity_class': mapping['entity_class'],
@@ -201,18 +146,20 @@ class DoufenParser:
                     'sheet': mapping['sheet'],
                     'row_index': i,
                 })
+                i = i + 1
 
             # set first sheet flag
             is_first_sheet = False
 
     def __get_item_number(self):
-        assert not self.__wb is None, 'workbook not found'
-        assert not self.__mappings is None, 'mappings not found'
+        assert self.__wb is not None, 'workbook not found'
+        assert self.__mappings is not None, 'mappings not found'
 
         sheets = [mapping['sheet'] for mapping in self.__mappings]
         item_number = 0
         for sheet in sheets:
-            item_number += self.__wb[sheet].max_row - 1
+            if sheet in self.__wb:
+                item_number += self.__wb[sheet].max_row - 1
 
         return item_number
 
@@ -229,13 +176,12 @@ class DoufenParser:
                 self.__update_total_items()
             self.__close_file()
             return self.items
-
         except Exception as e:
-            logger.error(e)
-            raise e
-
+            logger.error(f'Error parsing {self.__file_path} {e}')
+            self.task.is_failed = True
         finally:
             self.__close_file()
+        return []
 
 
 @dataclass
@@ -247,7 +193,7 @@ class DoufenRowData:
     rating: int
 
 
-def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet, is_private):
+def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet, default_public):
     params = {
         'owner': user,
         'created_time': data.time,
@@ -255,7 +201,7 @@ def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet,
         'rating': data.rating,
         'text': data.content,
         'status': translate_status(sheet),
-        'is_private': not is_private,
+        'visibility': 0 if default_public else 1,
         entity_class.__name__.lower(): entity,
     }
     mark = mark_class.objects.create(**params)
@@ -267,12 +213,15 @@ def add_new_mark(data, user, entity, entity_class, mark_class, tag_class, sheet,
                 entity_class.__name__.lower(): entity,
                 'mark': mark
             }
-            tag_class.objects.create(**params)
+            try:
+                tag_class.objects.create(**params)
+            except Exception as e:
+                logger.error(f'Error creating tag {tag} {mark}: {e}')
 
 
 def overwrite_mark(entity, entity_class, mark, mark_class, tag_class, data, sheet):
     old_rating = mark.rating
-    old_tags = getattr(mark, mark_class.__name__.lower()+'_tags').all()
+    old_tags = getattr(mark, mark_class.__name__.lower() + '_tags').all()
     # update mark logic
     mark.created_time = data.time
     mark.edited_time = data.time
@@ -291,7 +240,10 @@ def overwrite_mark(entity, entity_class, mark, mark_class, tag_class, data, shee
                 entity_class.__name__.lower(): entity,
                 'mark': mark
             }
-            tag_class.objects.create(**params)
+            try:
+                tag_class.objects.create(**params)
+            except Exception as e:
+                logger.error(f'Error creating tag {tag} {mark}: {e}')
 
 
 def sync_doufen_job(task, stop_check_func):
@@ -302,6 +254,7 @@ def sync_doufen_job(task, stop_check_func):
     if task.is_finished:
         return
 
+    print(f'Task {task.pk}: loading')
     parser = DoufenParser(task)
     items = parser.parse()
 
@@ -322,15 +275,17 @@ def sync_doufen_job(task, stop_check_func):
         # scrape the entity if not exists
         try:
             entity = entity_class.objects.get(source_url=data.url)
+            print(f'Task {task.pk}: {len(items)+1} remaining; matched {data.url}')
         except ObjectDoesNotExist:
             try:
+                print(f'Task {task.pk}: {len(items)+1} remaining; scraping {data.url}')
                 scraper.scrape(data.url)
                 form = scraper.save(request_user=task.user)
                 entity = form.instance
             except Exception as e:
-                logger.error(f"Scrape Failed URL: {data.url}")
-                logger.error(
-                    "Expections during scraping data:", exc_info=e)
+                logger.error(f"Task {task.pk}: scrape failed: {data.url} {e}")
+                if settings.DEBUG:
+                    logger.error("Expections during scraping data:", exc_info=e)
                 task.failed_urls.append(data.url)
                 task.finished_items += 1
                 task.save(update_fields=['failed_urls', 'finished_items'])
@@ -360,7 +315,7 @@ def sync_doufen_job(task, stop_check_func):
 
         except Exception as e:
             logger.error(
-                "Unknown exception when syncing marks", exc_info=e)
+                f"Task {task.pk}: error when syncing marks", exc_info=e)
             task.failed_urls.append(data.url)
             task.finished_items += 1
             task.save(update_fields=['failed_urls', 'finished_items'])
@@ -371,6 +326,7 @@ def sync_doufen_job(task, stop_check_func):
         task.save(update_fields=['success_items', 'finished_items'])
 
     # if task finish
+    print(f'Task {task.pk}: stopping')
     if len(items) == 0:
         task.is_finished = True
         task.clear_breakpoint()
@@ -386,13 +342,3 @@ def translate_status(sheet_name):
         return MarkStatusEnum.COLLECT
 
     raise ValueError("Not valid status")
-
-
-sync_task_manager = SyncTaskManger()
-
-# sync_task_manager.start()
-
-signal.signal(signal.SIGTERM, sync_task_manager.stop)
-if sys.platform.startswith('linux'):
-    signal.signal(signal.SIGHUP, sync_task_manager.stop)
-signal.signal(signal.SIGINT, sync_task_manager.stop)
diff --git a/sync/management/commands/resync.py b/sync/management/commands/resync.py
new file mode 100644
index 00000000..a5a0a73c
--- /dev/null
+++ b/sync/management/commands/resync.py
@@ -0,0 +1,91 @@
+from django.core.management.base import BaseCommand
+from common.scraper import get_scraper_by_url, get_normalized_url
+import pprint
+from sync.models import SyncTask
+from users.models import User
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
+from tqdm import tqdm
+from django.conf import settings
+import requests
+import os
+
+
+class Command(BaseCommand):
+    help = 'Re-scrape failed urls (via local proxy)'
+
+    def add_arguments(self, parser):
+        parser.add_argument('action', type=str, help='list/download')
+
+    def handle(self, *args, **options):
+        if options['action'] == 'list':
+            self.do_list()
+        else:
+            self.do_download()
+
+    def do_list(self):
+        tasks = SyncTask.objects.filter(failed_urls__isnull=False)
+        urls = []
+        for task in tqdm(tasks):
+            for url in task.failed_urls:
+                if url not in urls and url not in urls:
+                    url = get_normalized_url(str(url))
+                    scraper = get_scraper_by_url(url)
+                    if scraper is not None:
+                        try:
+                            url = scraper.get_effective_url(url)
+                            entity = scraper.data_class.objects.get(source_url=url)
+                        except ObjectDoesNotExist:
+                            urls.append(url)
+        f = open("/tmp/resync_todo.txt", "w")
+        f.write("\n".join(urls))
+        f.close()
+
+    def do_download(self):
+        self.stdout.write(f'Checking local proxy...{settings.LOCAL_PROXY}')
+        url = f'{settings.LOCAL_PROXY}?url=https://www.douban.com/doumail/'
+        try:
+            r = requests.get(url, timeout=settings.SCRAPING_TIMEOUT)
+        except Exception as e:
+            self.stdout.write(self.style.ERROR(e))
+            return
+        content = r.content.decode('utf-8')
+        if content.find('我的豆邮') == -1:
+            self.stdout.write(self.style.ERROR(f'Proxy check failed.'))
+            return
+
+        self.stdout.write(f'Loading urls...')
+        with open("/tmp/resync_todo.txt") as file:
+            todos = file.readlines()
+            todos = [line.strip() for line in todos]
+        with open("/tmp/resync_success.txt") as file:
+            skips = file.readlines()
+            skips = [line.strip() for line in skips]
+        f_f = open("/tmp/resync_failed.txt", "a")
+        f_i = open("/tmp/resync_ignore.txt", "a")
+        f_s = open("/tmp/resync_success.txt", "a")
+        user = User.objects.get(id=1)
+
+        for url in tqdm(todos):
+            scraper = get_scraper_by_url(url)
+            url = scraper.get_effective_url(url)
+            if url in skips:
+                self.stdout.write(f'Skip {url}')
+            elif scraper is None:
+                self.stdout.write(self.style.ERROR(f'Unable to find scraper for {url}'))
+                f_i.write(url + '\n')
+            else:
+                try:
+                    entity = scraper.data_class.objects.get(source_url=url)
+                    f_i.write(url + '\n')
+                except ObjectDoesNotExist:
+                    try:
+                        # self.stdout.write(f'Fetching {url} via {scraper.__name__}')
+                        scraper.scrape(url)
+                        form = scraper.save(request_user=user)
+                        f_s.write(url + '\n')
+                        f_s.flush()
+                        os.fsync(f_s.fileno())
+                        self.stdout.write(self.style.SUCCESS(f'Saved {url}'))
+                    except Exception as e:
+                        f_f.write(url + '\n')
+                        self.stdout.write(self.style.ERROR(f'Error {url}'))
diff --git a/sync/models.py b/sync/models.py
index e5264477..ae3993e7 100644
--- a/sync/models.py
+++ b/sync/models.py
@@ -1,13 +1,13 @@
 from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 import django.contrib.postgres.fields as postgres
 from users.models import User
-from boofilsic.settings import SYNC_FILE_PATH_ROOT
 from common.utils import GenerateDateUUIDMediaFilePath
+from django.conf import settings
 
 
 def sync_file_path(instance, filename):
-    return GenerateDateUUIDMediaFilePath(instance, filename, SYNC_FILE_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.SYNC_FILE_PATH_ROOT)
 
 
 class SyncTask(models.Model):
@@ -69,7 +69,7 @@ class SyncTask(models.Model):
 
     def __str__(self):
         """Unicode representation of SyncTask."""
-        return str(self.user.username) + '@' + str(self.started_time) + self.get_status_emoji()
+        return f'{self.id} {self.user} {self.file} {self.get_status_emoji()} {self.success_items}/{self.finished_items}/{self.total_items}'
 
     def get_status_emoji(self):
         return ("❌" if self.is_failed else "✔") if self.is_finished else "⚡"
diff --git a/sync/views.py b/sync/views.py
index 69aa7de6..aecd96e8 100644
--- a/sync/views.py
+++ b/sync/views.py
@@ -3,7 +3,7 @@ from django.contrib.auth.decorators import login_required
 from django.http import HttpResponseBadRequest, JsonResponse, HttpResponse
 from .models import SyncTask
 from .forms import SyncTaskForm
-from .jobs import sync_task_manager
+from .jobs import import_doufen_task
 import tempfile
 import os
 from threading import Thread
@@ -11,6 +11,7 @@ import openpyxl
 from django.utils.datastructures import MultiValueDictKeyError
 from openpyxl.utils.exceptions import InvalidFileException
 from zipfile import BadZipFile
+import django_rq
 
 
 @login_required
@@ -25,7 +26,7 @@ def sync_douban(request):
             wb = openpyxl.open(uploaded_file, read_only=True,
                                data_only=True, keep_links=False)
             wb.close()
-        except (MultiValueDictKeyError, InvalidFileException, BadZipFile) as e :
+        except (MultiValueDictKeyError, InvalidFileException, BadZipFile) as e:
             # raise e
             return HttpResponseBadRequest(content="invalid excel file")
 
@@ -35,8 +36,7 @@ def sync_douban(request):
             # stop all preivous task
             SyncTask.objects.filter(user=request.user, is_finished=False).update(is_finished=True)
             form.save()
-            sync_task_manager.add_task(form.instance)
-
+            django_rq.get_queue('doufen').enqueue(import_doufen_task, form.instance, job_id=f'SyncTask_{form.instance.id}')
             return HttpResponse(status=204)
         else:
             return HttpResponseBadRequest()
@@ -55,6 +55,7 @@ def query_progress(request):
         return JsonResponse()
 
 
+@login_required
 def query_last_task(request):
     task = request.user.user_synctasks.order_by('-id').first()
     if task is not None:
diff --git a/timeline/__init__.py b/timeline/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/timeline/admin.py b/timeline/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/timeline/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/timeline/apps.py b/timeline/apps.py
new file mode 100644
index 00000000..00df970d
--- /dev/null
+++ b/timeline/apps.py
@@ -0,0 +1,15 @@
+from django.apps import AppConfig
+
+
+class TimelineConfig(AppConfig):
+    name = 'timeline'
+
+    def ready(self):
+        from .models import init_post_save_handler
+        from books.models import BookMark, BookReview
+        from movies.models import MovieMark, MovieReview
+        from games.models import GameMark, GameReview
+        from music.models import AlbumMark, AlbumReview, SongMark, SongReview
+        from collection.models import Collection, CollectionMark
+        for m in [BookMark, BookReview, MovieMark, MovieReview, GameMark, GameReview, AlbumMark, AlbumReview, SongMark, SongReview, Collection, CollectionMark]:
+            init_post_save_handler(m)
diff --git a/timeline/management/commands/regen_activity.py b/timeline/management/commands/regen_activity.py
new file mode 100644
index 00000000..4dbb702c
--- /dev/null
+++ b/timeline/management/commands/regen_activity.py
@@ -0,0 +1,20 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+from timeline.models import Activity
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, AlbumReview, SongMark, SongReview
+from collection.models import Collection, CollectionMark
+from tqdm import tqdm
+
+
+class Command(BaseCommand):
+    help = 'Re-populating activity for timeline'
+
+    def handle(self, *args, **options):
+        for cl in [BookMark, BookReview, MovieMark, MovieReview, GameMark, GameReview, AlbumMark, AlbumReview, SongMark, SongReview, Collection, CollectionMark]:
+            for a in tqdm(cl.objects.filter(created_time__gt='2022-1-1 00:00+0800'), desc=f'Populating {cl.__name__}'):
+                Activity.upsert_item(a)
diff --git a/timeline/models.py b/timeline/models.py
new file mode 100644
index 00000000..d7959b04
--- /dev/null
+++ b/timeline/models.py
@@ -0,0 +1,63 @@
+from django.db import models
+from common.models import UserOwnedEntity
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, AlbumReview, SongMark, SongReview
+from collection.models import Collection, CollectionMark
+from django.db.models.signals import post_save, post_delete
+
+
+class Activity(UserOwnedEntity):
+    bookmark = models.ForeignKey(BookMark, models.CASCADE, null=True)
+    bookreview = models.ForeignKey(BookReview, models.CASCADE, null=True)
+    moviemark = models.ForeignKey(MovieMark, models.CASCADE, null=True)
+    moviereview = models.ForeignKey(MovieReview, models.CASCADE, null=True)
+    gamemark = models.ForeignKey(GameMark, models.CASCADE, null=True)
+    gamereview = models.ForeignKey(GameReview, models.CASCADE, null=True)
+    albummark = models.ForeignKey(AlbumMark, models.CASCADE, null=True)
+    albumreview = models.ForeignKey(AlbumReview, models.CASCADE, null=True)
+    songmark = models.ForeignKey(SongMark, models.CASCADE, null=True)
+    songreview = models.ForeignKey(SongReview, models.CASCADE, null=True)
+    collection = models.ForeignKey(Collection, models.CASCADE, null=True)
+    collectionmark = models.ForeignKey(CollectionMark, models.CASCADE, null=True)
+
+    @property
+    def target(self):
+        items = [self.bookmark, self.bookreview, self.moviemark, self.moviereview, self.gamemark, self.gamereview,
+                 self.songmark, self.songreview, self.albummark, self.albumreview, self.collection, self.collectionmark]
+        return next((x for x in items if x is not None), None)
+
+    @property
+    def mark(self):
+        items = [self.bookmark, self.moviemark, self.gamemark, self.songmark, self.albummark]
+        return next((x for x in items if x is not None), None)
+
+    @property
+    def review(self):
+        items = [self.bookreview, self.moviereview, self.gamereview, self.songreview, self.albumreview]
+        return next((x for x in items if x is not None), None)
+
+    @classmethod
+    def upsert_item(self, item):
+        attr = item.__class__.__name__.lower()
+        f = {'owner': item.owner, attr: item}
+        activity = Activity.objects.filter(**f).first()
+        if not activity:
+            activity = Activity.objects.create(**f)
+        activity.created_time = item.created_time
+        activity.visibility = item.visibility
+        activity.save()
+
+
+def _post_save_handler(sender, instance, created, **kwargs):
+    Activity.upsert_item(instance)
+
+
+# def activity_post_delete_handler(sender, instance, **kwargs):
+#     pass
+
+
+def init_post_save_handler(model):
+    post_save.connect(_post_save_handler, sender=model)
+    # post_delete.connect(activity_post_delete_handler, sender=model)  # delete handled by database
diff --git a/timeline/templates/timeline.html b/timeline/templates/timeline.html
new file mode 100644
index 00000000..17e89fd8
--- /dev/null
+++ b/timeline/templates/timeline.html
@@ -0,0 +1,83 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }}</title>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script>
+        $(document).ready( function() {
+        let render = function() {
+            let ratingLabels = $(".rating-star");
+            $(ratingLabels).each( function(index, value) {
+                let ratingScore = $(this).data("rating-score") / 2;
+                $(this).starRating({
+                    initialRating: ratingScore,
+                    readOnly: true,
+                    starSize: 16,
+                });
+            });
+        };
+        document.body.addEventListener('htmx:load', function(evt) {
+            render();
+        });
+        render();
+        });
+    </script>
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content" class="container">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            <div class="entity-list">
+
+                                <!-- <div class="set">
+                                    <h5 class="entity-list__title">
+                                        我的时间轴
+                                    </h5>
+                                </div> -->
+                                <ul class="entity-list__entities">
+                                    <div hx-get="{% url 'timeline:data' %}" hx-trigger="revealed" hx-swap="outerHTML"></div>
+                                </ul>
+                            </div>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+
+{% if unread_announcements %}
+{% include "partial/_announcement.html" %}
+{% endif %}
+</body>
+</html>
diff --git a/timeline/templates/timeline_data.html b/timeline/templates/timeline_data.html
new file mode 100644
index 00000000..5b014f39
--- /dev/null
+++ b/timeline/templates/timeline_data.html
@@ -0,0 +1,124 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+{% load neo %}
+
+{% for activity in activities %}
+{% current_user_marked_item activity.target.item as marked %}
+<li class="entity-list__entity">
+    <div class="entity-list__entity-img-wrapper">
+        <a href="{{ activity.target.item.url }}">
+            <img src="{{ activity.target.item.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img" style="min-width:80px;max-width:80px">
+        </a>
+        {% if not marked %}
+        <a class="entity-list__entity-action-icon" hx-post="{{ activity.target.item.wish_url }}">➕</a>
+        {% endif %}
+    </div>
+    <div class="entity-list__entity-text">
+        <div class="collection-item-position-edit">
+            <span class="entity-marks__mark-time">
+                {% if activity.target.shared_link %}
+                <a href="{{ activity.target.shared_link }}" target="_blank">
+                    <img src="{% static 'img/fediverse.svg' %}" style="filter: invert(93%) sepia(1%) saturate(53%) hue-rotate(314deg) brightness(95%) contrast(80%); vertical-align:text-top; max-width:14px; margin-right:6px;" />
+                    <span class="entity-marks__mark-time">{{ activity.target.created_time|prettydate }}</span></a>
+                {% else %}
+                <a><span class="entity-marks__mark-time">{{ activity.target.created_time|prettydate }}</span></a>
+                {% endif %}
+            </span>
+        </div>
+        <span class="entity-list__entity-info" style="top:0px;">
+            <a href="{% url 'users:home' activity.owner.mastodon_username %}">{{ activity.owner.display_name }}</a> {{ activity.target.translated_status }}
+        </span>
+        <div class="entity-list__entity-title">
+            <a href="{{ activity.target.item.url }}" class="entity-list__entity-link" style="font-weight:bold;">{{ activity.target.item.title }}
+            {% if activity.target.item.year %}<small style="font-weight: lighter">({{ activity.target.item.year }})</small>{% endif %}
+            </a>
+            {% if activity.target.item.source_url %}
+            <a href="{{ activity.target.item.source_url }}">
+                <span class="source-label source-label__{{ activity.target.item.source_site }}" style="font-size:xx-small;">{{ activity.target.item.get_source_site_display }}</span>
+            </a>
+            {% endif %}
+        </div>
+        <p class="entity-list__entity-brief">
+            {% if activity.review %}
+                <a href="{{ activity.review.url }}">{{ activity.review.title }}</a>
+            {% endif %}
+            {% if activity.mark %}
+                {% if activity.mark.rating %}
+                <span class="entity-marks__rating-star rating-star" data-rating-score="{{ activity.mark.rating | floatformat:"0" }}" style=""></span>
+                {% endif %}
+
+                {% if activity.mark.text %}
+                <p class="entity-marks__mark-content">{{ activity.mark.text }}</p>
+                {% endif %}
+            {% endif %}
+        </p>
+    </div>
+</li>
+{% if forloop.last %}
+<div class="htmx-indicator" style="margin-left: 60px;" 
+    hx-get="{% url 'timeline:data' %}?last={{ activity.created_time|date:'Y-m-d H:i:s.uO'|urlencode }}"
+    hx-trigger="revealed"
+    hx-swap="outerHTML">
+<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#ccc">
+    <rect y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.5s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.5s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="30" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.25s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.25s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="60" width="15" height="140" rx="6">
+        <animate attributeName="height"
+             begin="0s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="90" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.25s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.25s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+    <rect x="120" y="10" width="15" height="120" rx="6">
+        <animate attributeName="height"
+             begin="0.5s" dur="1s"
+             values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+             repeatCount="indefinite" />
+        <animate attributeName="y"
+             begin="0.5s" dur="1s"
+             values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+             repeatCount="indefinite" />
+    </rect>
+</svg>
+</div>
+{% endif %}
+{% empty %}
+<div>{% trans '目前没有更多内容了' %}</div>
+{% endfor %}
\ No newline at end of file
diff --git a/timeline/tests.py b/timeline/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/timeline/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/timeline/urls.py b/timeline/urls.py
new file mode 100644
index 00000000..b501f1e8
--- /dev/null
+++ b/timeline/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path, re_path
+from .views import *
+
+
+app_name = 'timeline'
+urlpatterns = [
+    path('', timeline, name='timeline'),
+    path('data', data, name='data'),
+]
diff --git a/timeline/views.py b/timeline/views.py
new file mode 100644
index 00000000..0d9142e0
--- /dev/null
+++ b/timeline/views.py
@@ -0,0 +1,71 @@
+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
+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 post_toot, TootVisibilityEnum
+from common.utils import PageLinksGenerator
+from .models import *
+from books.models import BookTag
+from movies.models import MovieTag
+from games.models import GameTag
+from music.models import AlbumTag
+from django.conf import settings
+import re
+from users.models import User
+from django.http import HttpResponseRedirect
+from django.db.models import Q
+import time
+from management.models import Announcement
+
+
+logger = logging.getLogger(__name__)
+mastodon_logger = logging.getLogger("django.mastodon")
+PAGE_SIZE = 20
+
+
+@login_required
+def timeline(request):
+    if request.method != 'GET':
+        return
+    user = request.user
+    unread = Announcement.objects.filter(pk__gt=user.read_announcement_index).order_by('-pk')
+    if unread:
+        user.read_announcement_index = Announcement.objects.latest('pk').pk
+        user.save(update_fields=['read_announcement_index'])
+    return render(
+        request,
+        'timeline.html',
+        {
+            'book_tags': BookTag.all_by_user(user)[:10],
+            'movie_tags': MovieTag.all_by_user(user)[:10],
+            'music_tags': AlbumTag.all_by_user(user)[:10],
+            'game_tags': GameTag.all_by_user(user)[:10],
+            'unread_announcements': unread,
+        }
+    )
+
+
+@login_required
+def data(request):
+    if request.method != 'GET':
+        return
+    q = Q(owner_id__in=request.user.following, visibility__lt=2) | Q(owner_id=request.user.id)
+    last = request.GET.get('last')
+    if last:
+        q = q & Q(created_time__lt=last)
+    activities = Activity.objects.filter(q).order_by('-created_time')[:PAGE_SIZE]
+    return render(
+        request,
+        'timeline_data.html',
+        {
+            'activities': activities,
+        }
+    )
diff --git a/users/account.py b/users/account.py
new file mode 100644
index 00000000..6bc38e71
--- /dev/null
+++ b/users/account.py
@@ -0,0 +1,255 @@
+from django.shortcuts import reverse, redirect, render, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import auth
+from django.contrib.auth import authenticate
+from django.core.paginator import Paginator
+from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Count
+from .models import User, Report, Preference
+from .forms import ReportForm
+from mastodon.api import *
+from mastodon import mastodon_request_included
+from common.config import *
+from common.models import MarkStatusEnum
+from common.utils import PageLinksGenerator
+from management.models import Announcement
+from books.models import *
+from movies.models import *
+from music.models import *
+from games.models import *
+from books.forms import BookMarkStatusTranslator
+from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
+from games.forms import GameMarkStatusTranslator
+from mastodon.models import MastodonApplication
+from mastodon.api import verify_account
+from django.conf import settings
+from urllib.parse import quote
+import django_rq
+from .account import *
+from .tasks import *
+from datetime import timedelta
+from django.utils import timezone
+import json
+from django.contrib import messages
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, SongMark, AlbumReview, SongReview
+from collection.models import Collection, CollectionMark
+from common.importers.goodreads import GoodreadsImporter
+from common.importers.douban import DoubanImporter
+
+
+# the 'login' page that user can see
+def login(request):
+    if request.method == 'GET':
+        selected_site = request.GET.get('site', default='')
+
+        sites = MastodonApplication.objects.all().order_by("domain_name")
+
+        # store redirect url in the cookie
+        if request.GET.get('next'):
+            request.session['next_url'] = request.GET.get('next')
+
+        return render(
+            request,
+            'users/login.html',
+            {
+                'sites': sites,
+                'scope': quote(settings.MASTODON_CLIENT_SCOPE),
+                'selected_site': selected_site,
+                'allow_any_site': settings.MASTODON_ALLOW_ANY_SITE,
+            }
+        )
+    else:
+        return HttpResponseBadRequest()
+
+
+# connect will redirect to mastodon server
+def connect(request):
+    login_domain = request.session['swap_domain'] if request.session.get('swap_login') else request.GET.get('domain')
+    if not login_domain:
+        return render(request, 'common/error.html', {'msg': '未指定实例域名', 'secondary_msg': "", })
+    login_domain = login_domain.strip().lower().split('//')[-1].split('/')[0].split('@')[-1]
+    domain, version = get_instance_info(login_domain)
+    app, error_msg = get_mastodon_application(domain)
+    if app is None:
+        return render(request, 'common/error.html', {'msg': error_msg, 'secondary_msg': "", })
+    else:
+        login_url = get_mastodon_login_url(app, login_domain, version, request)
+        resp = redirect(login_url)
+        resp.set_cookie('mastodon_domain', domain)
+        return resp
+
+
+# mastodon server redirect back to here
+@mastodon_request_included
+def OAuth2_login(request):
+    if request.method != 'GET':
+        return HttpResponseBadRequest()
+
+    code = request.GET.get('code')
+    site = request.COOKIES.get('mastodon_domain')
+    try:
+        token, refresh_token = obtain_token(site, request, code)
+    except ObjectDoesNotExist:
+        return HttpResponseBadRequest("Mastodon site not registered")
+    if not token:
+        return render(
+            request,
+            'common/error.html',
+            {
+                'msg': _("认证失败😫")
+            }
+        )
+
+    if request.session.get('swap_login', False) and request.user.is_authenticated:  # swap login for existing user
+        return swap_login(request, token, site, refresh_token)
+
+    user = authenticate(request, token=token, site=site)
+    if user:  # existing user
+        user.mastodon_token = token
+        user.mastodon_refresh_token = refresh_token
+        user.save(update_fields=['mastodon_token', 'mastodon_refresh_token'])
+        auth_login(request, user)
+        if request.session.get('next_url') is not None:
+            response = redirect(request.session.get('next_url'))
+            del request.session['next_url']
+        else:
+            response = redirect(reverse('common:home'))
+        return response
+    else:  # newly registered user
+        code, user_data = verify_account(site, token)
+        if code != 200 or user_data is None:
+            return render(
+                request,
+                'common/error.html',
+                {
+                    'msg': _("联邦网络访问失败😫")
+                }
+            )
+        new_user = User(
+            username=user_data['username'],
+            mastodon_id=user_data['id'],
+            mastodon_site=site,
+            mastodon_token=token,
+            mastodon_refresh_token=refresh_token,
+            mastodon_account=user_data,
+        )
+        new_user.save()
+        Preference.objects.create(user=new_user)
+        request.session['new_user'] = True
+        auth_login(request, new_user)
+        return redirect(reverse('users:register'))
+
+
+@mastodon_request_included
+@login_required
+def logout(request):
+    if request.method == 'GET':
+        # revoke_token(request.user.mastodon_site, request.user.mastodon_token)
+        auth_logout(request)
+        return redirect(reverse("users:login"))
+    else:
+        return HttpResponseBadRequest()
+
+
+@mastodon_request_included
+@login_required
+def reconnect(request):
+    if request.method == 'POST':
+        request.session['swap_login'] = True
+        request.session['swap_domain'] = request.POST['domain']
+        return connect(request)
+    else:
+        return HttpResponseBadRequest()
+
+
+@mastodon_request_included
+def register(request):
+    if request.session.get('new_user'):
+        del request.session['new_user']
+        return render(request, 'users/register.html')
+    else:
+        return redirect(reverse('common:home'))
+
+
+def swap_login(request, token, site, refresh_token):
+    del request.session['swap_login']
+    del request.session['swap_domain']
+    code, data = verify_account(site, token)
+    current_user = request.user
+    if code == 200 and data is not None:
+        username = data['username']
+        if username == current_user.username and site == current_user.mastodon_site:
+            messages.add_message(request, messages.ERROR, _(f'该身份 {username}@{site} 与当前账号相同。'))
+        else:
+            try:
+                existing_user = User.objects.get(username=username, mastodon_site=site)
+                messages.add_message(request, messages.ERROR, _(f'该身份 {username}@{site} 已被用于其它账号。'))
+            except ObjectDoesNotExist:
+                current_user.username = username
+                current_user.mastodon_id = data['id']
+                current_user.mastodon_site = site
+                current_user.mastodon_token = token
+                current_user.mastodon_refresh_token = refresh_token
+                current_user.mastodon_account = data
+                current_user.save(update_fields=['username', 'mastodon_id', 'mastodon_site', 'mastodon_token', 'mastodon_refresh_token', 'mastodon_account'])
+                django_rq.get_queue('mastodon').enqueue(refresh_mastodon_data_task, current_user, token)
+                messages.add_message(request, messages.INFO, _(f'账号身份已更新为 {username}@{site}。'))
+    else:
+        messages.add_message(request, messages.ERROR, _('连接联邦网络获取身份信息失败。'))
+    return redirect(reverse('users:data'))
+
+
+def auth_login(request, user):
+    """ Decorates django ``login()``. Attach token to session."""
+    auth.login(request, user)
+    if user.mastodon_last_refresh < timezone.now() - timedelta(hours=1) or user.mastodon_account == {}:
+        django_rq.get_queue('mastodon').enqueue(refresh_mastodon_data_task, user)
+
+
+def auth_logout(request):
+    """ Decorates django ``logout()``. Release token in session."""
+    auth.logout(request)
+
+
+@login_required
+def clear_data(request):
+    if request.method == 'POST':
+        if request.POST.get('verification') == request.user.mastodon_username:
+            BookMark.objects.filter(owner=request.user).delete()
+            MovieMark.objects.filter(owner=request.user).delete()
+            GameMark.objects.filter(owner=request.user).delete()
+            AlbumMark.objects.filter(owner=request.user).delete()
+            SongMark.objects.filter(owner=request.user).delete()
+            BookReview.objects.filter(owner=request.user).delete()
+            MovieReview.objects.filter(owner=request.user).delete()
+            GameReview.objects.filter(owner=request.user).delete()
+            AlbumReview.objects.filter(owner=request.user).delete()
+            SongReview.objects.filter(owner=request.user).delete()
+            CollectionMark.objects.filter(owner=request.user).delete()
+            Collection.objects.filter(owner=request.user).delete()
+            request.user.first_name = request.user.username
+            request.user.last_name = request.user.mastodon_site
+            request.user.is_active = False
+            request.user.username = 'removed_' + str(request.user.id)
+            request.user.mastodon_id = 0
+            request.user.mastodon_site = 'removed'
+            request.user.mastodon_token = ''
+            request.user.mastodon_locked = False
+            request.user.mastodon_followers = []
+            request.user.mastodon_following = []
+            request.user.mastodon_mutes = []
+            request.user.mastodon_blocks = []
+            request.user.mastodon_domain_blocks = []
+            request.user.mastodon_account = {}
+            request.user.save()
+            auth_logout(request)
+            return redirect(reverse("users:login"))
+        else:
+            messages.add_message(request, messages.ERROR, _('验证信息不符。'))
+    return redirect(reverse("users:data"))
diff --git a/users/data.py b/users/data.py
new file mode 100644
index 00000000..505344c8
--- /dev/null
+++ b/users/data.py
@@ -0,0 +1,142 @@
+from django.shortcuts import reverse, redirect, render, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import auth
+from django.contrib.auth import authenticate
+from django.core.paginator import Paginator
+from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Count
+from .models import User, Report, Preference
+from .forms import ReportForm
+from mastodon.api import *
+from mastodon import mastodon_request_included
+from common.config import *
+from common.models import MarkStatusEnum
+from common.utils import PageLinksGenerator
+from management.models import Announcement
+from books.models import *
+from movies.models import *
+from music.models import *
+from games.models import *
+from books.forms import BookMarkStatusTranslator
+from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
+from games.forms import GameMarkStatusTranslator
+from mastodon.models import MastodonApplication
+from mastodon.api import verify_account
+from django.conf import settings
+from urllib.parse import quote
+import django_rq
+from .account import *
+from .tasks import *
+from datetime import timedelta
+from django.utils import timezone
+import json
+from django.contrib import messages
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, SongMark, AlbumReview, SongReview
+from timeline.models import Activity
+from collection.models import Collection
+from common.importers.goodreads import GoodreadsImporter
+from common.importers.douban import DoubanImporter
+
+
+@mastodon_request_included
+@login_required
+def preferences(request):
+    preference = request.user.get_preference()
+    if request.method == 'POST':
+        preference.default_visibility = int(request.POST.get('default_visibility'))
+        preference.classic_homepage = bool(request.POST.get('classic_homepage'))
+        preference.mastodon_publish_public = bool(request.POST.get('mastodon_publish_public'))
+        preference.mastodon_append_tag = request.POST.get('mastodon_append_tag', '').strip()
+        preference.save(update_fields=['default_visibility', 'classic_homepage', 'mastodon_publish_public', 'mastodon_append_tag'])
+    return render(request, 'users/preferences.html')
+
+
+@mastodon_request_included
+@login_required
+def data(request):
+    return render(request, 'users/data.html', {
+        'allow_any_site': settings.MASTODON_ALLOW_ANY_SITE,
+        'latest_task': request.user.user_synctasks.order_by("-id").first(),
+        'import_status': request.user.get_preference().import_status,
+        'export_status': request.user.get_preference().export_status
+    })
+
+
+@mastodon_request_included
+@login_required
+def export_reviews(request):
+    if request.method != 'POST':
+        return redirect(reverse("users:data"))
+    return render(request, 'users/data.html')
+
+
+@mastodon_request_included
+@login_required
+def export_marks(request):
+    if request.method == 'POST':
+        if not request.user.preference.export_status.get('marks_pending'):
+            django_rq.get_queue('export').enqueue(export_marks_task, request.user)
+            request.user.preference.export_status['marks_pending'] = True
+            request.user.preference.save()
+        messages.add_message(request, messages.INFO, _('导出已开始。'))
+        return redirect(reverse("users:data"))
+    else:
+        try:
+            with open(request.user.preference.export_status['marks_file'], 'rb') as fh:
+                response = HttpResponse(fh.read(), content_type="application/vnd.ms-excel")
+                response['Content-Disposition'] = 'attachment;filename="marks.xlsx"'
+                return response
+        except Exception:
+            messages.add_message(request, messages.ERROR, _('导出文件已过期,请重新导出'))
+            return redirect(reverse("users:data"))
+
+
+@login_required
+def sync_mastodon(request):
+    if request.method == 'POST':
+        django_rq.get_queue('mastodon').enqueue(refresh_mastodon_data_task, request.user)
+        messages.add_message(request, messages.INFO, _('同步已开始。'))
+    return redirect(reverse("users:data"))
+
+
+@login_required
+def reset_visibility(request):
+    if request.method == 'POST':
+        visibility = int(request.POST.get('visibility'))
+        visibility = visibility if visibility >= 0 and visibility <= 2 else 0
+        BookMark.objects.filter(owner=request.user).update(visibility=visibility)
+        MovieMark.objects.filter(owner=request.user).update(visibility=visibility)
+        GameMark.objects.filter(owner=request.user).update(visibility=visibility)
+        AlbumMark.objects.filter(owner=request.user).update(visibility=visibility)
+        SongMark.objects.filter(owner=request.user).update(visibility=visibility)
+        Activity.objects.filter(owner=request.user).update(visibility=visibility)
+        messages.add_message(request, messages.INFO, _('已重置。'))
+    return redirect(reverse("users:data"))
+
+
+@login_required
+def import_goodreads(request):
+    if request.method == 'POST':
+        raw_url = request.POST.get('url')
+        if GoodreadsImporter.import_from_url(raw_url, request.user):
+            messages.add_message(request, messages.INFO, _('链接已保存,等待后台导入。'))
+        else:
+            messages.add_message(request, messages.ERROR, _('无法识别链接。'))
+    return redirect(reverse("users:data"))
+
+
+@login_required
+def import_douban(request):
+    if request.method == 'POST':
+        importer = DoubanImporter(request.user, request.POST.get('visibility'))
+        if importer.import_from_file(request.FILES['file']):
+            messages.add_message(request, messages.INFO, _('文件上传成功,等待后台导入。'))
+        else:
+            messages.add_message(request, messages.ERROR, _('无法识别文件。'))
+    return redirect(reverse("users:data"))
diff --git a/users/management/commands/backfill_mastodon.py b/users/management/commands/backfill_mastodon.py
new file mode 100644
index 00000000..55f4dea6
--- /dev/null
+++ b/users/management/commands/backfill_mastodon.py
@@ -0,0 +1,21 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from django.contrib.sessions.models import Session
+
+
+class Command(BaseCommand):
+    help = 'Backfill Mastodon data if missing'
+
+    def handle(self, *args, **options):
+        for session in Session.objects.order_by('-expire_date'):
+            uid = session.get_decoded().get('_auth_user_id')
+            token = session.get_decoded().get('oauth_token')
+            if uid and token:
+                user = User.objects.get(pk=uid)
+                if user.mastodon_token:
+                    print(f'skip {user}')
+                    continue
+                user.mastodon_token = token
+                user.refresh_mastodon_data()
+                user.save()
+                print(f"Refreshed {user}")
diff --git a/users/management/commands/disable_user.py b/users/management/commands/disable_user.py
new file mode 100644
index 00000000..ac28b6a5
--- /dev/null
+++ b/users/management/commands/disable_user.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+
+
+class Command(BaseCommand):
+    help = 'disable user'
+
+    def add_arguments(self, parser):
+        parser.add_argument('id', type=int, help='user id')
+
+    def handle(self, *args, **options):
+        h = int(options['id'])
+        u = User.objects.get(id=h)
+        u.username = '(duplicated)'+u.username
+        u.is_active = False
+        u.save()
+        print(f'{u} updated')
diff --git a/users/management/commands/refresh_following.py b/users/management/commands/refresh_following.py
new file mode 100644
index 00000000..b29f4f0c
--- /dev/null
+++ b/users/management/commands/refresh_following.py
@@ -0,0 +1,19 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+from tqdm import tqdm
+
+
+class Command(BaseCommand):
+    help = 'Refresh following data for all users'
+
+    def handle(self, *args, **options):
+        count = 0
+        for user in tqdm(User.objects.all()):
+            user.following = user.get_following_ids()
+            if user.following:
+                count += 1
+                user.save(update_fields=['following'])
+
+        print(f'{count} users updated')
diff --git a/users/management/commands/refresh_mastodon.py b/users/management/commands/refresh_mastodon.py
new file mode 100644
index 00000000..ff79d5fc
--- /dev/null
+++ b/users/management/commands/refresh_mastodon.py
@@ -0,0 +1,25 @@
+from django.core.management.base import BaseCommand
+from users.models import User
+from datetime import timedelta
+from django.utils import timezone
+from tqdm import tqdm
+
+
+class Command(BaseCommand):
+    help = 'Refresh Mastodon data for all users if not updated in last 24h'
+
+    def handle(self, *args, **options):
+        count = 0
+        for user in tqdm(User.objects.filter(mastodon_last_refresh__lt=timezone.now() - timedelta(hours=24), is_active=True)):
+            if user.mastodon_token or user.mastodon_refresh_token:
+                tqdm.write(f"Refreshing {user}")
+                if user.refresh_mastodon_data():
+                    tqdm.write(f"Refreshed {user}")
+                    count += 1
+                else:
+                    tqdm.write(f"Refresh failed for {user}")
+                user.save()
+            else:
+                tqdm.write(f'Missing token for {user}')
+
+        print(f'{count} users updated')
diff --git a/users/models.py b/users/models.py
index 7d35433f..2cc92e83 100644
--- a/users/models.py
+++ b/users/models.py
@@ -3,21 +3,40 @@ import django.contrib.postgres.fields as postgres
 from django.db import models
 from django.contrib.auth.models import AbstractUser
 from django.utils import timezone
-from boofilsic.settings import REPORT_MEDIA_PATH_ROOT, DEFAULT_PASSWORD
 from django.core.serializers.json import DjangoJSONEncoder
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
 from common.utils import GenerateDateUUIDMediaFilePath
+from django.conf import settings
+from mastodon.api import *
 
 
 def report_image_path(instance, filename):
-    return GenerateDateUUIDMediaFilePath(instance, filename, REPORT_MEDIA_PATH_ROOT)
+    return GenerateDateUUIDMediaFilePath(instance, filename, settings.REPORT_MEDIA_PATH_ROOT)
 
 
 class User(AbstractUser):
-    mastodon_id = models.IntegerField(blank=False)
+    if settings.MASTODON_ALLOW_ANY_SITE:
+        username = models.CharField(
+            _('username'),
+            max_length=150,
+            unique=False,
+            help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
+        )
+    following = models.JSONField(default=list)
+    mastodon_id = models.CharField(max_length=100, blank=False)
     # mastodon domain name, eg donotban.com
     mastodon_site = models.CharField(max_length=100, blank=False)
-    # store the latest read announcement id, 
+    mastodon_token = models.CharField(max_length=2048, default='')
+    mastodon_refresh_token = models.CharField(max_length=2048, default='')
+    mastodon_locked = models.BooleanField(default=False)
+    mastodon_followers = models.JSONField(default=list)
+    mastodon_following = models.JSONField(default=list)
+    mastodon_mutes = models.JSONField(default=list)
+    mastodon_blocks = models.JSONField(default=list)
+    mastodon_domain_blocks = models.JSONField(default=list)
+    mastodon_account = models.JSONField(default=dict)
+    mastodon_last_refresh = models.DateTimeField(default=timezone.now)
+    # store the latest read announcement id,
     # every time user read the announcement update this field
     read_announcement_index = models.PositiveIntegerField(default=0)
 
@@ -27,13 +46,111 @@ class User(AbstractUser):
                 fields=['username', 'mastodon_site'], name="unique_user_identity")
         ]
 
-    def save(self, *args, **kwargs):
-        """ Automatically populate password field with DEFAULT_PASSWORD before saving."""
-        self.set_password(DEFAULT_PASSWORD)
-        return super().save(*args, **kwargs)
+    # def save(self, *args, **kwargs):
+    #     """ Automatically populate password field with settings.DEFAULT_PASSWORD before saving."""
+    #     self.set_password(settings.DEFAULT_PASSWORD)
+    #     return super().save(*args, **kwargs)
+
+    @property
+    def mastodon_username(self):
+        return self.username + '@' + self.mastodon_site
+
+    @property
+    def display_name(self):
+        return self.mastodon_account['display_name'] if self.mastodon_account and 'display_name' in self.mastodon_account and self.mastodon_account['display_name'] else self.mastodon_username
 
     def __str__(self):
-        return self.username + '@' + self.mastodon_site
+        return self.mastodon_username
+
+    def get_preference(self):
+        pref = Preference.objects.filter(user=self).first()  # self.preference
+        if not pref:
+            pref = Preference.objects.create(user=self)
+        return pref
+
+    def refresh_mastodon_data(self):
+        """ Try refresh account data from mastodon server, return true if refreshed successfully, note it will not save to db """
+        self.mastodon_last_refresh = timezone.now()
+        code, mastodon_account = verify_account(self.mastodon_site, self.mastodon_token)
+        if code == 401 and self.mastodon_refresh_token:
+            self.mastodon_token = refresh_access_token(self.mastodon_site, self.mastodon_refresh_token)
+            if self.mastodon_token:
+                code, mastodon_account = verify_account(self.mastodon_site, self.mastodon_token)
+        updated = False
+        if mastodon_account:
+            self.mastodon_account = mastodon_account
+            self.mastodon_locked = mastodon_account['locked']
+            if self.username != mastodon_account['username']:
+                print(f"username changed from {self} to {mastodon_account['username']}")
+                self.username = mastodon_account['username']
+            # self.mastodon_token = token
+            # user.mastodon_id  = mastodon_account['id']
+            self.mastodon_followers = get_related_acct_list(self.mastodon_site, self.mastodon_token, f'/api/v1/accounts/{self.mastodon_id}/followers')
+            self.mastodon_following = get_related_acct_list(self.mastodon_site, self.mastodon_token, f'/api/v1/accounts/{self.mastodon_id}/following')
+            self.mastodon_mutes = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/mutes')
+            self.mastodon_blocks = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/blocks')
+            self.mastodon_domain_blocks = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/domain_blocks')
+            self.following = self.get_following_ids()
+            updated = True
+        elif code == 401:
+            print(f'401 {self}')
+            self.mastodon_token = ''
+        return updated
+
+    def get_following_ids(self):
+        fl = []
+        for m in self.mastodon_following:
+            target = User.get(m)
+            if target and ((not target.mastodon_locked) or self.mastodon_username in target.mastodon_followers):
+                fl.append(target.id)
+        return fl
+
+    def is_blocking(self, target):
+        return target.mastodon_username in self.mastodon_blocks or target.mastodon_site in self.mastodon_domain_blocks
+
+    def is_blocked_by(self, target):
+        return target.is_blocking(self)
+
+    def is_muting(self, target):
+        return target.mastodon_username in self.mastodon_mutes
+
+    def is_following(self, target):
+        return self.mastodon_username in target.mastodon_followers if target.mastodon_locked else self.mastodon_username in target.mastodon_followers or target.mastodon_username in self.mastodon_following
+
+    def is_followed_by(self, target):
+        return target.is_following(self)
+
+    def get_mark_for_item(self, item):
+        params = {item.__class__.__name__.lower() + '_id': item.id, 'owner': self}
+        mark = item.mark_class.objects.filter(**params).first()
+        return mark
+
+    def get_max_visibility(self, viewer):
+        if not viewer.is_authenticated:
+            return 0
+        elif viewer == self:
+            return 2
+        elif viewer.is_blocked_by(self):
+            return -1
+        elif viewer.is_following(self):
+            return 1
+        else:
+            return 0
+
+    @classmethod
+    def get(self, id):
+        if isinstance(id, str):
+            try:
+                username = id.split('@')[0]
+                site = id.split('@')[1]
+            except IndexError as e:
+                return None
+            query_kwargs = {'username': username, 'mastodon_site': site}
+        elif isinstance(id, int):
+            query_kwargs = {'pk': id}
+        else:
+            return None
+        return User.objects.filter(**query_kwargs).first()
 
 
 class Preference(models.Model):
@@ -43,9 +160,15 @@ class Preference(models.Model):
         blank=True,
         default=list,
     )
+    export_status = models.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
+    import_status = models.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
+    default_visibility = models.PositiveSmallIntegerField(default=0)
+    classic_homepage = models.BooleanField(null=False, default=False)
+    mastodon_publish_public = models.BooleanField(null=False, default=False)
+    mastodon_append_tag = models.CharField(max_length=2048, default='')
 
     def get_serialized_home_layout(self):
-        return str(self.home_layout).replace("\'","\"")
+        return str(self.home_layout).replace("\'", "\"")
 
     def __str__(self):
         return str(self.user)
diff --git a/users/static/js/followers_list.js b/users/static/js/followers_list.js
index 0c355f60..44680865 100644
--- a/users/static/js/followers_list.js
+++ b/users/static/js/followers_list.js
@@ -36,7 +36,8 @@ $(document).ready( function() {
             }
             $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
             $("#userInfoCard .mast-displayname").html(userName);
-            $("#userInfoCard .mast-brief").text($(userData.note).text());
+            $("#userInfoCard .mast-brief").text($("<div>"+userData.note.replace(/\<br/g,'\n<br').replace(/\<p/g,'\n<p')+"</div>").text());
+            $("#userInfoCard .mast-brief").html($("#userInfoCard .mast-brief").html().replace(/\n/g,'<br/>'));
             $(userInfoSpinner).remove();
         }
     );
@@ -45,7 +46,7 @@ $(document).ready( function() {
         id,
         mast_uri,
         token,
-        function(userList, request) {
+        function(userList, nextPage) {
             let subUserList = null;
             if (userList.length == 0) {
                 $(".mast-followers").hide();
@@ -101,12 +102,7 @@ $(document).ready( function() {
             });
 
             mainSpinner.hide();
-            request.getResponseHeader('link').split(',').forEach(link => {
-                if (link.includes('next')) {
-                    let regex = /<(.*?)>/;
-                    nextUrl = link.match(regex)[1];
-                }
-            });            
+            nextUrl = nextPage;
         }
     );
 
@@ -206,7 +202,12 @@ $(document).ready( function() {
                         } else {
                             temp.find(".mast-displayname").text(data.username);
                         }
-                        let url = $("#userPageURL").text().replace('0', data.id);
+                        let url;
+                        if (data.acct.includes('@')) {
+                            url = $("#userPageURL").text().replace('0', data.acct);
+                        } else {
+                            url = $("#userPageURL").text().replace('0', data.acct + '@' + mast_domain);
+                        }
                         temp.find("a").attr('href', url);
                         temp.find(".mast-brief").text(data.note.replace(/(<([^>]+)>)/ig, ""));
                         // console.log($(temp).html())
diff --git a/users/static/js/following_list.js b/users/static/js/following_list.js
index 0e6ca0ba..6b3ba278 100644
--- a/users/static/js/following_list.js
+++ b/users/static/js/following_list.js
@@ -36,7 +36,8 @@ $(document).ready( function() {
             }
             $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
             $("#userInfoCard .mast-displayname").html(userName);
-            $("#userInfoCard .mast-brief").text($(userData.note).text());
+            $("#userInfoCard .mast-brief").text($("<div>"+userData.note.replace(/\<br/g,'\n<br').replace(/\<p/g,'\n<p')+"</div>").text());
+            $("#userInfoCard .mast-brief").html($("#userInfoCard .mast-brief").html().replace(/\n/g,'<br/>'));
             $(userInfoSpinner).remove();
         }
     );
@@ -82,7 +83,7 @@ $(document).ready( function() {
         id,
         mast_uri,
         token,
-        function(userList, request) {
+        function(userList, nextPage) {
             // aside
             let subUserList = null;
             if (userList.length == 0) {
@@ -139,12 +140,7 @@ $(document).ready( function() {
             });
 
             mainSpinner.hide();
-            request.getResponseHeader('link').split(',').forEach(link => {
-                if (link.includes('next')) {
-                    let regex = /<(.*?)>/;
-                    nextUrl = link.match(regex)[1];
-                }
-            });
+            nextUrl = nextPage;
         }
     );
 
diff --git a/users/static/lib/js/js.cookie.min.js b/users/static/lib/js/js.cookie.min.js
deleted file mode 100644
index 1549485f..00000000
--- a/users/static/lib/js/js.cookie.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! js-cookie v3.0.0-rc.1 | MIT */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,r=e.Cookies=t();r.noConflict=function(){return e.Cookies=n,r}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)e[r]=n[r]}return e}var t={read:function(e){return e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}};return function n(r,o){function i(t,n,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape),n=r.write(n,t);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n+c}}return Object.create({set:i,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var n=document.cookie?document.cookie.split("; "):[],o={},i=0;i<n.length;i++){var c=n[i].split("="),u=c.slice(1).join("=");'"'===u[0]&&(u=u.slice(1,-1));try{var f=t.read(c[0]);if(o[f]=r.read(u,f),e===f)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){i(t,"",e({},n,{expires:-1}))},withAttributes:function(t){return n(this.converter,e({},this.attributes,t))},withConverter:function(t){return n(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(r)}})}(t,{path:"/"})});
diff --git a/users/tasks.py b/users/tasks.py
new file mode 100644
index 00000000..a7c17f82
--- /dev/null
+++ b/users/tasks.py
@@ -0,0 +1,146 @@
+from django.shortcuts import reverse, redirect, render, get_object_or_404
+from django.http import HttpResponseBadRequest, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import auth
+from django.contrib.auth import authenticate
+from django.core.paginator import Paginator
+from django.utils.translation import gettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Count
+from .models import User, Report, Preference
+from .forms import ReportForm
+from mastodon.api import *
+from mastodon import mastodon_request_included
+from common.config import *
+from common.models import MarkStatusEnum
+from common.utils import PageLinksGenerator
+from management.models import Announcement
+from books.models import *
+from movies.models import *
+from music.models import *
+from games.models import *
+from books.forms import BookMarkStatusTranslator
+from movies.forms import MovieMarkStatusTranslator
+from music.forms import MusicMarkStatusTranslator
+from games.forms import GameMarkStatusTranslator
+from mastodon.models import MastodonApplication
+from django.conf import settings
+from urllib.parse import quote
+from openpyxl import Workbook
+from common.utils import GenerateDateUUIDMediaFilePath
+from datetime import datetime
+import os
+
+
+def refresh_mastodon_data_task(user, token=None):
+    if token:
+        user.mastodon_token = token
+    if user.refresh_mastodon_data():
+        user.save()
+        print(f"{user} mastodon data refreshed")
+    else:
+        print(f"{user} mastodon data refresh failed")
+
+
+def export_marks_task(user):
+    user.preference.export_status['marks_pending'] = True
+    user.preference.save(update_fields=['export_status'])
+    filename = GenerateDateUUIDMediaFilePath(None, 'f.xlsx', settings.MEDIA_ROOT + settings.EXPORT_FILE_PATH_ROOT)
+    if not os.path.exists(os.path.dirname(filename)):
+        os.makedirs(os.path.dirname(filename))
+    heading = ['标题', '简介', '豆瓣评分', '链接', '创建时间', '我的评分', '标签', '评论', 'NeoDB链接', '其它ID']
+    wb = Workbook()  # adding write_only=True will speed up but corrupt the xlsx and won't be importable
+    for status, label in [('collect', '看过'), ('do', '在看'), ('wish', '想看')]:
+        ws = wb.create_sheet(title=label)
+        marks = MovieMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            movie = mark.movie
+            title = movie.title
+            summary = str(movie.year) + ' / ' + ','.join(movie.area) + ' / ' + ','.join(map(lambda x: str(MovieGenreTranslator[x]), movie.genre)) + ' / ' + ','.join(movie.director) + ' / ' + ','.join(movie.actor)
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (movie.rating / 2) if movie.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = movie.source_url
+            url = settings.APP_WEBSITE + movie.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, movie.imdb_code]
+            ws.append(line)
+
+    for status, label in [('collect', '听过'), ('do', '在听'), ('wish', '想听')]:
+        ws = wb.create_sheet(title=label)
+        marks = AlbumMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            album = mark.album
+            title = album.title
+            summary = ','.join(album.artist) + ' / ' + (album.release_date.strftime('%Y') if album.release_date else '')
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (album.rating / 2) if album.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = album.source_url
+            url = settings.APP_WEBSITE + album.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, '']
+            ws.append(line)
+
+    for status, label in [('collect', '读过'), ('do', '在读'), ('wish', '想读')]:
+        ws = wb.create_sheet(title=label)
+        marks = BookMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            book = mark.book
+            title = book.title
+            summary = ','.join(book.author) + ' / ' + str(book.pub_year) + ' / ' + book.pub_house
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (book.rating / 2) if book.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = book.source_url
+            url = settings.APP_WEBSITE + book.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, book.isbn]
+            ws.append(line)
+
+    for status, label in [('collect', '玩过'), ('do', '在玩'), ('wish', '想玩')]:
+        ws = wb.create_sheet(title=label)
+        marks = GameMark.objects.filter(owner=user, status=status).order_by("-edited_time")
+        ws.append(heading)
+        for mark in marks:
+            game = mark.game
+            title = game.title
+            summary = ','.join(game.genre) + ' / ' + ','.join(game.platform) + ' / ' + (game.release_date.strftime('%Y-%m-%d') if game.release_date else '')
+            tags = ','.join(list(map(lambda m: m.content, mark.tags)))
+            world_rating = (game.rating / 2) if game.rating else None
+            timestamp = mark.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = (mark.rating / 2) if mark.rating else None
+            text = mark.text
+            source_url = game.source_url
+            url = settings.APP_WEBSITE + game.get_absolute_url()
+            line = [title, summary, world_rating, source_url, timestamp, my_rating, tags, text, url, '']
+            ws.append(line)
+
+    review_heading = ['标题', '评论对象', '链接', '创建时间', '我的评分', '类型', '内容', '评论对象原始链接', '评论对象NeoDB链接']
+    for ReviewModel, label in [(MovieReview, '影评'), (BookReview, '书评'), (AlbumReview, '乐评'), (GameReview, '游戏评论')]:
+        ws = wb.create_sheet(title=label)
+        reviews = ReviewModel.objects.filter(owner=user).order_by("-edited_time")
+        ws.append(review_heading)
+        for review in reviews:
+            title = review.title
+            target = "《" + review.item.title + "》"
+            url = review.url
+            timestamp = review.edited_time.strftime('%Y-%m-%d %H:%M:%S')
+            my_rating = None  # (mark.rating / 2) if mark.rating else None
+            content = review.content
+            target_source_url = review.item.source_url
+            target_url = review.item.url
+            line = [title, target, url, timestamp, my_rating, label, content, target_source_url, target_url]
+            ws.append(line)
+
+    wb.save(filename=filename)
+    user.preference.export_status['marks_pending'] = False
+    user.preference.export_status['marks_file'] = filename
+    user.preference.export_status['marks_date'] = datetime.now().strftime("%Y-%m-%d %H:%M")
+    user.preference.save(update_fields=['export_status'])
diff --git a/users/templates/users/book_list.html b/users/templates/users/book_list.html
deleted file mode 100644
index ff2dfea9..00000000
--- a/users/templates/users/book_list.html
+++ /dev/null
@@ -1,282 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            <a href="{% url 'books:retrieve' mark.book.id %}">
-                                                <img src="{{ mark.book.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                <a href="{% url 'books:retrieve' mark.book.id %}" class="entity-list__entity-link">
-                                                    {{ mark.book.title }}
-                                                </a>
-                                                <a href="{{ mark.book.source_url }}">
-                                                    <span class="source-label source-label__{{ mark.book.source_site }}">{{ mark.book.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            {% comment %}                                                
-                                                <!-- {% if mark.book.rating %}
-                                                <div class="rating-star entity-list__rating-star" data-rating-score="{{ mark.book.rating | floatformat:"0" }}"></div>
-                                                <span class="entity-list__rating-score rating-score">
-                                                    {{ mark.book.rating }}
-                                                </span>
-                                                {% else %}
-                                                <div class="entity-list__rating entity-list__rating--empty"> {% trans '暂无评分' %}</div>
-                                                {% endif %} -->
-                                            {% endcomment %}
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                            {% if mark.book.pub_year %}
-                                            {{ mark.book.pub_year }}{% trans '年' %} /
-                                                {% if mark.book.pub_month %}
-                                                    {{ mark.book.pub_month }}{% trans '月' %} /
-                                                {% endif %} 
-                                            {% endif %}
-                                            
-                                            {% if mark.book.author %}
-                                            {% trans '作者' %}
-                                            {% for author in mark.book.author %}                  
-                                            {{ author }}{% if not forloop.last %},{% endif %}
-                                            {% endfor %}/
-                                            {% endif %}
-        
-                                            {% if mark.book.translator %}
-                                            {% trans '译者' %}
-                                            {% for translator in mark.book.translator %}                  
-                                            {{ translator }}{% if not forloop.last %},{% endif %}
-                                            {% endfor %}/
-                                            {% endif %}                                    
-        
-                                            {% if mark.book.orig_title %}
-                                            &nbsp;{% trans '原名' %}
-                                                {{ mark.book.orig_title }}
-                                            {% endif %}
-                                            </span>                    
-                                            <p class="entity-list__entity-brief">
-                                                {{ mark.book.brief }}
-                                            </p>
-                                            <div class="tag-collection">
-                                                {% for tag_dict in mark.book.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path
-                                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/data.html b/users/templates/users/data.html
new file mode 100644
index 00000000..d9f6a19a
--- /dev/null
+++ b/users/templates/users/data.html
@@ -0,0 +1,337 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - 数据管理</title>
+    {% include "partial/_common_libs.html" with jquery=1 %}
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            {% if messages %}
+                            <ul class="messages">
+                                {% for message in messages %}
+                                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
+                                {% endfor %}
+                            </ul>
+                            {% endif %}
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导入豆瓣标记和短评' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'sync:douban' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <input type="hidden" name="user" value="{{ request.user.id }}">
+                                            <span>{% trans '导入:' %}</span>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_book" id="syncBook" checked>
+                                                <label for="syncBook">{% trans '书' %}</label>
+                                            </div>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_movie" id="syncMovie" checked>
+                                                <label for="syncMovie">{% trans '电影' %}</label>
+                                            </div>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_music" id="syncMusic" checked>
+                                                <label for="syncMusic">{% trans '音乐' %}</label>
+                                            </div>
+                                            <div class="import-panel__checkbox">
+                                                <input type="checkbox" name="sync_game" id="syncGame" checked>
+                                                <label for="syncGame">{% trans '游戏' %}</label>
+                                            </div>
+                                            <div></div>
+                                            <span>{% trans '覆盖:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="overwrite" id="overwrite">
+                                                <label for="overwrite">{% trans '选中会覆盖现有标记' %}</label>
+                                            </div>
+                                            <div></div>
+                                            <span>{% trans '可见性:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="default_public" id="visibility" checked>
+                                                <label for="visibility">{% trans '选中后导入标记对其他用户可见;标记可见性在导入后也可更改。' %}</label>
+                                            </div>
+                                            <div></div>
+                                            <div class="import-panel__file-input">
+                                                从<a href="https://doufen.org" target="_blank">豆伴(豆坟)</a>备份导出的.xlsx文件,<strong>请勿手动修改该文件</strong>:
+                                                <input type="file" name="file" id="excelFile" required accept=".xlsx"> 
+                                            </div>
+                                            <input type="submit" class="import-panel__button" value="{% trans '导入' %}" id="uploadBtn"
+                                                {% if not latest_task is None and not latest_task.is_finished %}
+                                                disabled
+                                                {% endif %}
+                                            >
+                                        </form>
+                                        <div class="import-panel__progress"
+                                            {% if latest_task.is_finished or latest_task is None %}
+                                            style="display: none;"
+                                            {% endif %}
+                                        >
+                                            <label for="importProgress">{% trans '进度' %}</label>
+                                            <progress id="importProgress" value="{{ latest_task.finished_items }}" max="{{ latest_task.total_items }}"></progress>
+                                            <span class="float-right" id="progressPercent">{{ latest_task.get_progress | floatformat:"0" }}%</span>
+                                            <span class="clearfix"></span>
+                                        </div>
+                                        <div class="import-panel__last-task"
+                                            {% if not latest_task.is_finished %}`
+                                            style="display: none;"
+                                            {% endif %}
+                                        >
+                                            {% trans '上次导入:' %}
+                                            <span class="index">{% trans '总数' %} <span id="lastTaskTotalItems">{{ latest_task.total_items }}</span></span>
+                                            <span class="index">{% trans '同步' %} <span id="lastTaskSuccessItems">{{ latest_task.success_items }}</span></span>
+                                            <span class="index">{% trans '状态' %} <span id="lastTaskStatus">{{ latest_task.get_status_emoji }}</span></span>
+                                            <div class="import-panel__fail-urls"
+                                            {% if not latest_task.failed_urls %}
+                                                style="display: none;"
+                                            {% endif %}
+                                            >
+                                                <span>
+                                                    {% trans '失败条目链接' %}
+                                                </span>
+                                                <a class="float-right" style="cursor: pointer;" id="failedUrlsBtn">
+                                                    ▶
+                                                </a>
+                                                <script>
+                                                    $("#failedUrlsBtn").data("collapse", true);
+                                                    $("#failedUrlsBtn").on('click', ()=>{
+                                                        const btn = $("#failedUrlsBtn");
+                                                        if(btn.data("collapse") == true) {
+                                                            btn.data("collapse", false);
+                                                            btn.text("▼");
+                                                            $("#failedUrls").show();
+                                                        } else {
+                                                            btn.data("collapse", true);
+                                                            btn.text("▶");
+                                                            $("#failedUrls").hide();
+                                                        }
+                                                    });
+                                                </script>
+                                                <span class="clearfix"></span>
+                                                <ul id="failedUrls" style="display: none;">
+                                                    {% for url in latest_task.failed_urls %}
+                                                    <li>{{ url }}</li>
+                                                    {% endfor %}
+                                                </ul>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导入豆瓣评论' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:import_douban' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">
+                                                <p>从<a href="https://doufen.org" target="_blank">豆伴(豆坟)</a>备份导出的.xlsx文件:
+                                                    <input type="file" name="file" id="excel" required accept=".xlsx">
+                                                </p>
+                                                <p>可见性:
+                                                    <label for="id_visibility_0"><input type="radio" name="visibility" value="0" required="" id="id_visibility_0" checked>
+                                                     公开</label>
+                                                    <label for="id_visibility_1"><input type="radio" name="visibility" value="1" required="" id="id_visibility_1">
+                                                     仅关注者</label>
+                                                    <label for="id_visibility_2"><input type="radio" name="visibility" value="2" required="" id="id_visibility_2">
+                                                     仅自己</label>
+                                                </p>
+                                                {% if import_status.douban_pending %}
+                                                <input type="submit" class="import-panel__button" value="{% trans '备份文件已上传,请等待导入完成或刷新页面查看最新进度' %}" disabled />
+                                                {% else %}
+                                                <input type="submit" class="import-panel__button" value="{% trans '导入' %}"/>
+                                                {% endif %}
+
+                                                {% if import_status.douban_pending == 2 %}
+                                                正在等待
+                                                {% elif import_status.douban_pending == 1 %}
+                                                正在导入
+                                                    {% if import_status.douban_total %}
+                                                    共{{ import_status.douban_total }}篇,目前已处理{{ import_status.douban_processed }}篇,其中已存在{{ import_status.douban_skipped }}篇,新增{{ import_status.douban_imported }}篇
+                                                    {% endif %}
+                                                {% elif import_status.douban_file %}
+                                                    上次结果
+                                                    共计{{ import_status.douban_total }}篇,处理{{ import_status.douban_processed }}篇,其中已存在{{ import_status.douban_skipped }}篇,新增{{ import_status.douban_imported }}篇
+                                                {% endif %}
+                                            </div>
+                                            <div>
+                                                请务必在豆伴(豆坟)导出时勾选「书影音游剧」和「评论」;已经存在的评论不会被覆盖。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导入Goodreads帐号或书单' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:import_goodreads' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">输入Goodreads链接
+                                            <input type="url" name="url" value="" placeholder="例如 https://www.goodreads.com/user/show/12345-janedoe">
+                                            <input type="submit" class="import-panel__button" value="{% trans '导入' %}" id="uploadBtn" />
+                                            </div>
+                                            <div>
+                                                Goodreads用户主页链接形如 https://www.goodreads.com/user/show/12345-janedoe 将自动导入到当前用户的想读、在读、已读列表,每本书的评论导入为本站短评;
+                                                <br />
+                                                Goodreads书单链接形如 https://www.goodreads.com/review/list/12345-janedoe?shelf=name 或 https://www.goodreads.com/list/show/155086.Popular_Highlights 将自动导入成为收藏单,每本书的评论导入为收藏单条目备注。
+						<br />
+						欲导入的Goodreads用户需将Who Can View My Profile设置为anyone,导入后可改回原设置。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '导出个人数据' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:export_marks' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            {% if export_status.marks_pending %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '正在导出兼容豆伴(doufen)和NiceDB的标记、短评和评论' %}" id="uploadBtn" disabled />
+                                            {% else %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '导出兼容豆伴(doufen)和NiceDB的标记、短评和评论' %}" id="uploadBtn" />
+                                            {% endif %}
+                                            {% if export_status.marks_file %}
+                                            <a href="{% url 'users:export_marks' %}" download>下载 {{ export_status.marks_date }} 的导出</a>
+                                            {% endif %}
+                                        </form>
+                                        <!-- <form action="{% url 'users:export_reviews' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '导出评论' %}" id="uploadBtn"
+                                            >
+                                        </form> -->
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '重置所有标记和短评可见性' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:reset_visibility' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '重置' %}" id="uploadBtn" />
+                                            <div class="import-panel__checkbox">
+                                            <input type="radio" name="visibility" id="visPublic" value="0" checked>
+                                            <label for="visPublic">{% trans '公开' %}</label>
+                                            <input type="radio" name="visibility" id="visFollower" value="1">
+                                            <label for="visFollower">{% trans '仅关注者' %}</label>
+                                            <input type="radio" name="visibility" id="visSelf" value="2">
+                                            <label for="visSelf">{% trans '仅自己' %}</label>
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '更新社交关系数据' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:sync_mastodon' %}" method="POST" enctype="multipart/form-data" >
+                                            {% csrf_token %}
+                                            <input type="submit" class="import-panel__button" value="{% trans '同步' %}" id="uploadBtn" /> 上次更新时间 {{ user.mastodon_last_refresh }}
+                                            <div>
+                                            为了正确高效的展示短评和评论,{{ site_name }}会缓存你在联邦网络的关注、屏蔽和静音列表。如果你刚刚更新过帐户的上锁状态、增减过关注、静音或屏蔽,希望立即生效,可以点击这里立刻更新;这类信息也会每天自动同步。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        {% if allow_any_site %}
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '替换社交账号' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:reconnect' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">输入新社交账号所在的实例域名
+                                            <input type="input" name="domain" value="" placeholder="例如mastodon.online">
+                                            <input type="submit" class="import-panel__button" value="{% trans '登录新账号' %}" id="uploadBtn" />
+                                            </div>
+                                            <div>
+                                            替换后可使用新的联邦网络身份来登录{{ site_name }}和控制数据可见性,已有的标记评论收藏单等数据不受影响。
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        {% endif %}
+
+                        <div class="main-section-wrapper">
+                            <div class="tools-section-wrapper">
+                                <div class="import-panel">
+                                    <h5 class="import-panel__label">{% trans '删除数据和帐号信息' %}</h5>
+                                    <div class="import-panel__body">
+                                        <form action="{% url 'users:clear_data' %}" method="POST" >
+                                            {% csrf_token %}
+                                            <div class="import-panel__checkbox">输入完整的 用户名@实例名 以确认删除
+                                            <input type="input" name="verification" value="" placeholder="user@mastodon.social">
+                                            <input type="submit" class="import-panel__button" value="{% trans '永久删除' %}" id="uploadBtn" />
+                                            </div>
+                                            <div>
+                                            删除将无法撤销
+                                            </div>
+                                        </form>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+
+        </div>
+
+        {% include "partial/_footer.html" %}
+    </div>
+
+    <div id="queryProgressURL" data-url="{% url 'sync:progress' %}"></div>
+    <div id="querySyncInfoURL" data-url="{% url 'sync:last' %}"></div>
+</body>
+
+
+</html>
diff --git a/users/templates/users/game_list.html b/users/templates/users/game_list.html
deleted file mode 100644
index f4e97145..00000000
--- a/users/templates/users/game_list.html
+++ /dev/null
@@ -1,272 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load humanize %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            <a href="{% url 'games:retrieve' mark.game.id %}">
-                                                <img src="{{ mark.game.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                <a href="{% url 'games:retrieve' mark.game.id %}" class="entity-list__entity-link">
-                                                    {{ mark.game.title }}
-                                                </a>
-                                                <a href="{{ mark.game.source_url }}">
-                                                    <span class="source-label source-label__{{ mark.game.source_site }}">{{ mark.game.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                                {% if mark.game.other_title %}{% trans '别名' %}
-                                                {% for other_title in mark.game.other_title %}
-                                                {{ other_title }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                                
-                                                {% if mark.game.developer %}{% trans '开发商' %}
-                                                {% for developer in mark.game.developer %}
-                                                {{ developer }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                                
-                                                {% if mark.game.genre %}{% trans '类型' %}
-                                                {% for genre in mark.game.genre %}
-                                                {{ genre }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                                
-                                                {% if mark.game.platform %}{% trans '平台' %}
-                                                {% for platform in mark.game.platform %}
-                                                {{ platform }}{% if not forloop.last %} {% endif %}
-                                                {% endfor %}/
-                                                {% endif %}
-                                            </span>       
-                                          
-                                            <p class="entity-list__entity-brief">
-                                                {{ mark.game.brief }}
-                                            </p>
-                                            <div class="tag-collection">
-                                                {% for tag_dict in mark.game.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path
-                                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/home.html b/users/templates/users/home.html
index 1d49c7f7..ddad07d3 100644
--- a/users/templates/users/home.html
+++ b/users/templates/users/home.html
@@ -11,18 +11,16 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    
     {% if user == request.user %}
-        <title>{% trans 'NiceDB - 我的主页' %}</title>
+        <title>{{ site_name }} - {% trans '我的个人主页' %}</title>
     {% else %}
-        <title>{% trans 'NiceDB - ' %}{{user.username}}{% trans '的主页' %}</title>
+        <title>{{ site_name }} - {{user.display_name}}</title>
     {% endif %}
-        
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'js/mastodon.js' %}"></script>
     <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-
 </head>
 
 <body>
@@ -44,7 +42,7 @@
                                     {{ wish_book_count }}
                                 </span>
                                 {% if wish_book_more %}
-                                <a href="{% url 'users:book_list' user.id 'wish' %}"
+                                <a href="{% url 'users:book_list' user.mastodon_username 'wish' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                                 <ul class="entity-sort__entity-list">
@@ -72,7 +70,7 @@
                                     {{ do_book_count }}
                                 </span>
                                 {% if do_book_more %}
-                                <a href="{% url 'users:book_list' user.id 'do' %}"
+                                <a href="{% url 'users:book_list' user.mastodon_username 'do' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -101,7 +99,7 @@
                                     {{ collect_book_count }}
                                 </span>
                                 {% if collect_book_more %}
-                                <a href="{% url 'users:book_list' user.id 'collect' %}"
+                                <a href="{% url 'users:book_list' user.mastodon_username 'collect' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -122,6 +120,35 @@
                                 </ul>
                             </div>
 
+                            <div class="entity-sort" id="bookReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的书籍' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ book_reviews_count }}
+                                </span>
+                                {% if book_reviews_more %}
+                                <a href="{% url 'users:book_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for book_review in book_reviews %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'books:retrieve' book_review.book.id %}">
+                                            <img src="{{ book_review.book.cover|thumb:'normal' }}"
+                                                alt="{{book_review.book.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{book_review.book.title}}">{{ book_review.book.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
                             <div class="entity-sort" id="movieWish">
                                 <h5 class="entity-sort__label">
                                     {% trans '想看的电影/剧集' %}
@@ -130,7 +157,7 @@
                                     {{ wish_movie_count }}
                                 </span>
                                 {% if wish_movie_more %}
-                                <a href="{% url 'users:movie_list' user.id 'wish' %}"
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'wish' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -159,7 +186,7 @@
                                     {{ do_movie_count }}
                                 </span>
                                 {% if do_movie_more %}
-                                <a href="{% url 'users:movie_list' user.id 'do' %}"
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'do' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -188,7 +215,7 @@
                                     {{ collect_movie_count }}
                                 </span>
                                 {% if collect_movie_more %}
-                                <a href="{% url 'users:movie_list' user.id 'collect' %}"
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'collect' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -209,6 +236,35 @@
                                 </ul>
                             </div>
 
+                            <div class="entity-sort" id="movieReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的电影/剧集' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ movie_reviews_count }}
+                                </span>
+                                {% if movie_reviews_more %}
+                                <a href="{% url 'users:movie_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for movie_review in movie_reviews %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'movies:retrieve' movie_review.movie.id %}">
+                                            <img src="{{ movie_review.movie.cover|thumb:'normal' }}"
+                                                alt="{{movie_review.movie.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{movie_review.movie.title}}">{{ movie_review.movie.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
                             <div class="entity-sort" id="musicWish">
                                 <h5 class="entity-sort__label">
                                     {% trans '想听的音乐' %}
@@ -217,7 +273,7 @@
                                     {{ wish_music_count }}
                                 </span>
                                 {% if wish_music_more %}
-                                <a href="{% url 'users:music_list' user.id 'wish' %}"
+                                <a href="{% url 'users:music_list' user.mastodon_username 'wish' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -258,7 +314,7 @@
                                     {{ do_music_count }}
                                 </span>
                                 {% if do_music_more %}
-                                <a href="{% url 'users:music_list' user.id 'do' %}"
+                                <a href="{% url 'users:music_list' user.mastodon_username 'do' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -295,7 +351,7 @@
                                     {{ collect_music_count }}
                                 </span>
                                 {% if collect_music_more %}
-                                <a href="{% url 'users:music_list' user.id 'collect' %}"
+                                <a href="{% url 'users:music_list' user.mastodon_username 'collect' %}"
                                     class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
@@ -324,6 +380,43 @@
                                 </ul>
                             </div>
 
+                            <div class="entity-sort" id="musicReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的音乐' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ music_reviews_count }}
+                                </span>
+                                {% if music_reviews_more %}
+                                <a href="{% url 'users:music_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for music_review in music_reviews %}
+                                    <li class="entity-sort__entity">
+                                        {% if music_review.type == 'album' %}
+                                        <a href="{% url 'music:retrieve_album' music_review.album.id %}">
+                                            <img src="{{ music_review.album.cover|thumb:'normal' }}"
+                                                alt="{{music_review.album.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{music_review.album.title}}">{{ music_review.album.title }}</span>
+                                        </a>
+                                        {% else %}
+                                        <a href="{% url 'music:retrieve_song' music_review.song.id %}">
+                                            <img src="{{ music_review.song.cover|thumb:'normal' }}"
+                                                alt="{{music_review.song.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{music_review.song.title}}">{{ music_review.song.title }}</span>
+                                        </a>
+                                        {% endif %}
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
                             <div class="entity-sort" id="gameWish">
                                 <h5 class="entity-sort__label">
                                     {% trans '想玩的游戏' %}
@@ -332,7 +425,7 @@
                                     {{ wish_game_count }}
                                 </span>
                                 {% if wish_game_more %}
-                                <a href="{% url 'users:game_list' user.id 'wish' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'users:game_list' user.mastodon_username 'wish' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                             
                                 <ul class="entity-sort__entity-list">
@@ -360,7 +453,7 @@
                                     {{ do_game_count }}
                                 </span>
                                 {% if do_game_more %}
-                                <a href="{% url 'users:game_list' user.id 'do' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'users:game_list' user.mastodon_username 'do' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                             
                                 <ul class="entity-sort__entity-list">
@@ -388,7 +481,7 @@
                                     {{ collect_game_count }}
                                 </span>
                                 {% if collect_game_more %}
-                                <a href="{% url 'users:game_list' user.id 'collect' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
+                                <a href="{% url 'users:game_list' user.mastodon_username 'collect' %}" class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
                             
                                 <ul class="entity-sort__entity-list">
@@ -407,411 +500,174 @@
                                 </ul>
                             </div>
 
-                        </div>
-                            
-                            {% if user == request.user %}
-                                
-                            <div class="entity-sort-control">
-                                <div class="entity-sort-control__button" id="sortEditButton">
-                                    <span class="entity-sort-control__text" id="sortEditText">
-                                        {% trans '编辑布局' %}
-                                    </span>
-                                    <span class="entity-sort-control__text" id="sortSaveText" style="display: none;">
-                                        {% trans '保存' %}
-                                    </span>
-                                    <span class="icon-edit" id="sortEditIcon">
-                                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 383.947 383.947">
-                                            <polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893    " />
-                                            <path
-                                                d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04     C386.027,77.92,386.027,64.373,377.707,56.053z" />
-                                        </svg>
-                                    </span>
-                                    <span class="icon-save" id="sortSaveIcon" style="display: none;">
-                                        <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 384 384" >
-                                            <path
-                                                d="M298.667,0h-256C19.093,0,0,19.093,0,42.667v298.667C0,364.907,19.093,384,42.667,384h298.667    C364.907,384,384,364.907,384,341.333v-256L298.667,0z M192,341.333c-35.307,0-64-28.693-64-64c0-35.307,28.693-64,64-64    s64,28.693,64,64C256,312.64,227.307,341.333,192,341.333z M256,128H42.667V42.667H256V128z" />
-                                        </svg>
-                                    </span>
-                                </div>
-                                <div class="entity-sort-control__button" id="sortExitButton" style="display: none;">
-                                    <span class="entity-sort-control__text">
-                                        {% trans '取消' %}
-                                    </span>
-                                </div>
-                            </div>
-                            <div class="entity-sort-control__button entity-sort-control__button--float-right" id="toggleDisplayButtonTemplate" style="display: none;">
-                                <span class="showText" style="display: none;">
-                                    {% trans '显示' %}
+                            <div class="entity-sort" id="gameReviewed">
+                                <h5 class="entity-sort__label">
+                                    {% trans '评论过的游戏' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ game_reviews_count }}
                                 </span>
-                                <span class="hideText" style="display: none;">
-                                    {% trans '隐藏' %}
-                                </span>
-                            </div>
-                            <form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
-                                {% csrf_token %}
-                                <input type="hidden" name="layout">
-                            </form>
-                            <script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.10.0/html5sortable.min.js"
-                                integrity="sha512-tBlVMq89XaEC9iU5LyRjP2Vxs8SmVhEHGbv2Co6SbGa14Wsxy2qZN0jadrN+Xn5AifORaUbvZcG21/ExcNfWDA=="
-                                crossorigin="anonymous"></script>
-                            <script src="{% static 'js/sort_layout.js' %}"></script>
-                            {% endif %}
-                            <script>
-                                const initialLayoutData = JSON.parse("{{ layout|escapejs }}");
-                                // initialize sort element visibility and order
-                                    initialLayoutData.forEach(elem => {
-                                        // False to false, True to true
-                                        if (elem.visibility === "False") {
-                                            elem.visibility = false;
-                                        } else {
-                                            elem.visibility = true;
-                                        }
-                                        // set visiblity
-                                        $('#' + elem.id).data('visibility', elem.visibility);
-                                        if (!elem.visibility) {
-                                            $('#' + elem.id).hide();
-                                        }
-                                        // order
-                                        $('#' + elem.id).appendTo('.main-section-wrapper');
-                                    });
-                            </script>
-
-                    </div>
-
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
+                                {% if game_reviews_more %}
+                                <a href="{% url 'users:game_list' user.mastodon_username 'reviewed' %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
                                 {% endif %}
 
-                            </div>
-                        </div>
+                                <ul class="entity-sort__entity-list">
+                                    {% for game_review in game_reviews %}
+                                    <li class="entity-sort__entity">
 
-                        <!-- contains relations, reports, and upload excel entry -->
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
+                                        <a href="{% url 'games:retrieve' game_review.game.id %}">
+                                            <img src="{{ game_review.game.cover|thumb:'normal' }}"
+                                                alt="{{game_review.game.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{game_review.game.title}}">{{ game_review.game.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
+                            <div class="entity-sort" id="collectionCreated">
+                                <h5 class="entity-sort__label">
+                                    {% trans '创建的收藏单' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ collections_count }}
+                                </span>
+                                {% if collections_more %}
+                                <a href="{% url 'users:collection_list' user.mastodon_username %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+                                {% if user == request.user %}
+                                <a href="{% url 'collection:create' %}"class="entity-sort__more-link">{% trans '新建' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for collection in collections %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            <img src="{{ collection.cover|thumb:'normal' }}"
+                                                alt="{{collection.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{collection.title}}">{{ collection.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
+                            <div class="entity-sort" id="collectionMarked">
+                                <h5 class="entity-sort__label">
+                                    {% trans '关注的收藏单' %}
+                                </h5>
+                                <span class="entity-sort__count">
+                                    {{ marked_collections_count }}
+                                </span>
+                                {% if marked_collections_more %}
+                                <a href="{% url 'users:marked_collection_list' user.mastodon_username %}"
+                                    class="entity-sort__more-link">{% trans '更多' %}</a>
+                                {% endif %}
+
+                                <ul class="entity-sort__entity-list">
+                                    {% for collection in marked_collections %}
+                                    <li class="entity-sort__entity">
+
+                                        <a href="{% url 'collection:retrieve' collection.id %}">
+                                            <img src="{{ collection.cover|thumb:'normal' }}"
+                                                alt="{{collection.title}}" class="entity-sort__entity-img">
+                                            <span class="entity-sort__entity-name"
+                                                title="{{collection.title}}">{{ collection.title }}</span>
+                                        </a>
+                                    </li>
+                                    {% empty %}
+                                    <div>暂无记录</div>
+                                    {% endfor %}
+                                </ul>
+                            </div>
+
+                        </div>
+                            
+                        {% if user == request.user %}
+                            
+                        <div class="entity-sort-control">
+                            <div class="entity-sort-control__button" id="sortEditButton">
+                                <span class="entity-sort-control__text" id="sortEditText">
+                                    {% trans '编辑布局' %}
+                                </span>
+                                <span class="entity-sort-control__text" id="sortSaveText" style="display: none;">
+                                    {% trans '保存' %}
+                                </span>
+                                <span class="icon-edit" id="sortEditIcon">
+                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 383.947 383.947">
+                                        <polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893    " />
+                                        <path
+                                            d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04     C386.027,77.92,386.027,64.373,377.707,56.053z" />
+                                    </svg>
+                                </span>
+                                <span class="icon-save" id="sortSaveIcon" style="display: none;">
+                                    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 384 384" >
+                                        <path
+                                            d="M298.667,0h-256C19.093,0,0,19.093,0,42.667v298.667C0,364.907,19.093,384,42.667,384h298.667    C364.907,384,384,364.907,384,341.333v-256L298.667,0z M192,341.333c-35.307,0-64-28.693-64-64c0-35.307,28.693-64,64-64    s64,28.693,64,64C256,312.64,227.307,341.333,192,341.333z M256,128H42.667V42.667H256V128z" />
                                     </svg>
                                 </span>
                             </div>
-                            <div class="relation-dropdown__body">
-                                <div
-                                    class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-
-                                </div>
-                                
-                                <!-- import douban data -->                                
-                                {% if user == request.user %}                                    
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                                    <div class="import-panel">
-                                        <h5 class="import-panel__label">{% trans '导入豆瓣标记数据' %}</h5>
-                                        <span id="importHelp" class="import-panel__help">?</span>
-                                        <div class="import-panel__body">
-                                            <form action="{% url 'sync:douban' %}" method="POST" enctype="multipart/form-data" >
-
-                                                {% csrf_token %}
-                                                <input type="hidden" name="user" value="{{ request.user.id }}">
-                                                <span>{% trans '导入:' %}</span>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_book" id="syncBook">
-                                                    <label for="syncBook">{% trans '书' %}</label>
-                                                </div>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_movie" id="syncMovie">
-                                                    <label for="syncMovie">{% trans '电影' %}</label>
-                                                </div>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_music" id="syncMusic">
-                                                    <label for="syncMusic">{% trans '音乐' %}</label>
-                                                </div>
-                                                <div class="import-panel__checkbox">
-                                                    <input type="checkbox" name="sync_game" id="syncGame">
-                                                    <label for="syncGame">{% trans '游戏' %}</label>
-                                                </div>
-                                                <div></div>
-                                                <span>{% trans '覆盖:' %}</span>
-                                                <div class="import-panel__checkbox import-panel__checkbox--last">
-                                                    <input type="checkbox" name="overwrite" id="overwrite">
-                                                    <label for="overwrite">{% trans '覆盖原有标记' %}</label>
-                                                </div>
-                                                <span id="overwriteHelp" class="import-panel__help">?</span>
-                                                <div></div>
-                                                <span>{% trans '可见性:' %}</span>
-                                                <div class="import-panel__checkbox import-panel__checkbox--last">
-                                                    <input type="checkbox" name="default_public" id="visibility">
-                                                    <label for="visibility">{% trans '公开' %}</label>
-                                                </div>
-                                                <span id="visibilityHelp" class="import-panel__help">?</span>
-                                                <div></div>
-                                                <div class="import-panel__file-input">
-                                                    <input type="file" name="file" id="excelFile" required accept=".xlsx">
-                                                </div>
-                                                <input type="submit" class="import-panel__button" value="{% trans '导入' %}" id="uploadBtn"
-                                                    {% if not latest_task is None and not latest_task.is_finished %}
-                                                    disabled
-                                                    {% endif %}
-                                                >
-                                            </form>
-                                            <div class="import-panel__progress"
-                                                {% if latest_task.is_finished or latest_task is None %}
-                                                style="display: none;"
-                                                {% endif %}
-                                            >
-                                                <label for="importProgress">{% trans '进度' %}</label>
-                                                <progress id="importProgress" value="{{ latest_task.finished_items }}" max="{{ latest_task.total_items }}"></progress>
-                                                <span class="float-right" id="progressPercent">{{ latest_task.get_progress | floatformat:"0" }}%</span>
-                                                <span class="clearfix"></span>
-                                            </div>
-                                            <div class="import-panel__last-task"
-                                                {% if not latest_task.is_finished %}`
-                                                style="display: none;"
-                                                {% endif %}
-                                            >
-                                                {% trans '上次导入:' %}
-                                                <span class="index">{% trans '总数' %} <span id="lastTaskTotalItems">{{ latest_task.total_items }}</span></span>
-                                                <span class="index">{% trans '同步' %} <span id="lastTaskSuccessItems">{{ latest_task.success_items }}</span></span>
-                                                <span class="index">{% trans '状态' %} <span id="lastTaskStatus">{{ latest_task.get_status_emoji }}</span></span>
-                                                <div class="import-panel__fail-urls"
-                                                {% if not latest_task.failed_urls %}
-                                                    style="display: none;"
-                                                {% endif %}
-                                                >
-                                                    <span>
-                                                        {% trans '失败条目链接' %}
-                                                    </span>
-                                                    <a class="float-right" style="cursor: pointer;" id="failedUrlsBtn">
-                                                        ▶
-                                                    </a>
-                                                    <script>
-                                                        $("#failedUrlsBtn").data("collapse", true);
-                                                        $("#failedUrlsBtn").click(()=>{
-                                                            const btn = $("#failedUrlsBtn");
-                                                            if(btn.data("collapse") == true) {
-                                                                btn.data("collapse", false);
-                                                                btn.text("▼");
-                                                                $("#failedUrls").show();
-                                                            } else {
-                                                                btn.data("collapse", true);
-                                                                btn.text("▶");
-                                                                $("#failedUrls").hide();
-                                                            }
-                                                        });
-                                                    </script>
-                                                    <span class="clearfix"></span>
-                                                    <ul id="failedUrls" style="display: none;">
-                                                        {% for url in latest_task.failed_urls %}
-                                                        <li>{{ url }}</li>
-                                                        {% endfor %}
-                                                    </ul>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-                                {% endif %}
-                                    
-                                <div
-                                    class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                                    {% if request.user.is_staff and request.user == user%}
-                                    <div class="report-panel">
-                                        <h5 class="report-panel__label">{% trans '举报信息' %}</h5>
-                                        <a class="report-panel__all-link"
-                                            href="{% url 'users:manage_report' %}">全部举报</a>
-                                        <div class="report-panel__body">
-                                            <ul class="report-panel__report-list">
-                                                {% for report in reports %}
-                                                <li class="report-panel__report">
-                                                    <a href="{% url 'users:home' report.submit_user.id %}"
-                                                        class="report-panel__user-link">{{ report.submit_user }}</a>{% trans '举报了' %}<a
-                                                        href="{% url 'users:home' report.reported_user.id %}"
-                                                        class="report-panel__user-link">{{ report.reported_user }}</a>
-                                                </li>
-                                                {% empty %}
-                                                <div>{% trans '暂无新举报' %}</div>
-                                                {% endfor %}
-    
-                                            </ul>
-                                        </div>
-                                    </div>
-                                    {% endif %}
-                                </div>
+                            <div class="entity-sort-control__button" id="sortExitButton" style="display: none;">
+                                <span class="entity-sort-control__text">
+                                    {% trans '取消' %}
+                                </span>
                             </div>
                         </div>
+                        <div class="entity-sort-control__button entity-sort-control__button--float-right" id="toggleDisplayButtonTemplate" style="display: none;">
+                            <span class="showText" style="display: none;">
+                                {% trans '显示' %}
+                            </span>
+                            <span class="hideText" style="display: none;">
+                                {% trans '隐藏' %}
+                            </span>
+                        </div>
+                        <form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
+                            {% csrf_token %}
+                            <input type="hidden" name="layout">
+                        </form>
+                        <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/html5sortable.min.js" crossorigin="anonymous"></script>
+                        <script src="{% static 'js/sort_layout.js' %}"></script>
+                        {% endif %}
+                        <script>
+                            const initialLayoutData = JSON.parse("{{ layout|escapejs }}");
+                            // initialize sort element visibility and order
+                                initialLayoutData.forEach(elem => {
+                                    // False to false, True to true
+                                    if (elem.visibility === "False") {
+                                        elem.visibility = false;
+                                    } else {
+                                        elem.visibility = true;
+                                    }
+                                    // set visiblity
+                                    $('#' + elem.id).data('visibility', elem.visibility);
+                                    if (!elem.visibility) {
+                                        $('#' + elem.id).hide();
+                                    }
+                                    // order
+                                    $('#' + elem.id).appendTo('.main-section-wrapper');
+                                });
+                        </script>
 
                     </div>
+
+                    {% include "partial/_sidebar.html" %}
                 </div>
             </section>
-
         </div>
-            
         {% include "partial/_footer.html" %}
     </div>
 
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <div id="queryProgressURL" data-url="{% url 'sync:progress' %}"></div>
-    <div id="querySyncInfoURL" data-url="{% url 'sync:last' %}"></div>
-    <!--current user mastodon id-->
-    
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-        
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-
     {% if unread_announcements %}
-    <div id="modals">
-        <style>
-            .bottom-link {
-                margin-top: 30px; text-align: center; margin-bottom: 5px;
-            }
-            .bottom-link a {
-                color: #ccc;
-            }
-        </style>
-        <div class="announcement-modal modal">
-            <div class="announcement-modal__head">
-                <h4 class="announcement-modal__title">{% trans '公告' %}</h4>
-        
-                <span class="announcement-modal__close-button modal-close">
-                    <span class="icon-cross">
-                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                            <polygon
-                                points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
-                            </polygon>
-                        </svg>
-                    </span>
-                </span>
-            </div>
-            <div class="announcement-modal__body">
-                <ul>
-                    {% for ann in unread_announcements %}
-                        <li class="announcement">
-                            <a href="{% url 'management:retrieve' ann.pk %}">
-                                <h5 class="announcement__title">{{ ann.title }}</h5>
-                            </a>
-                            <span class="announcement__datetime">{{ ann.created_time }}</span>
-                            <p class="announcement__content">{{ ann.get_plain_content | truncate:200 }}</p>
-                        </li>
-                        {% if not forloop.last %}
-                            <div class="dividing-line" style="border-top-style: dashed;"></div>
-                        {% endif %}
-                    {% endfor %}
-                </ul>
-                <div class="bottom-link">
-                    <a href="{% url 'management:list' %}">{% trans '查看全部公告' %}</a>
-                </div>
-            </div>
-        </div>
-    </div>
-    <div class="bg-mask"></div>
+    {% include "partial/_announcement.html" %}
     {% endif %}
-    
-    
-    <script>
-        // because the modal and mask elements only exist when there are new announcements
-        $(".announcement-modal").show();
-        $(".bg-mask").show();
-        $(".modal-close").on('click', function () {
-            $(this).parents(".modal").hide();
-            $(".bg-mask").hide();
-        });
-
-    </script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js"
-        integrity="sha512-2rNj2KJ+D8s1ceNasTIex6z4HWyOnEYLVC3FigGOmyQCZc2eBXKgOxQmo3oKLHyfcj53uz4QMsRCWNbLd32Q1g=="
-        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/tippy.js/6.3.1/tippy.umd.min.js"
-        integrity="sha512-Ns7w8bjVjVcBVa+k3XLt0ObfsG2LQfr573HoIYtC4wh8gUKLvCx+rlggxfvsHqup6jvMAEmBtYXmhcKHL+6R5A=="
-        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-    <script>
-        tippy('#importHelp', {
-            content: "{% trans '上传导入由<a href=\"https://github.com/doufen-org/tofu\" target=\"_blank\">豆伴</a>(豆坟)导出的Excel文件,<strong>请勿手动修改该文件</strong>。部分条目由于需要登陆无法自动同步。' %}",
-            interactive: true,
-            allowHTML: true,
-            duration: 0,
-        });
-        tippy('#overwriteHelp', {
-            content: "{% trans '在导入之前如果已经在本站标记了某一个条目,是否使用来自豆瓣的标记覆盖原有的。' %}",
-            interactive: true,
-            allowHTML: true,
-            duration: 0,
-        });
-        tippy('#visibilityHelp', {
-            content: "{% trans '所同步的标记可见性是否为公开;对于已有的标记即便覆盖也不会改变可见性。' %}",
-            interactive: true,
-            allowHTML: true,
-            duration: 0,
-        });
-    </script>
 </body>
-
-
 </html>
\ No newline at end of file
diff --git a/users/templates/users/home_anonymous.html b/users/templates/users/home_anonymous.html
new file mode 100644
index 00000000..946ac96e
--- /dev/null
+++ b/users/templates/users/home_anonymous.html
@@ -0,0 +1,17 @@
+{% load static %}
+{% load i18n %}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="refresh" content="0;URL={{ login_url }}" />
+    <title>{{ site_name }} - {{ username }}@{{ site }}</title>
+    <meta property="og:title" content="{{ site_name }} - {{ username }}@{{ site }}的书影音">
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="{{ request.build_absolute_uri }}">
+    <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.jpg' %}">
+</head>
+<body>
+    <a href="https://{{ site }}/@{{ username }}" rel="me" style="display:none;">Mastodon homepage</a>
+</body>
+</html>
\ No newline at end of file
diff --git a/users/templates/users/item_list.html b/users/templates/users/item_list.html
new file mode 100644
index 00000000..321ad0fe
--- /dev/null
+++ b/users/templates/users/item_list.html
@@ -0,0 +1,94 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - {{ user.mastodon_username }} {{ list_title }}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+        
+            <section id="content" class="container">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            <div class="entity-list">
+
+                                <div class="set">
+                                    <h5 class="entity-list__title">
+                                        {{ user.mastodon_username }} {{ list_title }}
+                                    </h5>
+                                </div>
+                                <ul class="entity-list__entities">
+                                    {% for mark in marks %}
+                                    {% include "partial/list_item.html" with item=mark.item hide_category=True %}
+                                    {% empty %}
+                                    <div>{% trans '无结果' %}</div>
+                                    {% endfor %} 
+                                </ul>
+                            </div>
+                            <div class="pagination">
+                            
+                                {% if marks.pagination.has_prev %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ marks.previous_page_number }}"
+                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
+                                {% endif %}
+                            
+                                {% for page in marks.pagination.page_range %}
+                            
+                                {% if page == marks.pagination.current_page %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
+                                {% else %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}" class="pagination__page-link">{{ page }}</a>
+                                {% endif %}
+                            
+                                {% endfor %}
+                            
+                                {% if marks.pagination.has_next %}
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ marks.next_page_number }}"
+                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
+                                <a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
+                                {% endif %}
+                            
+                            </div>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    <script>
+      document.body.addEventListener('htmx:configRequest', (event) => {
+        event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
+      })
+    </script>
+</body>
+
+
+</html>
diff --git a/users/templates/users/login.html b/users/templates/users/login.html
index 27a0a0fa..d843543c 100644
--- a/users/templates/users/login.html
+++ b/users/templates/users/login.html
@@ -1,4 +1,3 @@
-
 {% load i18n %}
 {% load static %}
 <!DOCTYPE html>
@@ -6,74 +5,81 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta property="og:title" content="{% trans 'NiceDB - 登录' %}">
+    <meta property="og:title" content="{{ site_name }} - 联邦宇宙书影音游戏标注平台">
+    <meta name="description" property="og:description" content="{{ site_name }}致力于为联邦宇宙居民提供一个自由、开放、互联的书籍、电影、音乐和游戏收藏评论空间">
     <meta property="og:type" content="website">
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.jpg' %}">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
+    <title>{{ site_name }} - {% trans '登录' %}</title>
+    {% include "partial/_common_libs.html" %}
+    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/js.cookie.min.js' %}"></script>
-    <title>{% trans 'NiceDB - 登录' %}</title>
-</head>
-<body>
-    <style>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js"></script>
+    <script> $(document).ready( function() { $('.delayed').remove(); $('#loginButton').prop("disabled", false); } ); </script>
+    <style type="text/css">
+        .delayed {
+          animation: 10s fadeIn;
+          animation-fill-mode: forwards;
+          visibility: hidden;
+        }
+        @keyframes fadeIn {
+          99% {
+            visibility: hidden;
+          }
+          100% {
+            visibility: visible;
+            opacity: 1;
+          }
+        }
+        input, input[type='text']:focus, input[type='search']:focus{
+            border: #84C2FB solid 1px;
+        }
+        input[type='submit'] {
+            background: #84C2FB;
+            border: #84C2FB solid 1px;
+        }
+        input:invalid#domain {
+            border: #F9A879 dashed 1px;
+        }
+        a {
+            color: #84C2FB;
+        }
         select {
             padding-left: 16px;
             padding-right: 16px;
             margin-bottom: 20px;;
         }
     </style>
+</head>
+<body>
     <div id="loginBox" class="box">
-
-            <img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
-        
-        <div id="loginButton">
-            
-            
+        <img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
+        <div>
             {% if user.is_authenticated %}
-            <a href="{% url 'common:home' %}" class="button">{% trans '前往我的主页' %}</a>
+            <a href="{% url 'common:home' %}" class="button">{% trans '前往首页' %}</a>
+            {% else %}
+            <form action="/users/connect">
+            {% if allow_any_site %}
+            <input type="search" name="domain" id="domain"
+                pattern="(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,})"
+                title="实例域名(不含@和@之前的部分),如mastodon.social"
+                placeholder="实例域名(不含@和@之前的部分),如mastodon.social"
+                autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
+            <input type='submit' value="{% trans '授权登录' %}" id="loginButton" disabled />
+            <br><a target="_blank" href="https://about.neodb.social/doc/howto/">{% trans '了解更多' %}</a>
+            <script type="text/javascript">if (Cookies.get('mastodon_domain')) $('#domain').val(Cookies.get('mastodon_domain'));</script>
             {% else %}            
-            <select name="sites" id="sitesSelect" placeholder="test">
+            <select name="domain" placeholder="test">
                 {% for site in sites %}
                 <option value="{{ site.domain_name }}" data-client-id="{{ site.client_id }}">@{{ site.domain_name }}</option>
                 {% endfor %}
             </select>
-            <button name='login'>{% trans '授权登录' %}</button>
+            <input type='submit' value="{% trans '授权登录' %}" id="loginButton" />
             {% endif %}
-                
+            </form>
+            {% endif %}
+            <div class="delayed">网页加载超时,请检查网络(翻墙)设置。</div>
         </div>
-
-    </div>
-    {% if not user.is_authenticated %}
-    
-    
-    <script>
-        {% if selected_site %}
-        $("#sitesSelect").val("{{ selected_site }}");
-        {% else %}
-        $("#sitesSelect").val($("#sitesSelect option:first").val());
-        {% endif %}
-        $("button[name=login]").click(function(e) {
-            e.preventDefault();
-            let selected =  $("#sitesSelect").find(":selected");
-            let client_id = selected.data("client-id");
-            let domain = selected.val();
-
-            Cookies.set('mastodon_domain', domain);
-            {% if debug %}
-            location.href = "https://" + domain + "/oauth/authorize?client_id=" + client_id + 
-                "&scope=read+write&redirect_uri=http://{{ request.get_host }}{% url 'users:OAuth2_login' %}" +
-                "&response_type=code";
-            {% else %}
-            location.href = "https://" + domain + "/oauth/authorize?client_id=" + client_id + 
-                "&scope=read+write&redirect_uri=https://{{ request.get_host }}{% url 'users:OAuth2_login' %}" +
-                "&response_type=code";
-            {% endif %}
-        });
-    </script>
-    {% endif %}
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/users/templates/users/manage_report.html b/users/templates/users/manage_report.html
index 35d3604b..f077a434 100644
--- a/users/templates/users/manage_report.html
+++ b/users/templates/users/manage_report.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 管理举报' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '管理举报' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
@@ -27,9 +27,9 @@
                         
                         {% for report in reports %}
                             <div class="report">
-                                <a href="{% url 'users:home' report.submit_user.id %}">{{ report.submit_user.username }}</a>
+                                <a href="{% url 'users:home' report.submit_user.mastodon_username %}">{{ report.submit_user.username }}</a>
                                 {% trans '举报了' %}
-                                <a href="{% url 'users:home' report.reported_user.id %}">{{ report.reported_user.username }}</a>
+                                <a href="{% url 'users:home' report.reported_user.mastodon_username %}">{{ report.reported_user.username }}</a>
                                 @{{ report.submitted_time }}
                                 
                                 {% if report.image %}
@@ -49,12 +49,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/users/templates/users/movie_list.html b/users/templates/users/movie_list.html
deleted file mode 100644
index fa21d29a..00000000
--- a/users/templates/users/movie_list.html
+++ /dev/null
@@ -1,285 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load humanize %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            <a href="{% url 'movies:retrieve' mark.movie.id %}">
-                                                <img src="{{ mark.movie.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                <a href="{% url 'movies:retrieve' mark.movie.id %}" class="entity-list__entity-link">
-                                                    {% if mark.movie.season %}
-                                                    {{ mark.movie.title }} {% trans '第' %}{{ mark.movie.season|apnumber }}{% trans '季' %} {{ mark.movie.orig_title }} Season
-                                                    {{ mark.movie.season }}
-                                                    {% if mark.movie.year %}({{ mark.movie.year }}){% endif %}
-                                                        
-                                                    {% else %}
-                                                    {{ mark.movie.title }} {{ mark.movie.orig_title }}
-                                                    {% if mark.movie.year %}({{ mark.movie.year }}){% endif %}
-                                                    {% endif %}
-                                                </a>
-                                                <a href="{{ mark.movie.source_url }}">
-                                                    <span class="source-label source-label__{{ mark.movie.source_site }}">{{ mark.movie.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-
-                                            
-                                        {% if mark.movie.director %}{% trans '导演' %}
-                                        {% for director in mark.movie.director %}
-                                        {{ director }}{% if not forloop.last %} {% endif %}
-                                        {% endfor %}/
-                                        {% endif %}
-        
-                                        {% if mark.movie.genre %}{% trans '类型' %}
-                                        {% for genre in mark.movie.get_genre_display %}
-                                        {{ genre }}{% if not forloop.last %} {% endif %}
-                                        {% endfor %}/
-                                        {% endif %}
-
-                                        {% if mark.movie.other_title %}{% trans '又名' %}
-                                        {% for other_title in mark.movie.other_title %}
-                                        {{ other_title }}{% if not forloop.last %} {% endif %}
-                                        {% endfor %}
-                                        {% endif %}
-                                            </span>       
-                                            <span class="entity-list__entity-info entity-list__entity-info--full-length">
-                                            {% if mark.movie.actor %}{% trans '主演' %}
-                                            {% for actor in mark.movie.actor %}
-                                            <span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>{{ actor }}</span>
-                                            {% if forloop.counter <= 5 %}
-                                                {% if not forloop.counter == 5 %} / {% endif %}
-                                            {% endif %}
-                                            {% endfor %}
-                                            {% endif %}
-                                            </span>                                                
-                                            <p class="entity-list__entity-brief">
-                                                {{ mark.movie.brief }}
-                                            </p>
-                                            <div class="tag-collection">
-                                                {% for tag_dict in mark.movie.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path
-                                                                        d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/music_list.html b/users/templates/users/music_list.html
deleted file mode 100644
index 18c0fe74..00000000
--- a/users/templates/users/music_list.html
+++ /dev/null
@@ -1,290 +0,0 @@
-{% load static %}
-{% load i18n %}
-{% load l10n %}
-{% load humanize %}
-{% load admin_url %}
-{% load mastodon %}
-{% load oauth_token %}
-{% load truncate %}
-{% load thumb %}
-
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="{% static 'lib/js/rating-star.js' %}"></script>
-    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
-    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-</head>
-
-<body>
-    <div id="page-wrapper">
-        <div id="content-wrapper">
-            {% include "partial/_navbar.html" %}
-        
-            <section id="content" class="container">
-                <div class="grid grid--reverse-order">
-                    <div class="grid__main grid__main--reverse-order">
-                        <div class="main-section-wrapper">
-                            <div class="entity-list">
-
-                                <div class="set">
-                                    <h5 class="entity-list__title">
-                                        {{ user.username }}{{ list_title }}
-                                    </h5>
-                                </div>
-                                <ul class="entity-list__entities">
-                                    
-                                    {% for mark in marks %}
-                                    
-                                    {% with mark.music as music %}
-                                        
-                                    <li class="entity-list__entity">
-                                        <div class="entity-list__entity-img-wrapper">
-                                            {% if music.category_name|lower == 'album' %}
-                                            <a href="{% url 'music:retrieve_album' music.id %}">
-                                                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                            {% elif music.category_name|lower == 'song' %}
-                                            <a href="{% url 'music:retrieve_song' music.id %}">
-                                                <img src="{{ music.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img">
-                                            </a>
-                                            {% endif %}
-                                        </div>
-                                        <div class="entity-list__entity-text">
-                                            <div class="entity-list__entity-title">
-                                                {% if music.category_name|lower == 'album' %}
-                                                <a href="{% url 'music:retrieve_album' music.id %}" class="entity-list__entity-link">
-                                                    {{ music.title }}
-                                                </a>
-                                                {% elif music.category_name|lower == 'song' %}
-                                                <a href="{% url 'music:retrieve_song' music.id %}" class="entity-list__entity-link">
-                                                    {{ music.title }}
-                                                </a>
-                                                {% endif %}
-                                                <a href="{{ music.source_url }}">
-                                                    <span class="source-label source-label__{{ music.source_site }}">{{ music.get_source_site_display }}</span>
-                                                </a>
-                                            </div>
-                                            <span class="entity-list__entity-info ">
-                                                {% if music.artist %}{% trans '艺术家' %}
-                                                {% for artist in music.artist %}
-                                                <span>{{ artist }}</span>
-                                                {% if not forloop.last %} {% endif %}
-                                                {% endfor %}
-                                                {% endif %}
-                                            
-                                                {% if music.genre %}/ {% trans '流派' %}
-                                                {{ music.genre }}
-                                                {% endif %}
-                                            
-                                                {% if music.release_date %}/ {% trans '发行日期' %}
-                                                {{ music.release_date }}
-                                                {% endif %}
-                                            </span>
-                                            {% if music.brief %}
-                                            <p class="entity-list__entity-brief">
-                                                {{ music.brief }}
-                                            </p>
-                                            {% elif music.category_name|lower == 'album' %}
-                                            <p class="entity-list__entity-brief">
-                                                {% trans '曲目:' %}{{ music.track_list }}
-                                            </p>
-                                            {% else %}
-                                            <!-- song -->
-                                            <p class="entity-list__entity-brief">
-                                                {% trans '所属专辑:' %}{{ music.album }}
-                                            </p>
-                                            {% endif %}
-                                            <div class="tag-collection">
-                                                {% for tag_dict in music.tag_list %}
-                                                {% for k, v in tag_dict.items %}
-                                                {% if k == 'content' %}
-                                                <span class="tag-collection__tag">
-                                                    <a href="{% url 'common:search' %}?tag={{ v }}">{{ v }}</a>
-                                                </span>
-                                                {% endif %}
-                                                {% endfor %}
-                                                {% endfor %}
-                                            </div>
-                                            <div class="clearfix"></div>
-                                            <div class="dividing-line dividing-line--dashed"></div>
-                                            <div class="entity-marks" style="margin-bottom: 0;">
-                                                <ul class="entity-marks__mark-list">
-                                                    <li class="entity-marks__mark">
-
-                                                        {% if mark.rating %}
-                                                        <span class="entity-marks__rating-star rating-star"
-                                                            data-rating-score="{{ mark.rating | floatformat:"0" }}" style="left: -4px;"></span>
-                                                        {% endif %}
-                                                        {% if mark.is_private %}
-                                                        <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
-                                                                    <path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
-                                                                </svg></span>
-                                                        {% endif %}
-                                                        <span class="entity-marks__mark-time">{% trans '于' %} {{ mark.edited_time }} {% trans '标记' %}</span>
-                                                        {% if mark.text %}
-                                                        <p class="entity-marks__mark-content">{{ mark.text }}</p>
-                                                        {% endif %}
-                                                    </li>
-                                                </ul>
-                                            </div>                                            
-                                        </div>
-
-                                    </li>
-
-                                    {% endwith %}
-
-                                    {% empty %}
-                                    <div>{% trans '无结果' %}</div>
-                                    {% endfor %}
-                                    <!-- user mark -->
- 
-                                        
-                                </ul>
-                            </div>
-                            <div class="pagination">
-                            
-                                {% if marks.pagination.has_prev %}
-                                <a href="?page=1" class="pagination__nav-link pagination__nav-link">&laquo;</a>
-                                <a href="?page={{ marks.previous_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
-                                {% endif %}
-                            
-                                {% for page in marks.pagination.page_range %}
-                            
-                                {% if page == marks.pagination.current_page %}
-                                <a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
-                                {% else %}
-                                <a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
-                                {% endif %}
-                            
-                                {% endfor %}
-                            
-                                {% if marks.pagination.has_next %}
-                                <a href="?page={{ marks.next_page_number }}"
-                                    class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
-                                <a href="?page={{ marks.pagination.last_page }}" class="pagination__nav-link">&raquo;</a>
-                                {% endif %}
-                            
-                            </div>
-                        </div>
-                    </div>
-        
-                    <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
-                        <div class="aside-section-wrapper aside-section-wrapper--no-margin">
-                            <div class="user-profile" id="userInfoCard">
-                                <div class="user-profile__header">
-                                    <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
-                                    <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
-                                        <h5 class="user-profile__username mast-displayname"></h5>
-                                    </a>
-                                </div>
-                                <p class="user-profile__bio mast-brief"></p>
-                                <!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
-                    
-                                {% if request.user != user %}
-                                <a href="{% url 'users:report' %}?user_id={{ user.id }}"
-                                    class="user-profile__report-link">{% trans '举报用户' %}</a>
-                                {% endif %}
-                    
-                            </div>
-                        </div>
-                    
-                        <div class="relation-dropdown">
-                            <div class="relation-dropdown__button">
-                                <span class="icon-arrow">
-                                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
-                                        <path d="M8.12,3.29,5,6.42,1.86,3.29H.45L5,7.84,9.55,3.29Z" />
-                                    </svg>
-                                </span>
-                            </div>
-                            <div class="relation-dropdown__body">
-                                <div class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
-                    
-                                    <div class="user-relation" id="followings">
-                                        <h5 class="user-relation__label">
-                                            {% trans '关注的人' %}
-                                        </h5>
-                                        <a href="{% url 'users:following' user.id %}"
-                                            class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-following">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                    <div class="user-relation" id="followers">
-                                        <h5 class="user-relation__label">
-                                            {% trans '被他们关注' %}
-                                        </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
-                                            class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
-                                        <ul class="user-relation__related-user-list mast-followers">
-                                            <li class="user-relation__related-user">
-                                                <a>
-                                                    <img src="" alt="" class="user-relation__related-user-avatar">
-                                                    <div class="user-relation__related-user-name mast-displayname">
-                                                    </div>
-                                                </a>
-                                            </li>
-                                        </ul>
-                                    </div>
-                    
-                                </div>
-                            </div>
-                        </div>
-                    
-                    </div>
-                </div>
-            </section>
-        </div>
-        {% include "partial/_footer.html" %}
-    </div>
-
-    
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    {% if user == request.user %}
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
-    {% endif %}
-    <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
-    <div id="spinner" hidden>
-        <div class="spinner">
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-            <div></div>
-        </div>
-    </div>
-    <script>
-
-    </script>
-</body>
-
-
-</html>
diff --git a/users/templates/users/preferences.html b/users/templates/users/preferences.html
new file mode 100644
index 00000000..cca66196
--- /dev/null
+++ b/users/templates/users/preferences.html
@@ -0,0 +1,93 @@
+{% load static %}
+{% load i18n %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - 设置</title>
+    {% include "partial/_common_libs.html" %}
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid grid--reverse-order">
+                    <div class="grid__main grid__main--reverse-order">
+                        <div class="main-section-wrapper">
+                            <form action="{% url 'users:preferences' %}" method="POST">
+                                <div class="tools-section-wrapper">
+                                    <div class="import-panel">
+                                        <h5 class="import-panel__label">{% trans '使用偏好设置' %}</h5>
+                                        <div class="import-panel__body">
+                                            {% csrf_token %}
+                                            <span>{% trans '新标记默认可见性:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <label for="id_visibility_0"><input type="radio" name="default_visibility" value="0" required="" id="id_visibility_0" {%if request.user.preference.default_visibility == 0 %}checked{% endif %}>
+                                                 公开</label>
+                                                <label for="id_visibility_1"><input type="radio" name="default_visibility" value="1" required="" id="id_visibility_1" {%if request.user.preference.default_visibility == 1 %}checked{% endif %}>
+                                                 仅关注者</label>
+                                                <label for="id_visibility_2"><input type="radio" name="default_visibility" value="2" required="" id="id_visibility_2" {%if request.user.preference.default_visibility == 2 %}checked{% endif %}>
+                                                 仅自己</label>
+                                            </div>
+                                            <br>
+                                            <span>{% trans '登录后显示个人主页:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="classic_homepage" id="classic_homepage" {%if request.user.preference.classic_homepage %}checked{% endif %}>
+                                                <label for="classic_homepage">{% trans '默认登录后显示好友动态,如果希望登录后显示原版风格个人主页可选中此处' %}</label>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="tools-section-wrapper" style="margin-top: 2em;">
+                                    <div class="import-panel">
+                                        <h5 class="import-panel__label">{% trans '社交网络分享相关设置' %}</h5>
+                                        <div class="import-panel__body">
+                                            {% csrf_token %}
+                                            <span>{% trans '在联邦网络上以公开方式分享的帖文是否发布到公共时间轴上:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input type="checkbox" name="mastodon_publish_public" id="visibility" {%if request.user.preference.mastodon_publish_public %}checked{% endif %}>
+                                                <label for="visibility">{% trans '选中时为public,未选中时为unlisted' %}</label>
+                                            </div>
+                                            <br><br>
+                                            <span>{% trans '在联邦网络上分享帖文时附加标签:' %}</span>
+                                            <div class="import-panel__checkbox import-panel__checkbox--last">
+                                                <input name="mastodon_append_tag" id="tag" placeholder="#我的书影音" value="{{ request.user.preference.mastodon_append_tag }}" >
+                                                <label for="tag">{% trans '输入标签文字会被添加到帖文结尾' %}</label>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div style="margin-top: 2em;">
+                                    <input type="submit" class="import-panel__button" value="{% trans '保存' %}">
+                                </div>
+                            </form>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+                </div>
+            </section>
+
+        </div>
+
+        {% include "partial/_footer.html" %}
+    </div>
+
+</body>
+
+
+</html>
\ No newline at end of file
diff --git a/users/templates/users/register.html b/users/templates/users/register.html
index 9b750a66..cb63b5c6 100644
--- a/users/templates/users/register.html
+++ b/users/templates/users/register.html
@@ -6,11 +6,10 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css">
-    <link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
+    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
-    <title>{% trans 'NiceDB - 注册' %}</title>
+    <title>{{ site_name }} - {% trans '注册' %}</title>
 </head>
 
 <body>
@@ -19,18 +18,18 @@
         <img src="{% static 'img/logo.svg' %}" class="logo" alt="boofilsic logo">
 
         <div id="loginButton">
-            <p>欢迎来到NiceDB书影音!</p>
+            <p>欢迎来到{{ site_name }}!</p>
             <p>
-                NiceDB书影音继承了长毛象的用户关系,比如您在里瓣屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
-                这里仍是一片处女地,丰富的内容需要大家共同创造。
-                请注意虽然您可以随意发表任何言论,但试图添加垃圾数据到公共数据领域(如添加不存在的乱码的书籍)将会受到制裁!
-                BTW欧盟惯例本站使用了Cookie,请理解!
+                {{ site_name }}还在不断完善中,丰富的内容需要大家共同创造。
+                试图添加垃圾数据(如添加信息混乱或缺失的书籍、以推广为主要目的的评论)将会受到严肃处理。
+                {{ site_name }}继承了联邦宇宙的用户关系,比如您在联邦宇宙屏蔽了某人,那您将不会在书影音的公共区域看到TA的痕迹。
+                本站为非盈利站点,cookie和其他数据保管使用原则请参阅<a href="/announcement/data-policy/">站内公告</a>。
             </p>
             <p>
-                此外NiceDB书影音现处于“公开阿尔法测试”阶段,您的数据存在丢失的可能。使用过程中遇到的问题或者Bug欢迎向<a href="https://donotban.com/@whitiewhite">作者</a>提出。
+                此外,{{ site_name }}现处于测试阶段,疏漏在所难免,请妥善备份您的数据。
+                使用过程中遇到的问题或者错误欢迎向<a href="{{ support_link }}">维护者</a>提出。感谢理解和支持!
             </p>
-            <form action="{% url 'users:register' %}" method="post">
-                {% csrf_token %}
+            <form action="{% url 'common:home' %}">
                 <input type="submit" class="button" value="{% trans 'Cut the sh*t and get me in!' %}">
             </form>
 
diff --git a/users/templates/users/relation_list.html b/users/templates/users/relation_list.html
index 6984bbd3..ae14e2ea 100644
--- a/users/templates/users/relation_list.html
+++ b/users/templates/users/relation_list.html
@@ -11,11 +11,13 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     {% if is_followers_page %}
-    <title>{% trans 'NiceDB - 被他们关注' %}</title>
+    <title>{{ site_name }} - {% trans '被他们关注' %}</title>
     {% else %}
-    <title>{% trans 'NiceDB - 关注的人' %}</title>
+    <title>{{ site_name }} - {% trans '关注的人' %}</title>
     {% endif %}
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+
+    {% include "partial/_common_libs.html" with jquery=1 %}
+
     <script src="{% static 'js/mastodon.js' %}"></script>
 
     {% if is_followers_page %}
@@ -23,8 +25,6 @@
     {% else %}
     <script src="{% static 'js/following_list.js' %}"></script>
     {% endif %}
-
-    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
 <body>
@@ -64,7 +64,7 @@
                                 <div class="user-profile__header">
                                     <!-- <img src="" class="user-profile__avatar mast-avatar" alt="{{ user.username }}"> -->
                                     <img src="" class="user-profile__avatar mast-avatar">
-                                    <a href="{% url 'users:home' user.id %}">
+                                    <a href="{% url 'users:home' user.mastodon_username %}">
                                         <h5 class="user-profile__username mast-displayname"></h5>
                                     </a>
                                 </div>
@@ -94,7 +94,7 @@
                                         <h5 class="user-relation__label">
                                             {% trans '关注的人' %}
                                         </h5>
-                                        <a href="{% url 'users:following' user.id %}"
+                                        <a href="{% url 'users:following' user.mastodon_username %}"
                                             class="user-relation__more-link mast-following-more">{% trans '更多' %}</a>
                                         <ul class="user-relation__related-user-list mast-following">
                                             <li class="user-relation__related-user">
@@ -110,7 +110,7 @@
                                         <h5 class="user-relation__label">
                                             {% trans '被他们关注' %}
                                         </h5>
-                                        <a href="{% url 'users:followers' user.id %}"
+                                        <a href="{% url 'users:followers' user.mastodon_username %}"
                                             class="user-relation__more-link mast-followers-more">{% trans '更多' %}</a>
                                         <ul class="user-relation__related-user-list mast-followers">
                                             <li class="user-relation__related-user">
@@ -140,7 +140,7 @@
     {% if user == request.user %}
     <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
     {% else %}
-    <div id="userMastodonID" hidden="true">{{ user.target_site_id }}</div>
+    <div id="userMastodonID" hidden="true"></div>
     {% endif %}
     <div id="userPageURL" hidden="true">{% url 'users:home' 0 %}</div>
     <div id="spinner" hidden>
diff --git a/users/templates/users/report.html b/users/templates/users/report.html
index 5f60738e..ea47b64a 100644
--- a/users/templates/users/report.html
+++ b/users/templates/users/report.html
@@ -10,8 +10,8 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{% trans 'NiceDB - 举报用户' %}</title>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <title>{{ site_name }} - {% trans '举报用户' %}</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
     <script src="{% static 'js/create_update.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
@@ -38,12 +38,6 @@
     </div>
 
     
-    {% comment %}        
-    <div id="oauth2Token" hidden="true">{% oauth_token %}</div>
-    <div id="mastodonURI" hidden="true">{% mastodon  request.user.mastodon_site %}</div>
-    <!--current user mastodon id--> 
-    <div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
-    {% endcomment %}
         
     <script>
 
diff --git a/users/templates/users/tags.html b/users/templates/users/tags.html
new file mode 100644
index 00000000..2d9c738f
--- /dev/null
+++ b/users/templates/users/tags.html
@@ -0,0 +1,110 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{{ site_name }} - 我的标签</title>
+    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="{% static 'lib/js/rating-star.js' %}"></script>
+    <script src="{% static 'js/rating-star-readonly.js' %}"></script>
+    <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
+    <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
+    <script src="{% static 'js/mastodon.js' %}"></script>
+    <script src="{% static 'js/home.js' %}"></script>
+</head>
+
+<body>
+    <div id="page-wrapper">
+        <div id="content-wrapper">
+            {% include "partial/_navbar.html" %}
+
+            <section id="content">
+                <div class="grid">
+                    <div class="grid__main" id="main">
+                        <div class="main-section-wrapper">
+                            <div class="entity-reviews">                                
+                                <div class="tag-collection entity-reviews__review-list">
+                                    <h5>{% trans '书籍' %}</h5>
+                                    {% for v in book_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:book_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+				    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+
+                                    <h5>{% trans '电影和剧集' %}</h5>
+                                    {% for v in movie_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:movie_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+                                    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+
+                                    <h5>{% trans '音乐' %}</h5>
+                                    {% for v in music_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:music_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+                                    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+
+                                    <h5>{% trans '游戏' %}</h5>
+                                    {% for v in game_tags %}
+				    <span style="display: inline-block;margin: 4px;">
+				    <span class="tag-collection__tag" style="display:inline;float:none;">
+                                        <a href="{% url 'users:game_list' user.mastodon_username 'tagged' %}?t={{ v.content }}">{{ v.content }}</a>
+                                    </span>
+                                    <span class="entity-reviews__review-time">({{ v.total }})</span>
+				    </span>
+                                    {% empty %}
+                                        {% trans '暂无标签' %}
+                                    {% endfor %}
+                                    <div class="clearfix" style="margin-bottom: 16px;"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    {% include "partial/_sidebar.html" %}
+
+                </div>
+            </section>
+        </div>
+        {% include "partial/_footer.html" %}
+    </div>
+
+    
+
+    <script>
+
+    </script>
+</body>
+
+
+</html>
diff --git a/users/urls.py b/users/urls.py
index 121c1afe..3b0d2b34 100644
--- a/users/urls.py
+++ b/users/urls.py
@@ -5,22 +5,38 @@ app_name = 'users'
 urlpatterns = [
     path('login/', login, name='login'),
     path('register/', register, name='register'),
+    path('connect/', connect, name='connect'),
+    path('reconnect/', reconnect, name='reconnect'),
+    path('data/', data, name='data'),
+    path('data/import_goodreads', import_goodreads, name='import_goodreads'),
+    path('data/import_douban', import_douban, name='import_douban'),
+    path('data/export_reviews', export_reviews, name='export_reviews'),
+    path('data/export_marks', export_marks, name='export_marks'),
+    path('data/sync_mastodon', sync_mastodon, name='sync_mastodon'),
+    path('data/reset_visibility', reset_visibility, name='reset_visibility'),
+    path('data/clear_data', clear_data, name='clear_data'),
+    path('preferences/', preferences, name='preferences'),
     path('logout/', logout, name='logout'),
-    path('delete/', delete, name='delete'),
     path('layout/', set_layout, name='set_layout'),
     path('OAuth2_login/', OAuth2_login, name='OAuth2_login'),
-    path('<int:id>/', home, name='home'),
-    path('<int:id>/followers/', followers, name='followers'),
-    path('<int:id>/following/', following, name='following'),
-    path('<int:id>/book/<str:status>/', book_list, name='book_list'),
-    path('<int:id>/movie/<str:status>/', movie_list, name='movie_list'),
-    path('<int:id>/music/<str:status>/', music_list, name='music_list'),
-    path('<int:id>/game/<str:status>/', game_list, name='game_list'),
+    path('<int:id>/', home_redirect, name='home_redirect'),
+    # path('<int:id>/followers/', followers, name='followers'),
+    # path('<int:id>/following/', following, name='following'),
+    # path('<int:id>/collections/', collection_list, name='collection_list'),
+    # path('<int:id>/book/<str:status>/', book_list, name='book_list'),
+    # path('<int:id>/movie/<str:status>/', movie_list, name='movie_list'),
+    # path('<int:id>/music/<str:status>/', music_list, name='music_list'),
+    # path('<int:id>/game/<str:status>/', game_list, name='game_list'),
     path('<str:id>/', home, name='home'),
     path('<str:id>/followers/', followers, name='followers'),
     path('<str:id>/following/', following, name='following'),
+    path('<str:id>/tags/', tag_list, name='tag_list'),
+    path('<str:id>/collections/', collection_list, name='collection_list'),
+    path('<str:id>/collections/marked/', marked_collection_list, name='marked_collection_list'),
     path('<str:id>/book/<str:status>/', book_list, name='book_list'),
     path('<str:id>/movie/<str:status>/', movie_list, name='movie_list'),
+    path('<str:id>/music/<str:status>/', music_list, name='music_list'),
+    path('<str:id>/game/<str:status>/', game_list, name='game_list'),
     path('report/', report, name='report'),
     path('manage_report/', manage_report, name='manage_report'),
 ]
diff --git a/users/views.py b/users/views.py
index ff77d066..e216f806 100644
--- a/users/views.py
+++ b/users/views.py
@@ -5,11 +5,10 @@ from django.contrib import auth
 from django.contrib.auth import authenticate
 from django.core.paginator import Paginator
 from django.utils.translation import gettext_lazy as _
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.db.models import Count
 from .models import User, Report, Preference
 from .forms import ReportForm
-from mastodon.auth import *
 from mastodon.api import *
 from mastodon import mastodon_request_included
 from common.config import *
@@ -25,156 +24,69 @@ from movies.forms import MovieMarkStatusTranslator
 from music.forms import MusicMarkStatusTranslator
 from games.forms import GameMarkStatusTranslator
 from mastodon.models import MastodonApplication
+from mastodon.api import verify_account
+from django.conf import settings
+from urllib.parse import quote
+import django_rq
+from .account import *
+from .data import *
+from datetime import timedelta
+from django.utils import timezone
+import json
+from django.contrib import messages
+from books.models import BookMark, BookReview
+from movies.models import MovieMark, MovieReview
+from games.models import GameMark, GameReview
+from music.models import AlbumMark, SongMark, AlbumReview, SongReview
+from collection.models import Collection
+from common.importers.goodreads import GoodreadsImporter
+from common.importers.douban import DoubanImporter
 
 
-# Views
-########################################
-
-# no page rendered
-@mastodon_request_included
-def OAuth2_login(request):
-    """ oauth authentication and logging user into django system """
-    if request.method == 'GET':
-        code = request.GET.get('code')
-        site = request.COOKIES.get('mastodon_domain')
-
-        # Network IO
-        try:
-            token = obtain_token(site, request, code)
-        except ObjectDoesNotExist:
-            return HttpResponseBadRequest("Mastodon site not registered")
-        if token:
-            # oauth is completed when token aquired
-            user = authenticate(request, token=token, site=site)
-            if user:
-                auth_login(request, user, token)
-                if request.session.get('next_url') is not None:
-                    response = redirect(request.session.get('next_url'))
-                    del request.session['next_url']
-                else:
-                    response = redirect(reverse('common:home'))
-                    
-                response.delete_cookie('mastodon_domain')
-                return response
-            else:
-                # will be passed to register page
-                request.session['new_user_token'] = token
-                return redirect(reverse('users:register'))
-        else:
-            return render(
-                request,
-                'common/error.html',
-                {
-                    'msg': _("认证失败😫")
-                }
-            )
-    else:
-        return HttpResponseBadRequest()
+def render_user_not_found(request):
+    msg = _("😖哎呀,这位用户还没有加入本站,快去联邦宇宙呼唤TA来注册吧!")
+    sec_msg = _("")
+    return render(
+        request,
+        'common/error.html',
+        {
+            'msg': msg,
+            'secondary_msg': sec_msg,
+        }
+    )
 
 
-# the 'login' page that user can see
-def login(request):
-    if request.method == 'GET':
-        selected_site = request.GET.get('site', default='')
+def home_redirect(request, id):
+    try:
+        query_kwargs = {'pk': id}
+        user = User.objects.get(**query_kwargs)
+        return redirect(reverse("users:home", args=[user.mastodon_username]))
+    except Exception:
+        return redirect(settings.LOGIN_URL)
 
-        sites = MastodonApplication.objects.all().order_by("domain_name")
 
-        # store redirect url in the cookie
-        if request.GET.get('next'):
-            request.session['next_url'] = request.GET.get('next')
-
-        return render(
-            request,
-            'users/login.html',
-            {
-                'sites': sites,
-                'selected_site': selected_site,
-            }
-        )
-    else:
-        return HttpResponseBadRequest()
+def home_anonymous(request, id):
+    login_url = settings.LOGIN_URL + "?next=" + request.get_full_path()
+    try:
+        username = id.split('@')[0]
+        site = id.split('@')[1]
+        return render(request, 'users/home_anonymous.html', {
+                      'login_url': login_url,
+                      'username': username,
+                      'site': site,
+                      })
+    except Exception:
+        return redirect(login_url)
 
 
 @mastodon_request_included
-@login_required
-def logout(request):
-    if request.method == 'GET':
-        revoke_token(request.user.mastodon_site, request.session['oauth_token'])
-        auth_logout(request)
-        return redirect(reverse("users:login"))
-    else:
-        return HttpResponseBadRequest()
-
-
-@mastodon_request_included
-def register(request):
-    """ register confirm page """
-    if request.method == 'GET':
-        if request.session.get('oauth_token'):
-            return redirect(reverse('common:home'))
-        elif request.session.get('new_user_token'):
-            return render(
-                request,
-                'users/register.html'
-            )
-        else:
-            return HttpResponseBadRequest()
-    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'],
-            mastodon_site=request.COOKIES['mastodon_domain'],
-        )
-        new_user.save()
-        del request.session['new_user_token']
-        auth_login(request, new_user, token)
-        response = redirect(reverse('common:home'))
-        response.delete_cookie('mastodon_domain')
-        return response
-    else:
-        return HttpResponseBadRequest()
-
-
-def delete(request):
-    raise NotImplementedError
-
-
-@mastodon_request_included
-@login_required
 def home(request, id):
+    if not request.user.is_authenticated:
+        return home_anonymous(request, id)
     if request.method == 'GET':
-        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,
-                }
-            )
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
 
         # access one's own home page
         if user == request.user:
@@ -194,23 +106,19 @@ def home(request, id):
             album_marks = request.user.user_albummarks.all()
             song_marks = request.user.user_songmarks.all()
             game_marks = request.user.user_gamemarks.all()
-
-            latest_task = user.user_synctasks.order_by("-id").first()
+            book_reviews = request.user.user_bookreviews.all()
+            movie_reviews = request.user.user_moviereviews.all()
+            album_reviews = request.user.user_albumreviews.all()
+            song_reviews = request.user.user_songreviews.all()
+            game_reviews = request.user.user_gamereviews.all()
 
         # visit other's home page
         else:
-            latest_task = None
             # no these value on other's home page
             reports = None
             unread_announcements = None
 
-            # cross site info for visiting other's home page
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
-            
-            # make queries
-            relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
-            if relation['blocked_by']:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -219,20 +127,28 @@ def home(request, id):
                         'msg': msg,
                     }
                 )
-            book_marks = BookMark.get_available_by_user(user, relation['following'])
-            movie_marks = MovieMark.get_available_by_user(user, relation['following'])
-            song_marks = SongMark.get_available_by_user(user, relation['following'])
-            album_marks = AlbumMark.get_available_by_user(user, relation['following'])
-            game_marks = GameMark.get_available_by_user(user, relation['following'])
+            is_following = request.user.is_following(user)
+            book_marks = BookMark.get_available_by_user(user, is_following)
+            movie_marks = MovieMark.get_available_by_user(user, is_following)
+            song_marks = SongMark.get_available_by_user(user, is_following)
+            album_marks = AlbumMark.get_available_by_user(user, is_following)
+            game_marks = GameMark.get_available_by_user(user, is_following)
+            book_reviews = BookReview.get_available_by_user(user, is_following)
+            movie_reviews = MovieReview.get_available_by_user(user, is_following)
+            song_reviews = SongReview.get_available_by_user(user, is_following)
+            album_reviews = AlbumReview.get_available_by_user(user, is_following)
+            game_reviews = GameReview.get_available_by_user(user, is_following)
 
+        collections = Collection.objects.filter(owner=user)
+        marked_collections = Collection.objects.filter(pk__in=CollectionMark.objects.filter(owner=user).values_list('collection', flat=True))
 
         # book marks
-        filtered_book_marks = filter_marks(book_marks, BOOKS_PER_SET, 'book')          
+        filtered_book_marks = filter_marks(book_marks, BOOKS_PER_SET, 'book')
         book_marks_count = count_marks(book_marks, "book")
 
         # movie marks
         filtered_movie_marks = filter_marks(movie_marks, MOVIES_PER_SET, 'movie')
-        movie_marks_count= count_marks(movie_marks, "movie")
+        movie_marks_count = count_marks(movie_marks, "movie")
 
         # game marks
         filtered_game_marks = filter_marks(game_marks, GAMES_PER_SET, 'game')
@@ -241,7 +157,6 @@ def home(request, id):
         # music marks
         filtered_music_marks = filter_marks([song_marks, album_marks], MUSIC_PER_SET, 'music')
         music_marks_count = count_marks([song_marks, album_marks], "music")
-     
 
         for mark in filtered_music_marks["do_music_marks"] +\
             filtered_music_marks["wish_music_marks"] +\
@@ -251,12 +166,12 @@ def home(request, id):
                 mark.type = "album"
             else:
                 mark.type = "song"
-        
-        try:
-            layout = user.preference.get_serialized_home_layout()
-        except ObjectDoesNotExist:
-            Preference.objects.create(user=user)
-            layout = user.preference.get_serialized_home_layout()
+
+        music_reviews = list(album_reviews.order_by("-edited_time")) + list(song_reviews.order_by("-edited_time"))
+        for review in music_reviews:
+            review.type = 'album' if review.__class__ == AlbumReview else 'song'
+
+        layout = user.get_preference().get_serialized_home_layout()
 
         return render(
             request,
@@ -271,10 +186,36 @@ def home(request, id):
                 **movie_marks_count,
                 **music_marks_count,
                 **game_marks_count,
+
+                'book_tags': BookTag.all_by_user(user)[:10] if user == request.user else [],
+                'movie_tags': MovieTag.all_by_user(user)[:10] if user == request.user else [],
+                'music_tags': AlbumTag.all_by_user(user)[:10] if user == request.user else [],
+                'game_tags': GameTag.all_by_user(user)[:10] if user == request.user else [],
+
+                'book_reviews': book_reviews.order_by("-edited_time")[:BOOKS_PER_SET],
+                'movie_reviews': movie_reviews.order_by("-edited_time")[:MOVIES_PER_SET],
+                'music_reviews': music_reviews[:MUSIC_PER_SET],
+                'game_reviews': game_reviews[:GAMES_PER_SET],
+                'book_reviews_more': book_reviews.count() > BOOKS_PER_SET,
+                'movie_reviews_more': movie_reviews.count() > MOVIES_PER_SET,
+                'music_reviews_more': len(music_reviews) > MUSIC_PER_SET,
+                'game_reviews_more': game_reviews.count() > GAMES_PER_SET,
+                'book_reviews_count': book_reviews.count(),
+                'movie_reviews_count': movie_reviews.count(),
+                'music_reviews_count': len(music_reviews),
+                'game_reviews_count': game_reviews.count(),
+
+                'collections': collections.order_by("-edited_time")[:BOOKS_PER_SET],
+                'collections_count': collections.count(),
+                'collections_more': collections.count() > BOOKS_PER_SET,
+
+                'marked_collections': marked_collections.order_by("-edited_time")[:BOOKS_PER_SET],
+                'marked_collections_count': marked_collections.count(),
+                'marked_collections_more': marked_collections.count() > BOOKS_PER_SET,
+
                 'layout': layout,
                 'reports': reports,
                 'unread_announcements': unread_announcements,
-                'latest_task': latest_task,
             }
         )
     else:
@@ -283,7 +224,7 @@ def home(request, id):
 
 def filter_marks(querysets, maximum, type_name):
     """
-    Filter marks by amount limits and order them edited time, store results in a dict, 
+    Filter marks by amount limits and order them edited time, store results in a dict,
     which could be directly used in template.
     @param querysets: one queryset or multiple querysets as a list
     """
@@ -295,9 +236,9 @@ def filter_marks(querysets, maximum, type_name):
         marks = []
         count = 0
         for queryset in querysets:
-            marks += list(queryset.filter(status=MarkStatusEnum[status.upper()]).order_by("-edited_time")[:maximum])
+            marks += list(queryset.filter(status=MarkStatusEnum[status.upper()]).order_by("-created_time")[:maximum])
             count += queryset.filter(status=MarkStatusEnum[status.upper()]).count()
-            
+
         # marks
         marks = sorted(marks, key=lambda e: e.edited_time, reverse=True)[:maximum]
         result[f"{status}_{type_name}_marks"] = marks
@@ -309,6 +250,7 @@ def filter_marks(querysets, maximum, type_name):
 
     return result
 
+
 def count_marks(querysets, type_name):
     """
     Count all available marks, then assembly a dict to be used in template
@@ -327,44 +269,11 @@ def count_marks(querysets, type_name):
 
 @mastodon_request_included
 @login_required
-def followers(request, id):  
+def followers(request, id):
     if request.method == 'GET':
-        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,
-                }
-            )        
-        # mastodon request
-        if not user == request.user:
-            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,
-                    }
-                )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
+        user = User.get(id)
+        if user is None or user != request.user:
+            return render_user_not_found(request)
         return render(
             request,
             'users/relation_list.html',
@@ -381,42 +290,9 @@ def followers(request, id):
 @login_required
 def following(request, id):
     if request.method == 'GET':
-        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,
-                }
-            )        
-        # mastodon request
-        if not user == request.user:
-            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,
-                    }
-                )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
+        user = User.get(id)
+        if user is None or user != request.user:
+            return render_user_not_found(request)
         return render(
             request,
             'users/relation_list.html',
@@ -433,35 +309,15 @@ def following(request, id):
 @login_required
 def book_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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']:
+
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
+        if user != request.user:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -470,13 +326,22 @@ def book_list(request, id, status):
                         'msg': msg,
                     }
                 )
-            queryset = BookMark.get_available_by_user(user, relation['following']).filter(
-                status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = BookReview.get_available_by_user(user, is_following).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = BookTag.find_by_user(tag, user, request.user).order_by("-mark__created_time")
+            else:
+                queryset = BookMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         else:
-            queryset = BookMark.objects.filter(
-                owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            if status == 'reviewed':
+                queryset = BookReview.objects.filter(owner=user).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = BookTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time")
+            else:
+                queryset = BookMark.objects.filter(
+                    owner=user, status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         paginator = Paginator(queryset, ITEMS_PER_PAGE)
         page_number = request.GET.get('page', default=1)
         marks = paginator.get_page(page_number)
@@ -484,14 +349,20 @@ 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(_("的书"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的书"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的书"))
+        else:
+            list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书"))
         return render(
             request,
-            'users/book_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
@@ -502,35 +373,15 @@ def book_list(request, id, status):
 @login_required
 def movie_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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']:
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
+        if user != request.user:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -539,14 +390,21 @@ def movie_list(request, id, status):
                         'msg': msg,
                     }
                 )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
-        
-            queryset = MovieMark.get_available_by_user(user, relation['following']).filter(
-                status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = MovieReview.get_available_by_user(user, is_following).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = MovieTag.find_by_user(tag, user, request.user).order_by("-mark__created_time")
+            else:
+                queryset = MovieMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         else:
-            queryset = MovieMark.objects.filter(
-                owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            if status == 'reviewed':
+                queryset = MovieReview.objects.filter(owner=user).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = MovieTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time")
+            else:
+                queryset = MovieMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         paginator = Paginator(queryset, ITEMS_PER_PAGE)
         page_number = request.GET.get('page', default=1)
         marks = paginator.get_page(page_number)
@@ -554,53 +412,40 @@ 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(_("的电影和剧集"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的电影和剧集"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的电影和剧集"))
+        else:
+            list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集"))
+
         return render(
             request,
-            'users/movie_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
         return HttpResponseBadRequest()
-            
+
 
 @mastodon_request_included
 @login_required
 def game_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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']:
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
+        if user != request.user:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -609,14 +454,22 @@ def game_list(request, id, status):
                         'msg': msg,
                     }
                 )
-            user.target_site_id = get_cross_site_id(
-                user, request.user.mastodon_site, request.session['oauth_token'])
-        
-            queryset = GameMark.get_available_by_user(user, relation['following']).filter(
-                status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = GameReview.get_available_by_user(user, is_following).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = GameTag.find_by_user(tag, user, request.user).order_by("-mark__created_time")
+            else:
+                queryset = GameMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         else:
-            queryset = GameMark.objects.filter(
-                owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time")
+            if status == 'reviewed':
+                queryset = GameReview.objects.filter(owner=user).order_by("-edited_time")
+            elif status == 'tagged':
+                queryset = GameTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time")
+            else:
+                queryset = GameMark.objects.filter(
+                    owner=user, status=MarkStatusEnum[status.upper()]).order_by("-created_time")
         paginator = Paginator(queryset, ITEMS_PER_PAGE)
         page_number = request.GET.get('page', default=1)
         marks = paginator.get_page(page_number)
@@ -624,53 +477,39 @@ def game_list(request, id, status):
             mark.game.tag_list = mark.game.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(GameMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的游戏"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的游戏"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的游戏"))
+        else:
+            list_title = str(GameMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的游戏"))
         return render(
             request,
-            'users/game_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
         return HttpResponseBadRequest()
-            
+
 
 @mastodon_request_included
 @login_required
 def music_list(request, id, status):
     if request.method == 'GET':
-        if not status.upper() in MarkStatusEnum.names:
+        if status.upper() not in MarkStatusEnum.names and status not in ['reviewed', 'tagged']:
             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,
-                }
-            )        
+        user = User.get(id)
+        if user is None:
+            return render_user_not_found(request)
+        tag = request.GET.get('t', default='')
         if not user == request.user:
-            # mastodon request
-            relation = get_relationship(request.user, user, request.session['oauth_token'])[0]
-            if relation['blocked_by']:
+            if request.user.is_blocked_by(user) or request.user.is_blocking(user):
                 msg = _("你没有访问TA主页的权限😥")
                 return render(
                     request,
@@ -679,39 +518,55 @@ def music_list(request, id, status):
                         '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'])
+            is_following = request.user.is_following(user)
+            if status == 'reviewed':
+                queryset = list(AlbumReview.get_available_by_user(user, is_following).order_by("-edited_time")) + \
+                    list(SongReview.get_available_by_user(user, is_following).order_by("-edited_time"))
+            elif status == 'tagged':
+                queryset = list(AlbumTag.find_by_user(tag, user, request.user).order_by("-mark__created_time"))
+            else:
+                queryset = list(AlbumMark.get_available_by_user(user, is_following).filter(
+                    status=MarkStatusEnum[status.upper()])) \
+                        + list(SongMark.get_available_by_user(user, is_following).filter(
+                        status=MarkStatusEnum[status.upper()]))
         else:
-            queryset = list(AlbumMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()])) \
-                + list(SongMark.objects.filter(owner=user, status=MarkStatusEnum[status.upper()]))
+            if status == 'reviewed':
+                queryset = list(AlbumReview.objects.filter(owner=user).order_by("-edited_time")) + \
+                    list(SongReview.objects.filter(owner=user).order_by("-edited_time"))
+            elif status == 'tagged':
+                queryset = list(AlbumTag.objects.filter(content=tag, mark__owner=user).order_by("-mark__created_time"))
+            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:
+            if mark.__class__ in [AlbumMark, AlbumReview, AlbumTag]:
                 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:
+            elif mark.__class__ == SongMark or mark.__class__ == SongReview:
                 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(MusicMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的音乐"))
+        if status == 'reviewed':
+            list_title = str(_("评论过的音乐"))
+        elif status == 'tagged':
+            list_title = str(_(f"标记为「{tag}」的音乐"))
+        else:
+            list_title = str(MusicMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的音乐"))
         return render(
             request,
-            'users/music_list.html',
+            'users/item_list.html',
             {
                 'marks': marks,
                 'user': user,
-                'list_title' : list_title,
+                'status': status,
+                'list_title': list_title,
             }
         )
     else:
@@ -724,7 +579,7 @@ def set_layout(request):
         layout = json.loads(request.POST.get('layout'))
         request.user.preference.home_layout = layout
         request.user.preference.save()
-        return redirect(reverse("common:home"))
+        return redirect(reverse("users:home", args=[request.user.mastodon_username]))
     else:
         return HttpResponseBadRequest()
 
@@ -751,14 +606,14 @@ def report(request):
             form.instance.is_read = False
             form.instance.submit_user = request.user
             form.save()
-            return redirect(reverse("users:home", args=[form.instance.reported_user.id]))
+            return redirect(reverse("users:home", args=[form.instance.reported_user.mastodon_username]))
         else:
             return render(
                 request,
                 'users/report.html',
                 {
                     'form': form,
-                }                
+                }
             )
     else:
         return HttpResponseBadRequest()
@@ -782,15 +637,37 @@ def manage_report(request):
         return HttpResponseBadRequest()
 
 
-# Utils
-########################################
-def auth_login(request, user, token):
-    """ Decorates django ``login()``. Attach token to session."""
-    request.session['oauth_token'] = token
-    auth.login(request, user)
+@login_required
+def collection_list(request, id):
+    from collection.views import list
+    user = User.get(id)
+    if user is None:
+        return render_user_not_found(request)
+    return list(request, user.id)
 
 
-def auth_logout(request):
-    """ Decorates django ``logout()``. Release token in session."""
-    del request.session['oauth_token']
-    auth.logout(request)    
+@login_required
+def marked_collection_list(request, id):
+    from collection.views import list
+    user = User.get(id)
+    if user is None:
+        return render_user_not_found(request)
+    return list(request, user.id, True)
+
+
+@login_required
+def tag_list(request, id):
+    user = User.get(id)
+    if user is None:
+        return render_user_not_found(request)
+    if user != request.user:
+        raise PermissionDenied()  # tag list is for user's own view only, for now
+    return render(
+        request,
+        'users/tags.html', {
+            'book_tags': BookTag.all_by_user(user),
+            'movie_tags': MovieTag.all_by_user(user),
+            'music_tags': AlbumTag.all_by_user(user),
+            'game_tags': GameTag.all_by_user(user),
+        }
+    )

From ef70e5f9c6174ecdddeec574e118dda5a23391f3 Mon Sep 17 00:00:00 2001
From: doubaniux <goodsir@vivaldi.net>
Date: Fri, 11 Nov 2022 18:40:02 +0100
Subject: [PATCH 4/6] generalize instance related code

---
 .github/workflows/codeql-analysis.yml         |  4 +-
 .github/workflows/pysa.yml                    |  4 +-
 .gitignore                                    |  3 ++
 Dockerfile                                    |  2 +-
 boofilsic/settings.py                         | 46 +++++++++---------
 books/models.py                               |  2 +-
 books/templates/books/create_update.html      |  2 +-
 .../templates/books/create_update_review.html |  2 +-
 books/templates/books/delete.html             |  2 +-
 books/templates/books/delete_review.html      |  2 +-
 books/templates/books/mark_list.html          |  2 +-
 books/templates/books/review_detail.html      |  4 +-
 books/templates/books/review_list.html        |  2 +-
 books/templates/books/scrape.html             |  2 +-
 collection/models.py                          |  2 +-
 collection/templates/create_update.html       |  2 +-
 collection/templates/delete.html              |  4 +-
 collection/templates/entity_list.html         |  2 +-
 collection/templates/list.html                |  2 +-
 common/models.py                              |  4 +-
 common/static/css/boofilsic.css               | 13 +++++
 common/static/css/boofilsic.min.css           |  2 +-
 common/static/js/home.js                      | 18 +++++++
 common/static/js/mastodon.js                  |  1 +
 .../lib/css/{neo.css => collection.css}       | 17 -------
 common/static/lib/js/hyperscript-0.9.5.min.js |  2 -
 common/static/lib/js/hyperscript-0.9.7.min.js |  1 +
 common/static/sass/_Vendor.sass               | 15 ++++++
 common/templates/common/error.html            |  2 +-
 common/templates/common/search_result.html    |  6 +--
 common/templates/partial/_common_libs.html    | 14 +++---
 common/templates/partial/_sidebar.html        |  2 +-
 common/templates/partial/list_item_book.html  |  2 +-
 common/templates/partial/list_item_game.html  |  2 +-
 common/templates/partial/list_item_movie.html |  2 +-
 common/templates/partial/list_item_music.html |  2 +-
 common/templatetags/mastodon.py               | 14 ++++++
 common/templatetags/neo.py                    | 48 -------------------
 common/templatetags/prettydate.py             | 22 +++++++++
 common/templatetags/user_item.py              | 19 ++++++++
 docker-compose.yml                            |  2 +-
 games/models.py                               |  2 +-
 games/templates/games/create_update.html      |  2 +-
 .../templates/games/create_update_review.html |  2 +-
 games/templates/games/delete.html             |  2 +-
 games/templates/games/delete_review.html      |  2 +-
 games/templates/games/mark_list.html          |  2 +-
 games/templates/games/review_detail.html      |  4 +-
 games/templates/games/review_list.html        |  2 +-
 games/templates/games/scrape.html             |  2 +-
 management/templates/management/detail.html   |  2 +-
 management/templates/management/list.html     |  2 +-
 movies/models.py                              |  2 +-
 movies/templates/movies/create_update.html    |  2 +-
 .../movies/create_update_review.html          |  2 +-
 movies/templates/movies/delete.html           |  2 +-
 movies/templates/movies/delete_review.html    |  2 +-
 movies/templates/movies/mark_list.html        |  2 +-
 movies/templates/movies/review_detail.html    |  4 +-
 movies/templates/movies/review_list.html      |  2 +-
 movies/templates/movies/scrape.html           |  2 +-
 music/models.py                               |  4 +-
 music/templates/music/album_mark_list.html    |  2 +-
 .../templates/music/album_review_detail.html  |  4 +-
 music/templates/music/album_review_list.html  |  2 +-
 .../templates/music/create_update_album.html  |  2 +-
 .../music/create_update_album_review.html     |  2 +-
 music/templates/music/create_update_song.html |  2 +-
 .../music/create_update_song_review.html      |  2 +-
 music/templates/music/delete_album.html       |  2 +-
 .../templates/music/delete_album_review.html  |  2 +-
 music/templates/music/delete_song.html        |  2 +-
 music/templates/music/delete_song_review.html |  2 +-
 music/templates/music/scrape_album.html       |  2 +-
 music/templates/music/scrape_song.html        |  2 +-
 music/templates/music/song_mark_list.html     |  2 +-
 music/templates/music/song_review_detail.html |  4 +-
 music/templates/music/song_review_list.html   |  2 +-
 requirements.txt                              |  5 --
 timeline/templates/timeline_data.html         |  2 +-
 users/templates/users/home.html               |  6 +--
 users/templates/users/item_list.html          |  6 +--
 users/templates/users/login.html              |  5 +-
 users/templates/users/manage_report.html      |  2 +-
 users/templates/users/register.html           |  2 +-
 users/templates/users/report.html             |  2 +-
 users/templates/users/tags.html               |  2 +-
 87 files changed, 224 insertions(+), 193 deletions(-)
 rename common/static/lib/css/{neo.css => collection.css} (91%)
 delete mode 100644 common/static/lib/js/hyperscript-0.9.5.min.js
 create mode 100644 common/static/lib/js/hyperscript-0.9.7.min.js
 delete mode 100644 common/templatetags/neo.py
 create mode 100644 common/templatetags/prettydate.py
 create mode 100644 common/templatetags/user_item.py

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 514df728..429d462e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,10 @@ name: "CodeQL"
 
 on:
   push:
-    branches: [ "neo" ]
+    branches: [ "master" ]
   pull_request:
     # The branches below must be a subset of the branches above
-    branches: [ "neo" ]
+    branches: [ "master" ]
   schedule:
     - cron: '35 0 * * 0'
 
diff --git a/.github/workflows/pysa.yml b/.github/workflows/pysa.yml
index e4e20af3..50e8865b 100644
--- a/.github/workflows/pysa.yml
+++ b/.github/workflows/pysa.yml
@@ -17,9 +17,9 @@ name: Pysa
 on:
   workflow_dispatch:
   push:
-    branches: [ "neo" ]
+    branches: [ "master" ]
   pull_request:
-    branches: [ "neo" ]
+    branches: [ "master" ]
   schedule:
     - cron: '45 12 * * 4'
 
diff --git a/.gitignore b/.gitignore
index d1edae82..4d9446ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ log
 
 # conf folder for neodb
 /neodb
+
+# typesense folder
+/typesense-data
diff --git a/Dockerfile b/Dockerfile
index cc9bf6e8..5115d87f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ RUN pip install --no-cache-dir -r /tmp/requirements.txt \
     && useradd -U app_user \  
     && install -d -m 0755 -o app_user -g app_user /app/static
 
-ENV DJANGO_SETTINGS_MODULE=neodb.dev
+ENV DJANGO_SETTINGS_MODULE=yoursettings.dev
 WORKDIR /app
 USER app_user:app_user
 COPY --chown=app_user:app_user . .
diff --git a/boofilsic/settings.py b/boofilsic/settings.py
index 6393f4a0..bab03dbf 100644
--- a/boofilsic/settings.py
+++ b/boofilsic/settings.py
@@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
 
 import os
 import psycopg2.extensions
-from git import Repo
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -67,7 +66,6 @@ INSTALLED_APPS = [
     'timeline.apps.TimelineConfig',
     'easy_thumbnails',
     'user_messages',
-    'django_slack',
 ]
 
 MIDDLEWARE = [
@@ -113,9 +111,9 @@ if DEBUG:
         'default': {
             'ENGINE': 'django.db.backends.postgresql',
             'NAME': os.environ.get('DB_NAME', 'test'),
-            'USER': os.environ.get('DB_USER', 'donotban'),
-            'PASSWORD': os.environ.get('DB_PASSWORD', 'donotbansilvousplait'),
-            'HOST': os.environ.get('DB_HOST', '172.18.116.29'),
+            'USER': os.environ.get('DB_USER', 'postgres'),
+            'PASSWORD': os.environ.get('DB_PASSWORD', 'admin123'),
+            'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
             'OPTIONS': {
                 'client_encoding': 'UTF8',
                 # 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
@@ -280,26 +278,27 @@ LUMINATI_PASSWORD = 'nsb7te9bw0ney'
 SCRAPING_TIMEOUT = 90
 
 # ScraperAPI api key
-SCRAPERAPI_KEY = 'wnb3794v675b8w475h0e8hr7tyge'
+SCRAPERAPI_KEY = '***REMOVED***'
 PROXYCRAWL_KEY = None
 SCRAPESTACK_KEY = None
 
 # Spotify credentials
-SPOTIFY_CREDENTIAL = "NzYzNkYTE6MGQ0ODY0NTY2Y2b3n645sdfgAyY2I1ljYjg3Nzc0MjIwODQ0ZWE="
+SPOTIFY_CREDENTIAL = "***REMOVED***"
 
 # IMDb API service https://imdb-api.com/
-IMDB_API_KEY = "k23fwewff23"
+IMDB_API_KEY = "***REMOVED***"
 
 # The Movie Database (TMDB) API Keys
-TMDB_API3_KEY = "deadbeef"
-TMDB_API4_KEY = "deadbeef.deadbeef.deadbeef"
+TMDB_API3_KEY = "***REMOVED***"
+# TMDB_API4_KEY = "deadbeef.deadbeef.deadbeef"
 
 # Google Books API Key
-GOOGLE_API_KEY = 'deadbeef-deadbeef-deadbeef'
+GOOGLE_API_KEY = '***REMOVED***'
 
 # IGDB
-IGDB_CLIENT_ID = 'deadbeef'
-IGDB_ACCESS_TOKEN = 'deadbeef'
+IGDB_CLIENT_ID = '***REMOVED***'
+IGDB_SECRET = "***REMOVED***"
+IGDB_ACCESS_TOKEN = '***REMOVED***'
 
 # Thumbnail setting
 # It is possible to optimize the image size even more: https://easy-thumbnails.readthedocs.io/en/latest/ref/optimize/
@@ -346,19 +345,18 @@ RQ_SHOW_ADMIN_LINK = True
 
 SEARCH_INDEX_NEW_ONLY = False
 
-SEARCH_BACKEND = None
 
 # SEARCH_BACKEND = 'MEILISEARCH'
 # MEILISEARCH_SERVER = 'http://127.0.0.1:7700'
 # MEILISEARCH_KEY = 'deadbeef'
 
-# SEARCH_BACKEND = 'TYPESENSE'
-# TYPESENSE_CONNECTION = {
-#     'api_key': 'deadbeef',
-#     'nodes': [{
-#         'host': 'localhost',
-#         'port': '8108',
-#         'protocol': 'http'
-#     }],
-#     'connection_timeout_seconds': 2
-# }
+SEARCH_BACKEND = 'TYPESENSE'
+TYPESENSE_CONNECTION = {
+    'api_key': 'xyz',
+    'nodes': [{
+        'host': 'localhost',
+        'port': '8108',
+        'protocol': 'http'
+    }],
+    'connection_timeout_seconds': 2
+}
diff --git a/books/models.py b/books/models.py
index 8b23e9a6..2b1caeaf 100644
--- a/books/models.py
+++ b/books/models.py
@@ -160,7 +160,7 @@ class BookReview(Review):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + reverse("books:retrieve_review", args=[self.id])
+        return reverse("books:retrieve_review", args=[self.id])
 
     @property
     def item(self):
diff --git a/books/templates/books/create_update.html b/books/templates/books/create_update.html
index de4b8ca7..f48cd4b9 100644
--- a/books/templates/books/create_update.html
+++ b/books/templates/books/create_update.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
diff --git a/books/templates/books/create_update_review.html b/books/templates/books/create_update_review.html
index 04cc0c3b..d440b96f 100644
--- a/books/templates/books/create_update_review.html
+++ b/books/templates/books/create_update_review.html
@@ -13,7 +13,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/books/templates/books/delete.html b/books/templates/books/delete.html
index 82d2fd7a..c1792a20 100644
--- a/books/templates/books/delete.html
+++ b/books/templates/books/delete.html
@@ -12,7 +12,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除图书' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/books/templates/books/delete_review.html b/books/templates/books/delete_review.html
index 7a0fad5c..77c88340 100644
--- a/books/templates/books/delete_review.html
+++ b/books/templates/books/delete_review.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
 
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
diff --git a/books/templates/books/mark_list.html b/books/templates/books/mark_list.html
index fe96c4ad..bd81a459 100644
--- a/books/templates/books/mark_list.html
+++ b/books/templates/books/mark_list.html
@@ -13,7 +13,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ book.title }}{% trans '的标记' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
diff --git a/books/templates/books/review_detail.html b/books/templates/books/review_detail.html
index b4046532..a08d868e 100644
--- a/books/templates/books/review_detail.html
+++ b/books/templates/books/review_detail.html
@@ -17,12 +17,12 @@
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ book.cover|thumb:'normal' }}">
     <title>{{ site_name }}{% trans '书评' %} - {{ review.title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/books/templates/books/review_list.html b/books/templates/books/review_list.html
index a7682b0b..77f2db7e 100644
--- a/books/templates/books/review_list.html
+++ b/books/templates/books/review_list.html
@@ -13,7 +13,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ book.title }}{% trans '的评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/books/templates/books/scrape.html b/books/templates/books/scrape.html
index 4f0adcea..81fa6a99 100644
--- a/books/templates/books/scrape.html
+++ b/books/templates/books/scrape.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/collection/models.py b/collection/models.py
index d079ec60..dda20130 100644
--- a/collection/models.py
+++ b/collection/models.py
@@ -64,7 +64,7 @@ class Collection(UserOwnedEntity):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + reverse("collection:retrieve", args=[self.id])
+        return reverse("collection:retrieve", args=[self.id])
 
     @property
     def wish_url(self):
diff --git a/collection/templates/create_update.html b/collection/templates/create_update.html
index 43e82dcb..4f7cd5ca 100644
--- a/collection/templates/create_update.html
+++ b/collection/templates/create_update.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     <style type="text/css">
         #id_collaborative li, #id_visibility li {display: inline-block !important;}
diff --git a/collection/templates/delete.html b/collection/templates/delete.html
index ee6d440d..c187bc4c 100644
--- a/collection/templates/delete.html
+++ b/collection/templates/delete.html
@@ -20,12 +20,12 @@
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
     <title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.4/htmx.min.js"></script>
 </head>
 
 <body>
diff --git a/collection/templates/entity_list.html b/collection/templates/entity_list.html
index 4cdc9ee2..f4df87f5 100644
--- a/collection/templates/entity_list.html
+++ b/collection/templates/entity_list.html
@@ -12,7 +12,7 @@
     <li>
         <form hx-target=".entity-list" hx-post="{% url 'collection:append_item' form.instance.id %}" method="POST">
             {% csrf_token %}
-            <input type="url" name="url" placeholder="https://neodb.social/movies/1/" style="min-width:24rem" required>
+            <input type="url" name="url" placeholder="{{ request.scheme }}://{{ request.get_host }}/movies/1/" style="min-width:24rem" required>
             <input type="text" name="comment" placeholder="{% trans '备注' %}" style="min-width:24rem">
             <input class="button" type="submit" value="{% trans '添加' %}" >
         </form>
diff --git a/collection/templates/list.html b/collection/templates/list.html
index 0c9c75c3..027434a5 100644
--- a/collection/templates/list.html
+++ b/collection/templates/list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/common/models.py b/common/models.py
index 5d1b55b5..e897b18b 100644
--- a/common/models.py
+++ b/common/models.py
@@ -62,7 +62,7 @@ class Entity(models.Model):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + self.get_absolute_url()
+        return self.get_absolute_url()
 
     def get_json(self):
         return {
@@ -70,7 +70,7 @@ class Entity(models.Model):
             'brief': self.brief,
             'rating': self.rating,
             'url': self.url,
-            'cover_url': settings.APP_WEBSITE + self.cover.url,
+            'cover_url': self.cover.url,
             'top_tags': self.tags[:5],
             'category_name': self.verbose_category_name,
             'other_info': self.other_info,
diff --git a/common/static/css/boofilsic.css b/common/static/css/boofilsic.css
index b93d83b6..1307c064 100644
--- a/common/static/css/boofilsic.css
+++ b/common/static/css/boofilsic.css
@@ -2420,6 +2420,19 @@ select::placeholder {
   list-style: circle inside;
 }
 
+.markdownx-preview h1 {
+  font-size: 2.5em;
+}
+
+.markdownx-preview h2 {
+  font-size: 2.0em;
+}
+
+.markdownx-preview blockquote {
+  border-left: lightgray solid 0.4em;
+  padding-left: 0.4em;
+}
+
 .rating-star .jq-star {
   cursor: unset !important;
 }
diff --git a/common/static/css/boofilsic.min.css b/common/static/css/boofilsic.min.css
index 4a5c94d6..6b163092 100644
--- a/common/static/css/boofilsic.min.css
+++ b/common/static/css/boofilsic.min.css
@@ -1 +1 @@
-@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;object-fit:contain}img.emoji{height:14px;box-sizing:border-box;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:flex;justify-content:space-between;align-items:center;position:relative}.navbar .navbar__logo{flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:flex;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:inline-flex;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:flex;flex-direction:column;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;flex-direction:row}.grid .grid__aside--tablet-column{flex-direction:column}.grid--reverse-order{transform:scaleY(-1)}.grid .grid__main--reverse-order{transform:scaleY(-1)}.grid .grid__aside--reverse-order{transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{transform-origin:40px 40px;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){transform:rotate(0deg);animation-delay:-1.1s}.spinner div:nth-child(2){transform:rotate(30deg);animation-delay:-1s}.spinner div:nth-child(3){transform:rotate(60deg);animation-delay:-.9s}.spinner div:nth-child(4){transform:rotate(90deg);animation-delay:-.8s}.spinner div:nth-child(5){transform:rotate(120deg);animation-delay:-.7s}.spinner div:nth-child(6){transform:rotate(150deg);animation-delay:-.6s}.spinner div:nth-child(7){transform:rotate(180deg);animation-delay:-.5s}.spinner div:nth-child(8){transform:rotate(210deg);animation-delay:-.4s}.spinner div:nth-child(9){transform:rotate(240deg);animation-delay:-.3s}.spinner div:nth-child(10){transform:rotate(270deg);animation-delay:-.2s}.spinner div:nth-child(11){transform:rotate(300deg);animation-delay:-.1s}.spinner div:nth-child(12){transform:rotate(330deg);animation-delay:0s}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}.add-to-list-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.add-to-list-modal .add-to-list-modal__head{margin-bottom:20px}.add-to-list-modal .add-to-list-modal__head::after{content:' ';clear:both;display:table}.add-to-list-modal .add-to-list-modal__title{font-weight:bold;font-size:1.2em;float:left}.add-to-list-modal .add-to-list-modal__close-button{float:right;cursor:pointer}.add-to-list-modal .add-to-list-modal__confirm-button{float:right}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal .add-to-list-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__igdb{background-color:#323A44;color:#DFE1E2;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.source-label.source-label__bangumi{background:#FCFCFC;color:#F09199;font-style:italic;font-weight:600}.source-label.source-label__goodreads{background:#F4F1EA;color:#372213;font-weight:lighter}.source-label.source-label__tmdb{background:linear-gradient(90deg, #91CCA3, #1FB4E2);color:white;border:none;font-weight:lighter;padding-top:2px}.source-label.source-label__googlebooks{color:white;background-color:#4285F4;border-color:#4285F4}.source-label.source-label__bandcamp{color:#fff;background-color:#28A0C1;display:inline-block}.source-label.source-label__bandcamp span{display:inline-block;margin:0 4px}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;object-fit:contain;max-width:150px;object-position:top}.entity-detail .entity-detail__img-origin{cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:flex;color:#00a1cc;background-color:transparent;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:8px}.entity-sort .entity-sort__count{color:#bbb}.entity-sort .entity-sort__count::before{content:'('}.entity-sort .entity-sort__count::after{content:')'}.entity-sort .entity-sort__entity-list{display:flex;justify-content:flex-start;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:flex;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:flex;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:flex;margin:auto;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:flex;justify-content:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;transform:translate(50%, -50%)}.track-carousel__button--next{right:0;transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:flex;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:flex}}.aside-section-wrapper{display:flex;flex:1;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:flex;justify-content:space-between}.action-panel .action-panel__button-group--center{justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:flex;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:flex;justify-content:flex-start}.user-relation .user-relation__related-user-list:last-of-type{margin-bottom:0}.user-relation .user-relation__related-user{flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");width:48px;height:48px}@media (min-width: 575.98px) and (max-width: 991.98px){.user-relation .user-relation__related-user-avatar{height:unset;width:60%;max-width:96px}}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__body{padding-left:0}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.import-panel{overflow-x:hidden}.import-panel .import-panel__label{display:inline-block;margin-bottom:10px}.import-panel .import-panel__body{padding-left:0;border:2px dashed #00a1cc;padding:6px 9px}.import-panel .import-panel__body form{margin:0}@media (max-width: 991.98px){.import-panel .import-panel__body{border:unset;padding-left:0}}.import-panel .import-panel__help{background-color:#e5e5e5;border-radius:100000px;display:inline-block;width:16px;height:16px;text-align:center;font-size:12px;cursor:help}.import-panel .import-panel__checkbox{display:inline-block;margin-right:10px}.import-panel .import-panel__checkbox label{display:inline}.import-panel .import-panel__checkbox input[type="checkbox"]{margin:0;position:relative;top:2px}.import-panel .import-panel__checkbox--last{margin-right:0}.import-panel .import-panel__file-input{margin-top:10px}.import-panel .import-panel__button{line-height:unset;height:unset;padding:4px 15px}.import-panel .import-panel__progress{padding-top:10px}.import-panel .import-panel__progress:not(:first-child){border-top:#bbb 1px dashed}.import-panel .import-panel__progress label{display:inline}.import-panel .import-panel__progress progress{background-color:#d5d5d5;border-radius:0;height:10px;width:54%}.import-panel .import-panel__progress progress::-webkit-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__progress progress::-webkit-progress-value{background-color:#00a1cc}.import-panel .import-panel__progress progress::-moz-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__last-task:not(:first-child){padding-top:4px;border-top:#bbb 1px dashed}.import-panel .import-panel__last-task .index:not(:last-of-type){margin-right:8px}.import-panel .import-panel__fail-urls{margin-top:10px}.import-panel .import-panel__fail-urls li{word-break:break-all}.import-panel .import-panel__fail-urls ul{max-height:100px;overflow-y:auto}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:flex;margin-bottom:10px;flex-direction:column}.entity-card--horizontal{flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{flex-direction:column !important}.entity-card--horizontal{flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:flex;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{flex-direction:row}.action-panel .action-panel__button-group{justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:flex;justify-content:center;align-items:center;cursor:pointer;transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:2000px;transition:max-height 1s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;transition:max-height 1s ease-out;overflow:hidden}.entity-card{flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset}.tippy-box{border:#606c76 1px solid;background-color:#f7f7f7;padding:3px 5px}.tag-input input{flex-grow:1}.tools-section-wrapper input,.tools-section-wrapper select{width:unset}
+@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;object-fit:contain}img.emoji{height:14px;box-sizing:border-box;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:flex;justify-content:space-between;align-items:center;position:relative}.navbar .navbar__logo{flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:flex;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:inline-flex;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:flex;flex-direction:column;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;flex-direction:row}.grid .grid__aside--tablet-column{flex-direction:column}.grid--reverse-order{transform:scaleY(-1)}.grid .grid__main--reverse-order{transform:scaleY(-1)}.grid .grid__aside--reverse-order{transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{transform-origin:40px 40px;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){transform:rotate(0deg);animation-delay:-1.1s}.spinner div:nth-child(2){transform:rotate(30deg);animation-delay:-1s}.spinner div:nth-child(3){transform:rotate(60deg);animation-delay:-.9s}.spinner div:nth-child(4){transform:rotate(90deg);animation-delay:-.8s}.spinner div:nth-child(5){transform:rotate(120deg);animation-delay:-.7s}.spinner div:nth-child(6){transform:rotate(150deg);animation-delay:-.6s}.spinner div:nth-child(7){transform:rotate(180deg);animation-delay:-.5s}.spinner div:nth-child(8){transform:rotate(210deg);animation-delay:-.4s}.spinner div:nth-child(9){transform:rotate(240deg);animation-delay:-.3s}.spinner div:nth-child(10){transform:rotate(270deg);animation-delay:-.2s}.spinner div:nth-child(11){transform:rotate(300deg);animation-delay:-.1s}.spinner div:nth-child(12){transform:rotate(330deg);animation-delay:0s}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}.add-to-list-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.add-to-list-modal .add-to-list-modal__head{margin-bottom:20px}.add-to-list-modal .add-to-list-modal__head::after{content:' ';clear:both;display:table}.add-to-list-modal .add-to-list-modal__title{font-weight:bold;font-size:1.2em;float:left}.add-to-list-modal .add-to-list-modal__close-button{float:right;cursor:pointer}.add-to-list-modal .add-to-list-modal__confirm-button{float:right}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal .add-to-list-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__igdb{background-color:#323A44;color:#DFE1E2;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.source-label.source-label__bangumi{background:#FCFCFC;color:#F09199;font-style:italic;font-weight:600}.source-label.source-label__goodreads{background:#F4F1EA;color:#372213;font-weight:lighter}.source-label.source-label__tmdb{background:linear-gradient(90deg, #91CCA3, #1FB4E2);color:white;border:none;font-weight:lighter;padding-top:2px}.source-label.source-label__googlebooks{color:white;background-color:#4285F4;border-color:#4285F4}.source-label.source-label__bandcamp{color:#fff;background-color:#28A0C1;display:inline-block}.source-label.source-label__bandcamp span{display:inline-block;margin:0 4px}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;object-fit:contain;max-width:150px;object-position:top}.entity-detail .entity-detail__img-origin{cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:flex;color:#00a1cc;background-color:transparent;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:8px}.entity-sort .entity-sort__count{color:#bbb}.entity-sort .entity-sort__count::before{content:'('}.entity-sort .entity-sort__count::after{content:')'}.entity-sort .entity-sort__entity-list{display:flex;justify-content:flex-start;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:flex;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:flex;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:flex;margin:auto;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:flex;justify-content:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;transform:translate(50%, -50%)}.track-carousel__button--next{right:0;transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:flex;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:flex}}.aside-section-wrapper{display:flex;flex:1;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:flex;justify-content:space-between}.action-panel .action-panel__button-group--center{justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:flex;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:flex;justify-content:flex-start}.user-relation .user-relation__related-user-list:last-of-type{margin-bottom:0}.user-relation .user-relation__related-user{flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{background-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");width:48px;height:48px}@media (min-width: 575.98px) and (max-width: 991.98px){.user-relation .user-relation__related-user-avatar{height:unset;width:60%;max-width:96px}}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__body{padding-left:0}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.import-panel{overflow-x:hidden}.import-panel .import-panel__label{display:inline-block;margin-bottom:10px}.import-panel .import-panel__body{padding-left:0;border:2px dashed #00a1cc;padding:6px 9px}.import-panel .import-panel__body form{margin:0}@media (max-width: 991.98px){.import-panel .import-panel__body{border:unset;padding-left:0}}.import-panel .import-panel__help{background-color:#e5e5e5;border-radius:100000px;display:inline-block;width:16px;height:16px;text-align:center;font-size:12px;cursor:help}.import-panel .import-panel__checkbox{display:inline-block;margin-right:10px}.import-panel .import-panel__checkbox label{display:inline}.import-panel .import-panel__checkbox input[type="checkbox"]{margin:0;position:relative;top:2px}.import-panel .import-panel__checkbox--last{margin-right:0}.import-panel .import-panel__file-input{margin-top:10px}.import-panel .import-panel__button{line-height:unset;height:unset;padding:4px 15px}.import-panel .import-panel__progress{padding-top:10px}.import-panel .import-panel__progress:not(:first-child){border-top:#bbb 1px dashed}.import-panel .import-panel__progress label{display:inline}.import-panel .import-panel__progress progress{background-color:#d5d5d5;border-radius:0;height:10px;width:54%}.import-panel .import-panel__progress progress::-webkit-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__progress progress::-webkit-progress-value{background-color:#00a1cc}.import-panel .import-panel__progress progress::-moz-progress-bar{background-color:#d5d5d5}.import-panel .import-panel__last-task:not(:first-child){padding-top:4px;border-top:#bbb 1px dashed}.import-panel .import-panel__last-task .index:not(:last-of-type){margin-right:8px}.import-panel .import-panel__fail-urls{margin-top:10px}.import-panel .import-panel__fail-urls li{word-break:break-all}.import-panel .import-panel__fail-urls ul{max-height:100px;overflow-y:auto}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:flex;margin-bottom:10px;flex-direction:column}.entity-card--horizontal{flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{flex-direction:column !important}.entity-card--horizontal{flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:flex;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{flex-direction:row}.action-panel .action-panel__button-group{justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:flex;justify-content:center;align-items:center;cursor:pointer;transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{transition:transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:2000px;transition:max-height 1s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;transition:max-height 1s ease-out;overflow:hidden}.entity-card{flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.markdownx-preview h1{font-size:2.5em}.markdownx-preview h2{font-size:2.0em}.markdownx-preview blockquote{border-left:lightgray solid 0.4em;padding-left:0.4em}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;box-shadow:none;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset}.tippy-box{border:#606c76 1px solid;background-color:#f7f7f7;padding:3px 5px}.tag-input input{flex-grow:1}.tools-section-wrapper input,.tools-section-wrapper select{width:unset}
diff --git a/common/static/js/home.js b/common/static/js/home.js
index ca1cb32b..d32ac888 100644
--- a/common/static/js/home.js
+++ b/common/static/js/home.js
@@ -17,6 +17,24 @@ $(document).ready( function() {
         $(".mast-following-more").hide();
         $(".mast-followers-more").hide();
 
+        getUserInfo(
+            id, 
+            mast_uri, 
+            token, 
+            function(userData) {
+                let userName;
+                if (userData.display_name) {
+                    userName = translateEmojis(userData.display_name, userData.emojis, true);
+                } else {
+                    userName = userData.username;
+                }
+                $("#userInfoCard .mast-avatar").attr("src", userData.avatar);
+                $("#userInfoCard .mast-displayname").html(userName);
+                $("#userInfoCard .mast-brief").text($(userData.note).text());
+                $(userInfoSpinner).remove();
+            }
+        );
+
         getFollowers(
             id,
             mast_uri,
diff --git a/common/static/js/mastodon.js b/common/static/js/mastodon.js
index 02a54679..7e62f6d5 100644
--- a/common/static/js/mastodon.js
+++ b/common/static/js/mastodon.js
@@ -155,6 +155,7 @@ function getEmojiDict(emoji_list) {
 }
 
 function translateEmojis(text, emoji_list, large) {
+    console.log(text)
     let dict = getEmojiDict(emoji_list);
     let regex = /:(.*?):/g;
     let translation = null
diff --git a/common/static/lib/css/neo.css b/common/static/lib/css/collection.css
similarity index 91%
rename from common/static/lib/css/neo.css
rename to common/static/lib/css/collection.css
index 2f2fcbce..22aa8886 100644
--- a/common/static/lib/css/neo.css
+++ b/common/static/lib/css/collection.css
@@ -1,20 +1,3 @@
-.markdownx-preview h1 {
-    font-size: 2.5em;
-}
-
-.markdownx-preview h2 {
-    font-size: 2.0em;
-}
-
-.markdownx-preview h3 {
-    font-size: 1.6em;
-}
-
-.markdownx-preview blockquote {
-    border-left: lightgray solid 0.4em;
-    padding-left: 0.4em;
-}
-
 .collection-item-position-edit {
     float: right;
 }
diff --git a/common/static/lib/js/hyperscript-0.9.5.min.js b/common/static/lib/js/hyperscript-0.9.5.min.js
deleted file mode 100644
index a17cb5e1..00000000
--- a/common/static/lib/js/hyperscript-0.9.5.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e||self)._hyperscript=t()}(this,function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function t(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}function n(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,o(e,t)}function r(e){return r=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},r(e)}function o(e,t){return o=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},o(e,t)}function a(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(e){return!1}}function i(e,t,n){return i=a()?Reflect.construct:function(e,t,n){var r=[null];r.push.apply(r,t);var a=new(Function.bind.apply(e,r));return n&&o(a,n.prototype),a},i.apply(null,arguments)}function u(e){var t="function"==typeof Map?new Map:void 0;return u=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return i(e,arguments,r(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),o(n,e)},u(e)}function l(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function s(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(n)return(n=n.call(e)).next.bind(n);if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return l(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0;return function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function c(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function f(e,t){var n=e[t];if(n)return n;var r={};return e[t]=r,r}function m(e,t){return new(e.bind.apply(e,[e].concat(t)))}var p,d=globalThis,v=function(e){function n(e,t,n){this._css=e,this.relativeToElement=t,this.escape=n}var r=n.prototype;return r.contains=function(e){for(var t,n=s(this);!(t=n()).done;)if(t.value.contains(e))return!0;return!1},r[e]=function(){return this.selectMatches()[Symbol.iterator]()},r.selectMatches=function(){return T.getRootNode(this.relativeToElement).querySelectorAll(this.css)},t(n,[{key:"css",get:function(){return this.escape?T.escapeSelector(this._css):this._css}},{key:"className",get:function(){return this._css.substr(1)}},{key:"id",get:function(){return this.className()}},{key:"length",get:function(){return this.selectMatches().length}}]),n}(Symbol.iterator),h=function(){var e={"+":"PLUS","-":"MINUS","*":"MULTIPLY","/":"DIVIDE",".":"PERIOD","..":"ELLIPSIS","\\":"BACKSLASH",":":"COLON","%":"PERCENT","|":"PIPE","!":"EXCLAMATION","?":"QUESTION","#":"POUND","&":"AMPERSAND",$:"DOLLAR",";":"SEMI",",":"COMMA","(":"L_PAREN",")":"R_PAREN","<":"L_ANG",">":"R_ANG","<=":"LTE_ANG",">=":"GTE_ANG","==":"EQ","===":"EQQ","!=":"NEQ","!==":"NEQQ","{":"L_BRACE","}":"R_BRACE","[":"L_BRACKET","]":"R_BRACKET","=":"EQUALS"};function t(e){return i(e)||a(e)||"-"===e||"_"===e||":"===e}function n(e){return i(e)||a(e)||"-"===e||"_"===e||":"===e}function r(e){return" "===e||"\t"===e||o(e)}function o(e){return"\r"===e||"\n"===e}function a(e){return e>="0"&&e<="9"}function i(e){return e>="a"&&e<="z"||e>="A"&&e<="Z"}function u(e,t){return"_"===e||"$"===e}function l(e,t,n){o();var r=null;function o(){for(;"WHITESPACE"===f(0,!0).type;)t.push(e.shift())}function a(e,t){E.raiseParseError(e,t)}function i(e){if(m()&&m().op&&m().value===e)return s()}function u(e,t,n,r){if(m()&&m().type&&[e,t,n,r].indexOf(m().type)>=0)return s()}function l(e,t){if(-1===p.indexOf(e))return t=t||"IDENTIFIER",m()&&m().value===e&&m().type===t?s():void 0}function s(){var n=e.shift();return t.push(n),r=n,o(),n}function c(n,r){for(var a=[],i=f(0,!0);!(null!=r&&i.type===r||null!=n&&i.value===n||"EOF"===i.type);){var u=e.shift();t.push(u),a.push(i),i=f(0,!0)}return o(),a}function f(t,n){var r,o=0;do{if(!n)for(;e[o]&&"WHITESPACE"===e[o].type;)o++;r=e[o],t--,o++}while(t>-1);return r||{type:"EOF",value:"<<<EOF>>>"}}function m(){return f(0)}var p=[];return{pushFollow:function(e){p.push(e)},popFollow:function(){p.pop()},clearFollow:function(){var e=p;return p=[],e},restoreFollow:function(e){p=e},matchAnyToken:function(e,t,n){for(var r=0;r<arguments.length;r++){var o=arguments[r],a=l(o);if(a)return a}},matchAnyOpToken:function(e,t,n){for(var r=0;r<arguments.length;r++){var o=arguments[r],a=i(o);if(a)return a}},matchOpToken:i,requireOpToken:function(e){var t=i(e);if(t)return t;a(this,"Expected '"+e+"' but found '"+m().value+"'")},matchTokenType:u,requireTokenType:function(e,t,n,r){var o=u(e,t,n,r);if(o)return o;a(this,"Expected one of "+JSON.stringify([e,t,n]))},consumeToken:s,peekToken:function(t,n,r){return e[n]&&e[n].value===t&&e[n].type===r},matchToken:l,requireToken:function(e,t){var n=l(e,t);if(n)return n;a(this,"Expected '"+e+"' but found '"+m().value+"'")},list:e,consumed:t,source:n,hasMore:function(){return e.length>0},currentToken:m,lastMatch:function(){return r},token:f,consumeUntil:c,consumeUntilWhitespace:function(){return c(null,"WHITESPACE")},lastWhitespace:function(){return t[t.length-1]&&"WHITESPACE"===t[t.length-1].type?t[t.length-1].value:""},sourceFor:function(){return n.substring(this.startToken.start,this.endToken.end)},lineFor:function(){return n.split("\n")[this.startToken.line-1]}}}function s(e){if(e.length>0){var t=e[e.length-1];if("IDENTIFIER"===t.type||"CLASS_REF"===t.type||"ID_REF"===t.type)return!1;if(t.op&&(">"===t.value||")"===t.value))return!1}return!0}return{tokenize:function(c,f){var m,p=[],d=c,v=0,h=0,E=1,y="<START>",T=0;function k(){return f&&0===T}for(;v<d.length;)if("-"!==C()||"-"!==A()||!r(F())&&""!==F())if(r(C()))p.push(D());else if(M()||"."!==C()||!i(A())&&"{"!==A())if(M()||"#"!==C()||!i(A())&&"{"!==A())if("["===C()&&"@"===A())p.push(q());else if("@"===C())p.push(w());else if("*"===C()&&i(A()))p.push(S());else if(i(C())||!k()&&u(C()))p.push(I());else if(a(C()))p.push(R());else if(k()||'"'!==C()&&"`"!==C())if(k()||"'"!==C()){if(e[C()])"$"===y&&"{"===C()&&T++,"}"===C()&&T--,p.push(O());else if(k()||"`"===(m=C())||"^"===m)p.push(g("RESERVED",P()));else if(v<d.length)throw Error("Unknown token: "+C()+" ")}else s(p)?p.push(L()):p.push(O());else p.push(L());else p.push(N());else p.push(b());else x();return l(p,[],d);function g(e,t){return{type:e,value:t,start:v,end:v+1,column:h,line:E}}function x(){for(;C()&&!o(C());)P();P()}function b(){var e=g("CLASS_REF"),n=P();if("{"===C()){for(e.template=!0,n+=P();C()&&"}"!==C();)n+=P();if("}"!==C())throw Error("Unterminated class reference");n+=P()}else for(;t(C());)n+=P();return e.value=n,e.end=v,e}function q(){for(var e=g("ATTRIBUTE_REF"),t=P();v<d.length&&"]"!==C();)t+=P();return"]"===C()&&(t+=P()),e.value=t,e.end=v,e}function w(){for(var e=g("ATTRIBUTE_REF"),t=P();n(C());)t+=P();return e.value=t,e.end=v,e}function S(){for(var e=g("STYLE_REF"),t=P();i(C())||"-"===C();)t+=P();return e.value=t,e.end=v,e}function N(){var e=g("ID_REF"),t=P();if("{"===C()){for(e.template=!0,t+=P();C()&&"}"!==C();)t+=P();if("}"!==C())throw Error("Unterminated id reference");P()}else for(;n(C());)t+=P();return e.value=t,e.end=v,e}function I(){for(var e=g("IDENTIFIER"),t=P();i(C())||a(C())||u(C());)t+=P();return"!"===C()&&"beep"===t&&(t+=P()),e.value=t,e.end=v,e}function R(){for(var e=g("NUMBER"),t=P();a(C());)t+=P();for("."===C()&&a(A())&&(t+=P());a(C());)t+=P();return e.value=t,e.end=v,e}function O(){for(var t=(r=void 0,(r=g(void 0,void 0)).op=!0,r),n=P();C()&&e[n+C()];)n+=P();var r;return t.type=e[n],t.value=n,t.end=v,t}function L(){for(var e,t=g("STRING"),n=P(),r="";C()&&C()!==n;)if("\\"===C()){P();var o=P();r+="b"===o?"\b":"f"===o?"\f":"n"===o?"\n":"r"===o?"\r":"t"===o?"\t":"v"===o?"\v":o}else r+=P();if(C()!==n)throw Error("Unterminated string at [Line: "+(e=t).line+", Column: "+e.column+"]");return P(),t.value=r,t.end=v,t.template="`"===n,t}function C(){return d.charAt(v)}function A(){return d.charAt(v+1)}function F(){return d.charAt(v+2)}function P(){return y=C(),v++,h++,y}function M(){return i(y)||a(y)||")"===y||'"'===y||"'"===y||"`"===y||"}"===y||"]"===y}function D(){for(var e=g("WHITESPACE"),t="";C()&&r(C());)o(C())&&(h=0,E++),t+=P();return e.value=t,e.end=v,e}},makeTokensObject:l}}(),E=function(){var e={},t={},n={},r=[],o=[];function a(e,t,n){e.startToken=t,e.sourceFor=n.sourceFor,e.lineFor=n.lineFor,e.programSource=n.source}function i(t,n,r){return void 0===r&&(r=void 0),function(r){var o=e[t];if(o){var i=n.currentToken(),u=o(E,T,n,r);if(u)for(a(u,i,n),u.endToken=u.endToken||n.lastMatch(),r=u.root;null!=r;)a(r,i,n),r=r.root;return u}}(r)}function u(e,t,n,r){var o=i(e,t,r);return o||c(t,n||"Expected "+e),o}function l(e,t){for(var n=0;n<e.length;n++){var r=i(e[n],t);if(r)return r}}function s(t,n){e[t]=n}function c(e,t){t=(t||"Unexpected Token : "+e.currentToken().value)+"\n\n"+function(e){var t=e.currentToken(),n=e.source.split("\n"),r=n[t&&t.line?t.line-1:n.length-1];return r+"\n"+" ".repeat(t&&t.line?t.column:r.length-1)+"^^\n\n"}(e);var n=new Error(t);throw n.tokens=e,n}function f(e){return t[e.value]}function m(e){return n[e.value]}return s("feature",function(e,t,r){if(r.matchOpToken("(")){var o=e.requireElement("feature",r);return r.requireOpToken(")"),o}var a=n[r.currentToken().value];if(a)return a(e,t,r)}),s("command",function(e,n,r){if(r.matchOpToken("(")){var o=e.requireElement("command",r);return r.requireOpToken(")"),o}var a,i=t[r.currentToken().value];return i?a=i(e,n,r):"IDENTIFIER"===r.currentToken().type&&(a=e.parseElement("pseudoCommand",r)),a?e.parseElement("indirectStatement",r,a):a}),s("commandList",function(e,t,n){var r=e.parseElement("command",n);if(r){n.matchToken("then");var o=e.parseElement("commandList",n);return o&&(r.next=o),r}}),s("leaf",function(e,t,n){var o=l(r,n);return null==o?i("symbol",n):o}),s("indirectExpression",function(e,t,n,r){for(var a=0;a<o.length;a++){var i=o[a];r.endToken=n.lastMatch();var u=e.parseElement(i,n,r);if(u)return u}return r}),s("indirectStatement",function(e,t,n,r){if(n.matchToken("unless")){r.endToken=n.lastMatch();var o={type:"unlessStatementModifier",args:[e.requireElement("expression",n)],op:function(e,t){return t?this.next:r},execute:function(e){return t.unifiedExec(this,e)}};return r.parent=o,o}return r}),s("primaryExpression",function(e,t,n){var r=e.parseElement("leaf",n);if(r)return e.parseElement("indirectExpression",n,r);e.raiseParseError(n,"Unexpected value: "+n.currentToken().value)}),{setParent:function e(t,n){"object"==typeof t&&(t.parent=n,"object"==typeof n&&(n.children=n.children||new Set,n.children.add(t)),e(t.next,n))},requireElement:u,parseElement:i,featureStart:m,commandStart:f,commandBoundary:function(e){return!("end"!=e.value&&"then"!=e.value&&"else"!=e.value&&"otherwise"!=e.value&&")"!=e.value&&!f(e)&&!m(e)&&"EOF"!=e.type)},parseAnyOf:l,parseHyperScript:function(e){var t=i("hyperscript",e);if(e.hasMore()&&c(e),t)return t},raiseParseError:c,addGrammarElement:s,addCommand:function(n,r){var o=n+"Command",a=function(e,t,n){var a=r(e,t,n);if(a)return a.type=o,a.execute=function(e){return e.meta.command=a,t.unifiedExec(this,e)},a};e[o]=a,t[n]=a},addFeature:function(t,r){var o=t+"Feature",a=function(e,n,a){var i=r(e,n,a);if(i)return i.isFeature=!0,i.keyword=t,i.type=o,i};e[o]=a,n[t]=a},addLeafExpression:function(e,t){r.push(e),s(e,t)},addIndirectExpression:function(e,t){o.push(e),s(e,t)},parseStringTemplate:function(e){var t=[""];do{if(t.push(e.lastWhitespace()),"$"===e.currentToken().value){e.consumeToken();var n=e.matchOpToken("{");t.push(u("expression",e)),n&&e.requireOpToken("}"),t.push("")}else if("\\"===e.currentToken().value)e.consumeToken(),e.consumeToken();else{var r=e.consumeToken();t[t.length-1]+=r?r.value:""}}while(e.hasMore());return t.push(e.lastWhitespace()),t},ensureTerminated:function(e){for(var t={type:"implicitReturn",op:function(e){return e.meta.returned=!0,e.meta.resolve&&e.meta.resolve(),T.HALT},execute:function(e){}},n=e;n.next;)n=n.next;n.next=t}}}(),y={dynamicResolvers:[function(e,t){if("Fixed"===e)return Number(t).toFixed();if(0===e.indexOf("Fixed:")){var n=e.split(":")[1];return Number(t).toFixed(parseInt(n))}}],String:function(e){return e.toString?e.toString():""+e},Int:function(e){return parseInt(e)},Float:function(e){return parseFloat(e)},Number:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return Number(e)}),Date:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return new Date(e)}),Array:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return Array.from(e)}),JSON:function(e){function t(t){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(e){return JSON.stringify(e)}),Object:function(e){return e instanceof String&&(e=e.toString()),"string"==typeof e?JSON.parse(e):c({},e)}},T=function(){function e(e,t){var n=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return n&&n.call(e,t)}function t(e,t,n,r){(n=n||{}).sender=r;var o=function(e,t){var n;return d.Event&&"function"==typeof d.Event?(n=new Event(e,{bubbles:!0,cancelable:!0})).detail=t:(n=document.createEvent("CustomEvent")).initCustomEvent(e,!0,!0,t),n}(t,n);return e.dispatchEvent(o)}function r(e){return Array.isArray(e)||"undefined"!=typeof NodeList&&(e instanceof NodeList||e instanceof HTMLCollection)}function o(e){return e instanceof v||r(e)}function a(e,t){if(null==e);else if(function(e){return"object"==typeof e&&Symbol.iterator in e&&"function"==typeof e[Symbol.iterator]}(e))for(var n,o=s(e);!(n=o()).done;)t(n.value);else if(r(e))for(var a=0;a<e.length;a++)t(e[a]);else t(e)}function i(e){for(var t=0;t<e.length;t++){var n=e[t];if(n.asyncWrapper&&(e[t]=n.value),Array.isArray(n))for(var r=0;r<n.length;r++){var o=n[r];o.asyncWrapper&&(n[r]=o.value)}}}var l={};function m(e,t){var n=[t],r=!1,o=!1;if(e.args)for(var a=0;a<e.args.length;a++){var u=e.args[a];if(null==u)n.push(null);else if(Array.isArray(u)){for(var l=[],s=0;s<u.length;s++){var c=u[s];(f=c?c.evaluate(t):null)&&(f.then?r=!0:f.asyncWrapper&&(o=!0)),l.push(f)}n.push(l)}else if(u.evaluate){var f;(f=u.evaluate(t))&&(f.then?r=!0:f.asyncWrapper&&(o=!0)),n.push(f)}else n.push(u)}return r?new Promise(function(t,r){n=function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n];Array.isArray(r)?t.push(Promise.all(r)):t.push(r)}return t}(n),Promise.all(n).then(function(n){o&&i(n);try{var a=e.op.apply(e,n);t(a)}catch(e){r(e)}}).catch(function(e){r(e)})}):(o&&i(n),e.op.apply(e,n))}var k=null;function g(){return null==k&&(k=p.config.attributes.replace(/ /g,"").split(",")),k}function x(e){for(var t=0;t<g().length;t++){var n=g()[t];if(e.hasAttribute&&e.hasAttribute(n))return e.getAttribute(n)}return e instanceof HTMLScriptElement&&"text/hyperscript"===e.type?e.innerText:null}var b=new WeakMap;function q(e){var t=b.get(e);return void 0===t&&b.set(e,t={}),t}function w(e,t){e&&(c(t,q(e)),w(e.parentElement,t))}function S(e,t,n,r){var o={meta:{parser:E,lexer:h,runtime:T,owner:e,feature:t,iterators:{}},me:n,event:r,target:r?r.target:null,detail:r?r.detail:null,sender:r&&r.detail?r.detail.sender:null,body:"document"in d?document.body:null};return o.meta.ctx=o,w(e,o),o}function N(e){var t=h.tokenize(e);if(E.commandStart(t.currentToken())){var n=E.requireElement("commandList",t);return t.hasMore()&&E.raiseParseError(t),E.ensureTerminated(n),n}if(E.featureStart(t.currentToken())){var r=E.requireElement("hyperscript",t);return t.hasMore()&&E.raiseParseError(t),r}var o=E.requireElement("expression",t);return t.hasMore()&&E.raiseParseError(t),o}function I(e,n){if(!e.closest||!e.closest(p.config.disableSelector)){var r=O(e);if(!r.initialized){var o=x(e);if(o)try{r.initialized=!0,r.script=o;var a=h.tokenize(o),i=E.parseHyperScript(a);if(!i)return;i.apply(n||e,e),setTimeout(function(){t(n||e,"load",{hyperscript:!0})},1)}catch(t){T.triggerEvent(e,"exception",{error:t}),console.error("hyperscript errors were found on the following element:",e,"\n\n",t.message,t.stack)}}}}var R=new WeakMap;function O(e){var t=R.get(e);return void 0===t&&R.set(e,t={}),t}function L(e){var t=e.meta&&e.meta.owner;if(t){var n=O(t),r="elementScope";return e.meta.feature&&e.meta.feature.behavior&&(r=e.meta.feature.behavior+"Scope"),f(n,r)}return{}}function C(e,t,n){if(null!=e){var r=n(e,t);if(void 0!==r)return r;if(o(e)){for(var a,i=[],u=s(e);!(a=u()).done;){var l=n(a.value,t);l&&i.push(l)}return i}}}return{typeCheck:function(e,t,n){return!(null!=e||!n)||Object.prototype.toString.call(e).slice(8,-1)===t},forEach:a,implicitLoop:function(e,t){if(o(e))for(var n,r=s(e);!(n=r()).done;)t(n.value);else t(e)},shouldAutoIterate:o,triggerEvent:t,matchesSelector:e,getScript:x,processNode:function(t){var n=T.getScriptSelector();e(t,n)&&I(t,t),t instanceof HTMLScriptElement&&"text/hyperscript"===t.type&&I(t,document.body),t.querySelectorAll&&a(t.querySelectorAll(n+", [type='text/hyperscript']"),function(e){I(e,e instanceof HTMLScriptElement&&"text/hyperscript"===e.type?document.body:e)})},evaluate:function(e,t,r){var o=function(e){function t(t){var n;return(n=e.call(this)||this).module=t,n}return n(t,e),t.prototype.toString=function(){return this.module.id},t}(u(EventTarget)),a="document"in d?d.document.body:new o(r&&r.module);t=c(S(a,null,a,null),t||{});var i=N(e);return i.execute?(i.execute(t),t.result):i.apply?(i.apply(a,a,r),q(a)):i.evaluate(t)},evaluateNoPromise:function(e,t){var n=e.evaluate(t);if(n.next)throw new Error(e.sourceFor()+" returned a Promise in a context that they are not allowed.");return n},parse:N,getScriptSelector:function(){return g().map(function(e){return"["+e+"]"}).join(", ")},resolveSymbol:function(e,t,n){if("me"===e||"my"===e||"I"===e)return t.me;if("it"===e||"its"===e)return t.result;if("you"===e||"your"===e||"yourself"===e)return t.beingTold;if("global"===n)return d[e];if("element"===n)return L(t)[e];if("local"===n)return t[e];if(t.meta&&t.meta.context){var r=t.meta.context[e];if(void 0!==r)return r}var o=t[e];return void 0!==o||void 0!==(o=L(t)[e])?o:d[e]},setSymbol:function(e,t,n,r){if("global"===n)d[e]=r;else if("element"===n)(o=L(t))[e]=r;else if("local"===n)t[e]=r;else{var o,a=t[e];void 0!==a?t[e]=r:void 0!==(a=(o=L(t))[e])?o[e]=r:t[e]=r}},makeContext:S,findNext:function e(t,n){if(t)return t.resolveNext?t.resolveNext(n):t.next?t.next:e(t.parent,n)},unifiedEval:m,convertValue:function(e,t){for(var n=y.dynamicResolvers,r=0;r<n.length;r++){var o=(0,n[r])(t,e);if(void 0!==o)return o}if(null==e)return null;var a=y[t];if(a)return a(e);throw"Unknown conversion : "+t},unifiedExec:function e(t,n){for(;;){try{var r=m(t,n)}catch(e){if(n.meta.handlingFinally)console.error(" Exception in finally block: ",e),r=l;else{if(T.registerHyperTrace(n,e),n.meta.errorHandler&&!n.meta.handlingError){n.meta.handlingError=!0,n[n.meta.errorSymbol]=e,t=n.meta.errorHandler;continue}n.meta.currentException=e,r=l}}if(null==r)return void console.error(t," did not return a next element to execute! context: ",n);if(r.then)return void r.then(function(t){e(t,n)}).catch(function(t){e({op:function(){throw t}},n)});if(r===l){if(!n.meta.finallyHandler||n.meta.handlingFinally){if(n.meta.onHalt&&n.meta.onHalt(),n.meta.currentException){if(n.meta.reject)return void n.meta.reject(n.meta.currentException);throw n.meta.currentException}return}n.meta.handlingFinally=!0,t=n.meta.finallyHandler}else t=r}},resolveProperty:function(e,t){return C(e,t,function(e,t){return e[t]})},resolveAttribute:function(e,t){return C(e,t,function(e,t){return e.getAttribute&&e.getAttribute(t)})},resolveStyle:function(e,t){return C(e,t,function(e,t){return e.style&&e.style[t]})},resolveComputedStyle:function(e,t){return C(e,t,function(e,t){return getComputedStyle(e).getPropertyValue(t)})},assignToNamespace:function(e,t,n,r){var o;for(o="undefined"!=typeof document&&e===document.body?d:q(e);t.length>0;){var a=t.shift(),i=o[a];null==i&&(o[a]=i={}),o=i}o[n]=r},registerHyperTrace:function(e,t){for(var n=[],r=null;null!=e;)n.push(e),r=e,e=e.meta.caller;null==r.meta.traceMap&&(r.meta.traceMap=new Map),r.meta.traceMap.get(t)||r.meta.traceMap.set(t,{trace:n,print:function(e){(e=e||console.error)("hypertrace /// ");for(var t=0,r=0;r<n.length;r++)t=Math.max(t,n[r].meta.feature.displayName.length);for(r=0;r<n.length;r++){var o=n[r];e("  ->",o.meta.feature.displayName.padEnd(t+2),"-",o.meta.owner)}}})},getHyperTrace:function(e,t){for(var n=e;n.meta.caller;)n=n.meta.caller;if(n.meta.traceMap)return n.meta.traceMap.get(t,[])},getInternalData:O,getHyperscriptFeatures:q,escapeSelector:function(e){return e.replace(/:/g,function(e){return"\\"+e})},nullCheck:function(e,t){if(null==e)throw new Error("'"+t.sourceFor()+"' is null")},isEmpty:function(e){return null==e||0===e.length},doesExist:function(e){if(null==e)return!1;if(o(e))for(var t=s(e);!t().done;)return!0;return!1},getRootNode:function(e){if(e&&e instanceof Node){var t=e.getRootNode();if(t instanceof Document||t instanceof ShadowRoot)return t}return document},getEventQueueFor:function(e,t){var n=O(e),r=n.eventQueues;null==r&&(r=new Map,n.eventQueues=r);var o=r.get(t);return null==o&&r.set(t,o={queue:[],executing:!1}),o},hyperscriptUrl:"document"in d?"undefined"==typeof document&&"undefined"==typeof location?new(require("url").URL)("file:"+__filename).href:"undefined"==typeof document?location.href:document.currentScript&&document.currentScript.src||new URL("_hyperscript_web.min.js",document.baseURI).href:null,HALT:l}}(),k=function(e,t,n){if(t.contains)return t.contains(n);if(t.includes)return t.includes(n);throw Error("The value of "+e.sourceFor()+" does not have a contains or includes method on it")},g=function(e,t,n){if(t.match)return!!t.match(n);if(t.matches)return t.matches(n);throw Error("The value of "+e.sourceFor()+" does not have a match or matches method on it")},x=function(e,t,n,r){var o=t.requireElement("eventName",r),a=t.parseElement("namedArgumentList",r);if("send"===e&&r.matchToken("to")||"trigger"===e&&r.matchToken("on"))var i=t.requireElement("expression",r);else i=t.requireElement("implicitMeTarget",r);var u={eventName:o,details:a,to:i,args:[i,o,a],op:function(e,t,r,o){return n.nullCheck(t,i),n.forEach(t,function(t){n.triggerEvent(t,r,o,e.me)}),n.findNext(u,e)}};return u},b=function(e,t){var n,r="text";return e.matchToken("a")||e.matchToken("an"),e.matchToken("json")||e.matchToken("Object")?r="json":e.matchToken("response")?r="response":e.matchToken("html")?r="html":e.matchToken("text")||(n=t.requireElement("dotOrColonPath",e).evaluate()),{type:r,conversion:n}};E.addLeafExpression("parenthesized",function(e,t,n){if(n.matchOpToken("(")){var r=n.clearFollow();try{var o=e.requireElement("expression",n)}finally{n.restoreFollow(r)}return n.requireOpToken(")"),o}}),E.addLeafExpression("string",function(e,t,n){var r=n.matchTokenType("STRING");if(r){var o,a=r.value;if(r.template){var i=h.tokenize(a,!0);o=e.parseStringTemplate(i)}else o=[];return{type:"string",token:r,args:o,op:function(e){for(var t="",n=1;n<arguments.length;n++){var r=arguments[n];void 0!==r&&(t+=r)}return t},evaluate:function(e){return 0===o.length?a:t.unifiedEval(this,e)}}}}),E.addGrammarElement("nakedString",function(e,t,n){if(n.hasMore()){var r=n.consumeUntilWhitespace();return n.matchTokenType("WHITESPACE"),{type:"nakedString",tokens:r,evaluate:function(e){return r.map(function(e){return e.value}).join("")}}}}),E.addLeafExpression("number",function(e,t,n){var r=n.matchTokenType("NUMBER");if(r){var o=r,a=parseFloat(r.value);return{type:"number",value:a,numberToken:o,evaluate:function(){return a}}}}),E.addLeafExpression("idRef",function(e,t,n){var r=n.matchTokenType("ID_REF");if(r){if(r.template){var o=r.value.substr(2,r.value.length-2),a=h.tokenize(o);return{type:"idRefTemplate",args:[e.requireElement("expression",a)],op:function(e,n){return t.getRootNode(e.me).getElementById(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}var i=r.value.substr(1);return{type:"idRef",css:r.value,value:i,evaluate:function(e){return t.getRootNode(e.me).getElementById(i)}}}}),E.addLeafExpression("classRef",function(e,t,n){var r=n.matchTokenType("CLASS_REF");if(r){if(r.template){var o=r.value.substr(2,r.value.length-2),a=h.tokenize(o);return{type:"classRefTemplate",args:[e.requireElement("expression",a)],op:function(e,t){return new v("."+t,e.me,!0)},evaluate:function(e){return t.unifiedEval(this,e)}}}var i=r.value;return{type:"classRef",css:i,evaluate:function(e){return new v(i,e.me,!0)}}}});var q=function(e,r){function o(t,n,r){var o;return(o=e.call(this,t,n)||this).templateParts=r,o.elements=r.filter(function(e){return e instanceof Element}),o}return n(o,e),o.prototype[r]=function(){this.elements.forEach(function(e,t){return e.dataset.hsQueryId=t});var t=e.prototype[Symbol.iterator].call(this);return this.elements.forEach(function(e){return e.removeAttribute("data-hs-query-id")}),t},t(o,[{key:"css",get:function(){for(var e,t="",n=0,r=s(this.templateParts);!(e=r()).done;){var o=e.value;o instanceof Element?t+="[data-hs-query-id='"+n+++"']":t+=o}return t}}]),o}(v,Symbol.iterator);E.addLeafExpression("queryRef",function(e,t,n){if(n.matchOpToken("<")){var r=n.consumeUntil("/");n.requireOpToken("/"),n.requireOpToken(">");var o=r.map(function(e){return"STRING"===e.type?'"'+e.value+'"':e.value}).join("");if(o.indexOf("$")>=0)var a=!0,i=h.tokenize(o,!0),u=e.parseStringTemplate(i);return{type:"queryRef",css:o,args:u,op:function(e){return a?new q(o,e.me,[].slice.call(arguments,1)):new v(o,e.me)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("attributeRef",function(e,t,n){var r=n.matchTokenType("ATTRIBUTE_REF");if(r){var o=r.value;if(0===o.indexOf("["))var a=o.substring(2,o.length-1);else a=o.substring(1);var i="["+a+"]",u=a.split("="),l=u[0],s=u[1];return s&&0===s.indexOf('"')&&(s=s.substring(1,s.length-1)),{type:"attributeRef",name:l,css:i,value:s,op:function(e){var t=e.beingTold||e.me;if(t)return t.getAttribute(l)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("styleRef",function(e,t,n){var r=n.matchTokenType("STYLE_REF");if(r){var o=r.value.substr(1);return o.startsWith("computed-")?{type:"computedStyleRef",name:o=o.substr("computed-".length),op:function(e){var n=e.beingTold||e.me;if(n)return t.resolveComputedStyle(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}}:{type:"styleRef",name:o,op:function(e){var n=e.beingTold||e.me;if(n)return t.resolveStyle(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("objectKey",function(e,t,n){var r;if(r=n.matchTokenType("STRING"))return{type:"objectKey",key:r.value,evaluate:function(){return r.value}};if(n.matchOpToken("[")){var o=e.parseElement("expression",n);return n.requireOpToken("]"),{type:"objectKey",expr:o,args:[o],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}var a="";do{(r=n.matchTokenType("IDENTIFIER")||n.matchOpToken("-"))&&(a+=r.value)}while(r);return{type:"objectKey",key:a,evaluate:function(){return a}}}),E.addLeafExpression("objectLiteral",function(e,t,n){if(n.matchOpToken("{")){var r=[],o=[];if(!n.matchOpToken("}")){do{var a=e.requireElement("objectKey",n);n.requireOpToken(":");var i=e.requireElement("expression",n);o.push(i),r.push(a)}while(n.matchOpToken(","));n.requireOpToken("}")}return{type:"objectLiteral",args:[r,o],op:function(e,t,n){for(var r={},o=0;o<t.length;o++)r[t[o]]=n[o];return r},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("nakedNamedArgumentList",function(e,t,n){var r=[],o=[];if("IDENTIFIER"===n.currentToken().type)do{var a=n.requireTokenType("IDENTIFIER");n.requireOpToken(":");var i=e.requireElement("expression",n);o.push(i),r.push({name:a,value:i})}while(n.matchOpToken(","));return{type:"namedArgumentList",fields:r,args:[o],op:function(e,t){for(var n={_namedArgList_:!0},o=0;o<t.length;o++)n[r[o].name.value]=t[o];return n},evaluate:function(e){return t.unifiedEval(this,e)}}}),E.addGrammarElement("namedArgumentList",function(e,t,n){if(n.matchOpToken("(")){var r=e.requireElement("nakedNamedArgumentList",n);return n.requireOpToken(")"),r}}),E.addGrammarElement("symbol",function(e,t,n){var r="default";n.matchToken("global")?r="global":n.matchToken("element")||n.matchToken("module")?(r="element",n.matchOpToken("'")&&n.requireToken("s")):n.matchToken("local")&&(r="local");var o=n.matchOpToken(":"),a=n.matchTokenType("IDENTIFIER");if(a){var i=a.value;return o&&(i=":"+i),"default"===r&&(0===i.indexOf("$")&&(r="global"),0===i.indexOf(":")&&(r="element")),{type:"symbol",token:a,scope:r,name:i,evaluate:function(e){return t.resolveSymbol(i,e,r)}}}}),E.addGrammarElement("implicitMeTarget",function(e,t,n){return{type:"implicitMeTarget",evaluate:function(e){return e.beingTold||e.me}}}),E.addLeafExpression("boolean",function(e,t,n){var r=n.matchToken("true")||n.matchToken("false");if(r){var o="true"===r.value;return{type:"boolean",evaluate:function(e){return o}}}}),E.addLeafExpression("null",function(e,t,n){if(n.matchToken("null"))return{type:"null",evaluate:function(e){return null}}}),E.addLeafExpression("arrayLiteral",function(e,t,n){if(n.matchOpToken("[")){var r=[];if(!n.matchOpToken("]")){do{var o=e.requireElement("expression",n);r.push(o)}while(n.matchOpToken(","));n.requireOpToken("]")}return{type:"arrayLiteral",values:r,args:[r],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("blockLiteral",function(e,t,n){if(n.matchOpToken("\\")){var r=[],o=n.matchTokenType("IDENTIFIER");if(o)for(r.push(o);n.matchOpToken(",");)r.push(n.requireTokenType("IDENTIFIER"));n.requireOpToken("-"),n.requireOpToken(">");var a=e.requireElement("expression",n);return{type:"blockLiteral",args:r,expr:a,evaluate:function(e){return function(){for(var t=0;t<r.length;t++)e[r[t].value]=arguments[t];return a.evaluate(e)}}}}}),E.addIndirectExpression("propertyAccess",function(e,t,n,r){if(n.matchOpToken(".")){var o=n.requireTokenType("IDENTIFIER");return e.parseElement("indirectExpression",n,{type:"propertyAccess",root:r,prop:o,args:[r],op:function(e,n){return t.resolveProperty(n,o.value)},evaluate:function(e){return t.unifiedEval(this,e)}})}}),E.addIndirectExpression("of",function(e,t,n,r){if(n.matchToken("of")){for(var o=e.requireElement("unaryExpression",n),a=null,i=r;i.root;)a=i,i=i.root;"symbol"!==i.type&&"attributeRef"!==i.type&&"styleRef"!==i.type&&"computedStyleRef"!==i.type&&e.raiseParseError(n,"Cannot take a property of a non-symbol: "+i.type);var u="attributeRef"===i.type,l="styleRef"===i.type||"computedStyleRef"===i.type;if(u||l)var s=i;var c=i.name,f={type:"ofExpression",prop:i.token,root:o,attribute:s,expression:r,args:[o],op:function(e,n){return u?t.resolveAttribute(n,c):l?"computedStyleRef"===i.type?t.resolveComputedStyle(n,c):t.resolveStyle(n,c):t.resolveProperty(n,c)},evaluate:function(e){return t.unifiedEval(this,e)}};return"attributeRef"===i.type&&(f.attribute=i),a?(a.root=f,a.args=[f]):r=f,e.parseElement("indirectExpression",n,r)}}),E.addIndirectExpression("possessive",function(e,t,n,r){if(!e.possessivesDisabled){var o=n.matchOpToken("'");if(o||"symbol"===r.type&&("my"===r.name||"its"===r.name||"your"===r.name)&&("IDENTIFIER"===n.currentToken().type||"ATTRIBUTE_REF"===n.currentToken().type||"STYLE_REF"===n.currentToken().type)){o&&n.requireToken("s");var a=e.parseElement("attributeRef",n);if(null==a){var i=e.parseElement("styleRef",n);if(null==i)var u=n.requireTokenType("IDENTIFIER")}return e.parseElement("indirectExpression",n,{type:"possessive",root:r,attribute:a||i,prop:u,args:[r],op:function(e,n){if(a)var r=t.resolveAttribute(n,a.name);else r=i?"computedStyleRef"===i.type?t.resolveComputedStyle(n,i.name):t.resolveStyle(n,i.name):t.resolveProperty(n,u.value);return r},evaluate:function(e){return t.unifiedEval(this,e)}})}}}),E.addIndirectExpression("inExpression",function(e,t,n,r){if(n.matchToken("in")){var o={type:"inExpression",root:r,args:[r,e.requireElement("unaryExpression",n)],op:function(e,n,r){var o=[];if(n.css)t.implicitLoop(r,function(e){for(var t=e.querySelectorAll(n.css),r=0;r<t.length;r++)o.push(t[r])});else if(n instanceof Element){var a=!1;if(t.implicitLoop(r,function(e){e.contains(n)&&(a=!0)}),a)return n}else t.implicitLoop(n,function(e){t.implicitLoop(r,function(t){e===t&&o.push(e)})});return o},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",n,o)}}),E.addIndirectExpression("asExpression",function(e,t,n,r){if(n.matchToken("as")){n.matchToken("a")||n.matchToken("an");var o=e.requireElement("dotOrColonPath",n).evaluate();return e.parseElement("indirectExpression",n,{type:"asExpression",root:r,args:[r],op:function(e,n){return t.convertValue(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}})}}),E.addIndirectExpression("functionCall",function(e,t,n,r){if(n.matchOpToken("(")){var o=[];if(!n.matchOpToken(")")){do{o.push(e.requireElement("expression",n))}while(n.matchOpToken(","));n.requireOpToken(")")}if(r.root)var a={type:"functionCall",root:r,argExressions:o,args:[r.root,o],op:function(e,n,o){t.nullCheck(n,r.root);var a=n[r.prop.value];return t.nullCheck(a,r),a.hyperfunc&&o.push(e),a.apply(n,o)},evaluate:function(e){return t.unifiedEval(this,e)}};else a={type:"functionCall",root:r,argExressions:o,args:[r,o],op:function(e,n,o){return t.nullCheck(n,r),n.hyperfunc&&o.push(e),n.apply(null,o)},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",n,a)}}),E.addIndirectExpression("attributeRefAccess",function(e,t,n,r){var o=e.parseElement("attributeRef",n);if(o)return{type:"attributeRefAccess",root:r,attribute:o,args:[r],op:function(e,n){return t.resolveAttribute(n,o.name)},evaluate:function(e){return T.unifiedEval(this,e)}}}),E.addIndirectExpression("arrayIndex",function(e,t,n,r){if(n.matchOpToken("[")){var o=!1,a=!1,i=null,u=null;n.matchOpToken("..")?(o=!0,i=e.requireElement("expression",n)):(i=e.requireElement("expression",n),n.matchOpToken("..")&&(a=!0,"R_BRACKET"!==n.currentToken().type&&(u=e.parseElement("expression",n)))),n.requireOpToken("]");var l={type:"arrayIndex",root:r,prop:i,firstIndex:i,secondIndex:u,args:[r,i,u],op:function(e,t,n,r){return null==t?null:o?(n<0&&(n=t.length+n),t.slice(0,n+1)):a?null!=r?(r<0&&(r=t.length+r),t.slice(n,r+1)):t.slice(n):t[n]},evaluate:function(e){return T.unifiedEval(this,e)}};return E.parseElement("indirectExpression",n,l)}});var w=["em","ex","cap","ch","ic","rem","lh","rlh","vw","vh","vi","vb","vmin","vmax","cm","mm","Q","pc","pt","px"];E.addGrammarElement("postfixExpression",function(e,t,n){var r=e.parseElement("primaryExpression",n),o=n.matchAnyToken.apply(n,w)||n.matchOpToken("%");if(o)return{type:"stringPostfix",postfix:o.value,args:[r],op:function(e,t){return""+t+o.value},evaluate:function(e){return t.unifiedEval(this,e)}};var a=null;if(n.matchToken("s")||n.matchToken("seconds")?a=1e3:(n.matchToken("ms")||n.matchToken("milliseconds"))&&(a=1),a)return{type:"timeExpression",time:r,factor:a,args:[r],op:function(e,t){return t*a},evaluate:function(e){return t.unifiedEval(this,e)}};if(n.matchOpToken(":")){var i=n.requireTokenType("IDENTIFIER"),u=!n.matchOpToken("!");return{type:"typeCheck",typeName:i,nullOk:u,args:[r],op:function(e,n){if(t.typeCheck(n,i.value,u))return n;throw new Error("Typecheck failed!  Expected: "+i.value)},evaluate:function(e){return t.unifiedEval(this,e)}}}return r}),E.addGrammarElement("logicalNot",function(e,t,n){if(n.matchToken("not")){var r=e.requireElement("unaryExpression",n);return{type:"logicalNot",root:r,args:[r],op:function(e,t){return!t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("noExpression",function(e,t,n){if(n.matchToken("no")){var r=e.requireElement("unaryExpression",n);return{type:"noExpression",root:r,args:[r],op:function(e,n){return t.isEmpty(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addLeafExpression("some",function(e,t,n){if(n.matchToken("some")){var r=e.requireElement("expression",n);return{type:"noExpression",root:r,args:[r],op:function(e,n){return!t.isEmpty(n)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("negativeNumber",function(e,t,n){if(n.matchOpToken("-")){var r=e.requireElement("unaryExpression",n);return{type:"negativeNumber",root:r,args:[r],op:function(e,t){return-1*t},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("unaryExpression",function(e,t,n){return n.matchToken("the"),e.parseAnyOf(["beepExpression","logicalNot","relativePositionalExpression","positionalExpression","noExpression","negativeNumber","postfixExpression"],n)}),E.addGrammarElement("beepExpression",function(e,t,n){if(n.matchToken("beep!")){var r=e.parseElement("unaryExpression",n);if(r){r.booped=!0;var o=r.evaluate;return r.evaluate=function(e){var n=o.apply(r,arguments),a=e.me;if(t.triggerEvent(a,"hyperscript:beep",{element:a,expression:r,value:n})){var i,u=n;"String"===(i=n?n instanceof v?"ElementCollection":n.constructor?n.constructor.name:"unknown":"object (null)")?u='"'+u+'"':n instanceof v&&(u=Array.from(n)),console.log("///_ BEEP! The expression ("+r.sourceFor().substr(6)+") evaluates to:",u,"of type "+i)}return n},r}}});var S=function(e,t,n,r){var o=[];T.forEach(t,function(t){(t.matches(n)||t===e)&&o.push(t)});for(var a=0;a<o.length-1;a++)if(o[a]===e)return o[a+1];if(r){var i=o[0];if(i&&i.matches(n))return i}};E.addGrammarElement("relativePositionalExpression",function(e,t,n){var r=n.matchAnyToken("next","previous");if(r){if("next"===r.value)var o=!0;var a=e.parseElement("expression",n);if(n.matchToken("from")){n.pushFollow("in");try{var i=e.requireElement("unaryExpression",n)}finally{n.popFollow()}}else i=e.requireElement("implicitMeTarget",n);var u,l=!1;if(n.matchToken("in")){l=!0;var s=e.requireElement("unaryExpression",n)}else u=n.matchToken("within")?e.requireElement("unaryExpression",n):document.body;var c=!1;return n.matchToken("with")&&(n.requireToken("wrapping"),c=!0),{type:"relativePositionalExpression",from:i,forwardSearch:o,inSearch:l,wrapping:c,inElt:s,withinElt:u,operator:r.value,args:[a,i,s,u],op:function(e,t,n,r,a){var i,u,s=t.css;if(null==s)throw"Expected a CSS value";if(l){if(r)return o?S(n,r,s,c):(i=s,u=c,S(n,Array.from(r).reverse(),i,u))}else if(a)return o?function(e,t,n,r){for(var o=t.querySelectorAll(n),a=0;a<o.length;a++){var i=o[a];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return i}if(r)return o[0]}(n,a,s,c):function(e,t,n,r){for(var o=t.querySelectorAll(n),a=o.length-1;a>=0;a--){var i=o[a];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return i}if(r)return o[o.length-1]}(n,a,s,c)},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("positionalExpression",function(e,t,n){var r=n.matchAnyToken("first","last","random");if(r){n.matchAnyToken("in","from","of");var o=e.requireElement("unaryExpression",n),a=r.value;return{type:"positionalExpression",rhs:o,operator:r.value,args:[o],op:function(e,t){if(t&&!Array.isArray(t)&&(t=t.children?t.children:Array.from(t)),t){if("first"===a)return t[0];if("last"===a)return t[t.length-1];if("random"===a)return t[Math.floor(Math.random()*t.length)]}},evaluate:function(e){return t.unifiedEval(this,e)}}}}),E.addGrammarElement("mathOperator",function(e,t,n){var r,o=e.parseElement("unaryExpression",n),a=null;for(r=n.matchAnyOpToken("+","-","*","/","%");r;){var i=r.value;(a=a||r).value!==i&&e.raiseParseError(n,"You must parenthesize math operations with different operators");var u=e.parseElement("unaryExpression",n);o={type:"mathOperator",lhs:o,rhs:u,operator:i,args:[o,u],op:function(e,t,n){return"+"===i?t+n:"-"===i?t-n:"*"===i?t*n:"/"===i?t/n:"%"===i?t%n:void 0},evaluate:function(e){return t.unifiedEval(this,e)}},r=n.matchAnyOpToken("+","-","*","/","%")}return o}),E.addGrammarElement("mathExpression",function(e,t,n){return e.parseAnyOf(["mathOperator","unaryExpression"],n)}),E.addGrammarElement("comparisonOperator",function(e,t,n){var r=e.parseElement("mathExpression",n),o=n.matchAnyOpToken("<",">","<=",">=","==","===","!=","!=="),a=o?o.value:null,i=!0,u=!1;if(null==a&&(n.matchToken("is")||n.matchToken("am")?n.matchToken("not")?n.matchToken("in")?a="not in":n.matchToken("a")?(a="not a",u=!0):n.matchToken("empty")?(a="not empty",i=!1):a="!=":n.matchToken("in")?a="in":n.matchToken("a")?(a="a",u=!0):n.matchToken("empty")?(a="empty",i=!1):n.matchToken("less")?(n.requireToken("than"),n.matchToken("or")?(n.requireToken("equal"),n.requireToken("to"),a="<="):a="<"):n.matchToken("greater")?(n.requireToken("than"),n.matchToken("or")?(n.requireToken("equal"),n.requireToken("to"),a=">="):a=">"):a="==":n.matchToken("exist")||n.matchToken("exists")?(a="exist",i=!1):n.matchToken("matches")||n.matchToken("match")?a="match":n.matchToken("contains")||n.matchToken("contain")?a="contain":n.matchToken("includes")||n.matchToken("include")?a="include":(n.matchToken("do")||n.matchToken("does"))&&(n.requireToken("not"),n.matchToken("matches")||n.matchToken("match")?a="not match":n.matchToken("contains")||n.matchToken("contain")?a="not contain":n.matchToken("exist")||n.matchToken("exist")?(a="not exist",i=!1):n.matchToken("include")?a="not include":e.raiseParseError(n,"Expected matches or contains"))),a){if(u)var l=n.requireTokenType("IDENTIFIER"),s=!n.matchOpToken("!");else if(i){var c=e.requireElement("mathExpression",n);"match"!==a&&"not match"!==a||(c=c.css?c.css:c)}var f=r;r={type:"comparisonOperator",operator:a,typeName:l,nullOk:s,lhs:r,rhs:c,args:[r,c],op:function(e,n,r){if("=="===a)return n==r;if("!="===a)return n!=r;if("match"===a)return null!=n&&g(f,n,r);if("not match"===a)return null==n||!g(f,n,r);if("in"===a)return null!=r&&k(c,r,n);if("not in"===a)return null==r||!k(c,r,n);if("contain"===a)return null!=n&&k(f,n,r);if("not contain"===a)return null==n||!k(f,n,r);if("include"===a)return null!=n&&k(f,n,r);if("not include"===a)return null==n||!k(f,n,r);if("==="===a)return n===r;if("!=="===a)return n!==r;if("<"===a)return n<r;if(">"===a)return n>r;if("<="===a)return n<=r;if(">="===a)return n>=r;if("empty"===a)return t.isEmpty(n);if("not empty"===a)return!t.isEmpty(n);if("exist"===a)return t.doesExist(n);if("not exist"===a)return!t.doesExist(n);if("a"===a)return t.typeCheck(n,l.value,s);if("not a"===a)return!t.typeCheck(n,l.value,s);throw"Unknown comparison : "+a},evaluate:function(e){return t.unifiedEval(this,e)}}}return r}),E.addGrammarElement("comparisonExpression",function(e,t,n){return e.parseAnyOf(["comparisonOperator","mathExpression"],n)}),E.addGrammarElement("logicalOperator",function(e,t,n){var r,o=e.parseElement("comparisonExpression",n),a=null;r=n.matchToken("and")||n.matchToken("or");for(var i=function(){(a=a||r).value!==r.value&&e.raiseParseError(n,"You must parenthesize logical operations with different operators"),u=e.requireElement("comparisonExpression",n);var i=r.value;o={type:"logicalOperator",operator:i,lhs:o,rhs:u,args:[o,u],op:function(e,t,n){return"and"===i?t&&n:t||n},evaluate:function(e){return t.unifiedEval(this,e)}},r=n.matchToken("and")||n.matchToken("or")};r;){var u;i()}return o}),E.addGrammarElement("logicalExpression",function(e,t,n){return e.parseAnyOf(["logicalOperator","mathExpression"],n)}),E.addGrammarElement("asyncExpression",function(e,t,n){return n.matchToken("async")?{type:"asyncExpression",value:e.requireElement("logicalExpression",n),evaluate:function(e){return{asyncWrapper:!0,value:this.value.evaluate(e)}}}:e.parseElement("logicalExpression",n)}),E.addGrammarElement("expression",function(e,t,n){return n.matchToken("the"),e.parseElement("asyncExpression",n)}),E.addGrammarElement("assignableExpression",function(e,t,n){n.matchToken("the");var r=e.parseElement("primaryExpression",n);return!r||"symbol"!==r.type&&"ofExpression"!==r.type&&"propertyAccess"!==r.type&&"attributeRefAccess"!==r.type&&"attributeRef"!==r.type&&"styleRef"!==r.type&&"arrayIndex"!==r.type&&"possessive"!==r.type?(E.raiseParseError(n,"A target expression must be writable.  The expression type '"+(r&&r.type)+"' is not."),r):r}),E.addGrammarElement("hyperscript",function(e,t,n){var r=[];if(n.hasMore())for(;e.featureStart(n.currentToken())||"("===n.currentToken().value;){var o=e.requireElement("feature",n);r.push(o),n.matchToken("end")}return{type:"hyperscript",features:r,apply:function(e,t,n){for(var o,a=s(r);!(o=a()).done;)o.value.install(e,t,n)}}});var N=function(e){var t=[];if("("===e.token(0).value&&(")"===e.token(1).value||","===e.token(2).value||")"===e.token(2).value)){e.matchOpToken("(");do{t.push(e.requireTokenType("IDENTIFIER"))}while(e.matchOpToken(","));e.requireOpToken(")")}return t};E.addFeature("on",function(e,t,n){if(n.matchToken("on")){var r=!1;n.matchToken("every")&&(r=!0);var o=[],a=null;do{var i=e.requireElement("eventName",n,"Expected event name").evaluate();a=a?a+" or "+i:"on "+i;var u=N(n),l=null;if(n.matchOpToken("[")&&(l=e.requireElement("expression",n),n.requireOpToken("]")),"NUMBER"===n.currentToken().type){var f=n.consumeToken(),m=parseInt(f.value);if(n.matchToken("to"))var p=n.consumeToken(),d=parseInt(p.value);else if(n.matchToken("and")){var v=!0;n.requireToken("on")}}if("intersection"===i){var h={};if(n.matchToken("with")&&(h.with=e.requireElement("expression",n).evaluate()),n.matchToken("having"))do{n.matchToken("margin")?h.rootMargin=e.requireElement("stringLike",n).evaluate():n.matchToken("threshold")?h.threshold=e.requireElement("expression",n).evaluate():e.raiseParseError(n,"Unknown intersection config specification")}while(n.matchToken("and"))}else if("mutation"===i){var E={};if(n.matchToken("of"))do{if(n.matchToken("anything"))E.attributes=!0,E.subtree=!0,E.characterData=!0,E.childList=!0;else if(n.matchToken("childList"))E.childList=!0;else if(n.matchToken("attributes"))E.attributes=!0,E.attributeOldValue=!0;else if(n.matchToken("subtree"))E.subtree=!0;else if(n.matchToken("characterData"))E.characterData=!0,E.characterDataOldValue=!0;else if("ATTRIBUTE_REF"===n.currentToken().type){var y=n.consumeToken();null==E.attributeFilter&&(E.attributeFilter=[]),0==y.value.indexOf("@")?E.attributeFilter.push(y.value.substring(1)):e.raiseParseError(n,"Only shorthand attribute references are allowed here")}else e.raiseParseError(n,"Unknown mutation config specification")}while(n.matchToken("or"));else E.attributes=!0,E.characterData=!0,E.childList=!0}var k=null,g=!1;if(n.matchToken("from")&&(n.matchToken("elsewhere")?g=!0:(k=e.parseElement("expression",n))||e.raiseParseError(n,'Expected either target value or "elsewhere".')),null===k&&!1===g&&n.matchToken("elsewhere")&&(g=!0),n.matchToken("in"))var x=e.parseElement("unaryExpression",n);if(n.matchToken("debounced")){n.requireToken("at");var b=e.requireElement("expression",n).evaluate({})}else if(n.matchToken("throttled")){n.requireToken("at");var q=e.requireElement("expression",n).evaluate({})}o.push({execCount:0,every:r,on:i,args:u,filter:l,from:k,inExpr:x,elsewhere:g,startCount:m,endCount:d,unbounded:v,debounceTime:b,throttleTime:q,mutationSpec:E,intersectionSpec:h,debounced:void 0,lastExec:void 0})}while(n.matchToken("or"));var w=!0;if(!r&&n.matchToken("queue"))if(n.matchToken("all"))w=!1;else if(n.matchToken("first"))var S=!0;else if(n.matchToken("none"))var I=!0;else n.requireToken("last");var R=e.requireElement("commandList",n);if(e.ensureTerminated(R),n.matchToken("catch")){var O=n.requireTokenType("IDENTIFIER").value,L=e.requireElement("commandList",n);e.ensureTerminated(L)}if(n.matchToken("finally")){var C=e.requireElement("commandList",n);e.ensureTerminated(C)}var A={displayName:a,events:o,start:R,every:r,execCount:0,errorHandler:L,errorSymbol:O,execute:function(e){var n=t.getEventQueueFor(e.me,A);if(n.executing&&!1===r){if(I||S&&n.queue.length>0)return;return w&&(n.queue.length=0),void n.queue.push(e)}A.execCount++,n.executing=!0,e.meta.onHalt=function(){n.executing=!1;var e=n.queue.shift();e&&setTimeout(function(){A.execute(e)},1)},e.meta.reject=function(n){console.error(n.message?n.message:n);var r=t.getHyperTrace(e,n);r&&r.print(),t.triggerEvent(e.me,"exception",{error:n})},R.execute(e)},install:function(e,n){for(var r,o=function(){var n=r.value;i=n.elsewhere?[document]:n.from?n.from.evaluate(t.makeContext(e,A,e,null)):[e],t.implicitLoop(i,function(r){var o=n.on;if(n.mutationSpec&&(o="hyperscript:mutation",new MutationObserver(function(e,t){A.executing||T.triggerEvent(r,o,{mutationList:e,observer:t})}).observe(r,n.mutationSpec)),n.intersectionSpec){o="hyperscript:insersection";var a=new IntersectionObserver(function(e){for(var t,n=s(e);!(t=n()).done;){var i=t.value,u={observer:a};(u=c(u,i)).intersecting=i.isIntersecting,T.triggerEvent(r,o,u)}},n.intersectionSpec);a.observe(r)}(r.addEventListener||r.on).call(r,o,function a(i){if("undefined"!=typeof Node&&e instanceof Node&&r!==e&&!e.isConnected)r.removeEventListener(o,a);else{var u=t.makeContext(e,A,e,i);if(!n.elsewhere||!e.contains(i.target)){n.from&&(u.result=r);for(var l,c=s(n.args);!(l=c()).done;){var f=l.value,m=u.event[f.value];void 0!==m?u[f.value]=m:"detail"in u.event&&(u[f.value]=u.event.detail[f.value])}if(u.meta.errorHandler=L,u.meta.errorSymbol=O,u.meta.finallyHandler=C,n.filter){var p=u.meta.context;u.meta.context=u.event;try{if(!n.filter.evaluate(u))return}finally{u.meta.context=p}}if(n.inExpr)for(var d=i.target;;){if(d.matches&&d.matches(n.inExpr.css)){u.result=d;break}if(null==(d=d.parentElement))return}if(n.execCount++,n.startCount)if(n.endCount){if(n.execCount<n.startCount||n.execCount>n.endCount)return}else if(n.unbounded){if(n.execCount<n.startCount)return}else if(n.execCount!==n.startCount)return;if(n.debounceTime)return n.debounced&&clearTimeout(n.debounced),void(n.debounced=setTimeout(function(){A.execute(u)},n.debounceTime));if(n.throttleTime){if(n.lastExec&&Date.now()<n.lastExec+n.throttleTime)return;n.lastExec=Date.now()}A.execute(u)}}})})},a=s(A.events);!(r=a()).done;){var i;o()}}};return e.setParent(R,A),A}}),E.addFeature("def",function(e,t,n){if(n.matchToken("def")){var r=e.requireElement("dotOrColonPath",n).evaluate(),o=r.split("."),a=o.pop(),i=[];if(n.matchOpToken("("))if(n.matchOpToken(")"));else{do{i.push(n.requireTokenType("IDENTIFIER"))}while(n.matchOpToken(","));n.requireOpToken(")")}var u=e.requireElement("commandList",n);if(n.matchToken("catch"))var l=n.requireTokenType("IDENTIFIER").value,s=e.parseElement("commandList",n);if(n.matchToken("finally")){var c=e.requireElement("commandList",n);e.ensureTerminated(c)}var f={displayName:a+"("+i.map(function(e){return e.value}).join(", ")+")",name:a,args:i,start:u,errorHandler:s,errorSymbol:l,finallyHandler:c,install:function(e,n){var m=function(){var r=t.makeContext(n,f,e,null);r.meta.errorHandler=s,r.meta.errorSymbol=l,r.meta.finallyHandler=c;for(var o=0;o<i.length;o++){var a=i[o],m=arguments[o];a&&(r[a.value]=m)}r.meta.caller=arguments[i.length],r.meta.caller&&(r.meta.callingCommand=r.meta.caller.meta.command);var p,d=null,v=new Promise(function(e,t){p=e,d=t});return u.execute(r),r.meta.returned?r.meta.returnValue:(r.meta.resolve=p,r.meta.reject=d,v)};m.hyperfunc=!0,m.hypername=r,t.assignToNamespace(e,o,a,m)}};return e.ensureTerminated(u),s&&e.ensureTerminated(s),e.setParent(u,f),f}}),E.addFeature("set",function(e,t,n){var r=e.parseElement("setCommand",n);if(r){"element"!==r.target.scope&&e.raiseParseError(n,"variables declared at the feature level must be element scoped.");var o={start:r,install:function(e,n){r&&r.execute(t.makeContext(e,o,e,null))}};return e.ensureTerminated(r),o}}),E.addFeature("init",function(e,t,n){if(n.matchToken("init")){var r=e.requireElement("commandList",n),o={start:r,install:function(e,n){setTimeout(function(){r&&r.execute(t.makeContext(e,o,e,null))},0)}};return e.ensureTerminated(r),e.setParent(r,o),o}}),E.addFeature("worker",function(e,t,n){n.matchToken("worker")&&e.raiseParseError(n,"In order to use the 'worker' feature, include the _hyperscript worker plugin. See https://hyperscript.org/features/worker/ for more info.")}),E.addFeature("behavior",function(e,t,n){if(n.matchToken("behavior")){var r=e.requireElement("dotOrColonPath",n).evaluate(),o=r.split("."),a=o.pop(),i=[];if(n.matchOpToken("(")&&!n.matchOpToken(")")){do{i.push(n.requireTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));n.requireOpToken(")")}for(var u=e.requireElement("hyperscript",n),l=0;l<u.features.length;l++)u.features[l].behavior=r;return{install:function(e,n){t.assignToNamespace(d.document&&d.document.body,o,a,function(e,n,o){for(var a=f(t.getInternalData(e),r+"Scope"),l=0;l<i.length;l++)a[i[l]]=o[i[l]];u.apply(e,n)})}}}}),E.addFeature("install",function(e,t,n){if(n.matchToken("install")){var r,o=e.requireElement("dotOrColonPath",n).evaluate(),a=o.split("."),i=e.parseElement("namedArgumentList",n);return r={install:function(e,n){t.unifiedEval({args:[i],op:function(t,r){for(var i=d,u=0;u<a.length;u++)if("object"!=typeof(i=i[a[u]])&&"function"!=typeof i)throw new Error("No such behavior defined as "+o);if(!(i instanceof Function))throw new Error(o+" is not a behavior");i(e,n,r)}},t.makeContext(e,r,e))}}}}),E.addGrammarElement("jsBody",function(e,t,n){for(var r=n.currentToken().start,o=n.currentToken(),a=[],i="",u=!1;n.hasMore();){o=n.consumeToken();var l=n.token(0,!0);if("IDENTIFIER"===l.type&&"end"===l.value)break;u?"IDENTIFIER"===o.type||"NUMBER"===o.type?i+=o.value:(""!==i&&a.push(i),i="",u=!1):"IDENTIFIER"===o.type&&"function"===o.value&&(u=!0)}return{type:"jsBody",exposedFunctionNames:a,jsSource:n.source.substring(r,o.end+1)}}),E.addFeature("js",function(e,t,n){if(n.matchToken("js")){var r=e.requireElement("jsBody",n),o=r.jsSource+"\nreturn { "+r.exposedFunctionNames.map(function(e){return e+":"+e}).join(",")+" } ",a=new Function(o);return{jsSource:o,function:a,exposedFunctionNames:r.exposedFunctionNames,install:function(){c(d,a())}}}}),E.addCommand("js",function(e,t,n){if(n.matchToken("js")){var r=[];if(n.matchOpToken("("))if(n.matchOpToken(")"));else{do{var o=n.requireTokenType("IDENTIFIER");r.push(o.value)}while(n.matchOpToken(","));n.requireOpToken(")")}var a=e.requireElement("jsBody",n);n.matchToken("end");var i=m(Function,r.concat([a.jsSource]));return{jsSource:a.jsSource,function:i,inputs:r,op:function(e){var n=[];r.forEach(function(r){n.push(t.resolveSymbol(r,e,"default"))});var o=i.apply(d,n);return o&&"function"==typeof o.then?new Promise(function(n){o.then(function(r){e.result=r,n(t.findNext(this,e))})}):(e.result=o,t.findNext(this,e))}}}}),E.addCommand("async",function(e,t,n){if(n.matchToken("async")){if(n.matchToken("do")){for(var r=o=e.requireElement("commandList",n);r.next;)r=r.next;r.next=t.HALT,n.requireToken("end")}else var o=e.requireElement("command",n);var a={body:o,op:function(e){return setTimeout(function(){o.execute(e)}),t.findNext(this,e)}};return e.setParent(o,a),a}}),E.addCommand("tell",function(e,t,n){var r=n.currentToken();if(n.matchToken("tell")){var o=e.requireElement("expression",n),a=e.requireElement("commandList",n);n.hasMore()&&!e.featureStart(n.currentToken())&&n.requireToken("end");var i="tell_"+r.start,u={value:o,body:a,args:[o],resolveNext:function(e){var n=e.meta.iterators[i];return n.index<n.value.length?(e.beingTold=n.value[n.index++],a):(e.beingTold=n.originalBeingTold,this.next?this.next:t.findNext(this.parent,e))},op:function(e,t){return null==t?t=[]:Array.isArray(t)||t instanceof NodeList||(t=[t]),e.meta.iterators[i]={originalBeingTold:e.beingTold,index:0,value:t},this.resolveNext(e)}};return e.setParent(a,u),u}}),E.addCommand("wait",function(e,t,n){if(n.matchToken("wait")){var r,o;if(n.matchToken("for")){n.matchToken("a");var a=[];do{var i=n.token(0);a.push("NUMBER"===i.type||"L_PAREN"===i.type?{time:e.requireElement("expression",n).evaluate()}:{name:E.requireElement("dotOrColonPath",n,"Expected event name").evaluate(),args:N(n)})}while(n.matchToken("or"));if(n.matchToken("from"))var u=e.requireElement("expression",n);return r={event:a,on:u,args:[u],op:function(e,n){var r=this,o=n||e.me;if(!(o instanceof EventTarget))throw new Error("Not a valid event target: "+this.on.sourceFor());return new Promise(function(n){for(var i,u=!1,l=function(){var a=i.value;f=function(o){if(e.result=o,a.args)for(var i,l=s(a.args);!(i=l()).done;){var c=i.value;e[c.value]=o[c.value]||(o.detail?o.detail[c.value]:null)}u||(u=!0,n(t.findNext(r,e)))},a.name?o.addEventListener(a.name,f,{once:!0}):null!=a.time&&setTimeout(f,a.time,a.time)},c=s(a);!(i=c()).done;){var f;l()}})}},r}return n.matchToken("a")?(n.requireToken("tick"),o=0):o=E.requireElement("expression",n),{type:"waitCmd",time:o,args:[o],op:function(e,n){var r=this;return new Promise(function(o){setTimeout(function(){o(t.findNext(r,e))},n)})},execute:function(e){return t.unifiedExec(this,e)}}}}),E.addGrammarElement("dotOrColonPath",function(e,t,n){var r=n.matchTokenType("IDENTIFIER");if(r){var o=[r.value],a=n.matchOpToken(".")||n.matchOpToken(":");if(a)do{o.push(n.requireTokenType("IDENTIFIER","NUMBER").value)}while(n.matchOpToken(a.value));return{type:"dotOrColonPath",path:o,evaluate:function(){return o.join(a?a.value:"")}}}}),E.addGrammarElement("eventName",function(e,t,n){var r;return(r=n.matchTokenType("STRING"))?{evaluate:function(){return r.value}}:e.parseElement("dotOrColonPath",n)}),E.addCommand("trigger",function(e,t,n){if(n.matchToken("trigger"))return x("trigger",e,t,n)}),E.addCommand("send",function(e,t,n){if(n.matchToken("send"))return x("send",e,t,n)});var I=function(e,t,n,r){if(r)if(e.commandBoundary(n.currentToken()))e.raiseParseError(n,"'return' commands must return a value.  If you do not wish to return a value, use 'exit' instead.");else var o=e.requireElement("expression",n);var a={value:o,args:[o],op:function(e,n){var r=e.meta.resolve;return e.meta.returned=!0,e.meta.returnValue=n,r&&(n?r(n):r()),t.HALT}};return a};E.addCommand("return",function(e,t,n){if(n.matchToken("return"))return I(e,t,n,!0)}),E.addCommand("exit",function(e,t,n){if(n.matchToken("exit"))return I(e,t,n,!1)}),E.addCommand("halt",function(e,t,n){if(n.matchToken("halt")){if(n.matchToken("the")){n.requireToken("event"),n.matchOpToken("'")&&n.requireToken("s");var r=!0}if(n.matchToken("bubbling"))var o=!0;else if(n.matchToken("default"))var a=!0;var i=I(e,t,n,!1);return{keepExecuting:!0,bubbling:o,haltDefault:a,exit:i,op:function(e){if(e.event)return o?e.event.stopPropagation():(a||e.event.stopPropagation(),e.event.preventDefault()),r?t.findNext(this,e):i}}}}),E.addCommand("log",function(e,t,n){if(n.matchToken("log")){for(var r=[e.parseElement("expression",n)];n.matchOpToken(",");)r.push(e.requireElement("expression",n));if(n.matchToken("with"))var o=e.requireElement("expression",n);var a={exprs:r,withExpr:o,args:[o,r],op:function(e,n,r){return n?n.apply(null,r):console.log.apply(null,r),t.findNext(this,e)}};return a}}),E.addCommand("throw",function(e,t,n){if(n.matchToken("throw")){var r=e.requireElement("expression",n),o={expr:r,args:[r],op:function(e,n){throw t.registerHyperTrace(e,n),n}};return o}});var R=function(e,t,n){var r=e.requireElement("expression",n),o={expr:r,args:[r],op:function(e,n){return e.result=n,t.findNext(o,e)}};return o};E.addCommand("call",function(e,t,n){if(n.matchToken("call")){var r=R(e,t,n);return r.expr&&"functionCall"!==r.expr.type&&e.raiseParseError(n,"Must be a function invocation"),r}}),E.addCommand("get",function(e,t,n){if(n.matchToken("get"))return R(e,t,n)}),E.addCommand("make",function(e,t,n){if(n.matchToken("make")){n.matchToken("a")||n.matchToken("an");var r,o=e.requireElement("expression",n),a=[];if("queryRef"!==o.type&&n.matchToken("from"))do{a.push(e.requireElement("expression",n))}while(n.matchOpToken(","));if(n.matchToken("called"))var i=e.requireElement("symbol",n);return"queryRef"===o.type?r={op:function(e){for(var n,r,a="div",u=[],l=/(?:(^|#|\.)([^#\. ]+))/g;n=l.exec(o.css);)""===n[1]?a=n[2].trim():"#"===n[1]?r=n[2].trim():u.push(n[2].trim());var s=document.createElement(a);void 0!==r&&(s.id=r);for(var c=0;c<u.length;c++)s.classList.add(u[c]);return e.result=s,i&&t.setSymbol(i.name,e,i.scope,s),t.findNext(this,e)}}:(r={args:[o,a],op:function(e,n,r){return e.result=m(n,r),i&&t.setSymbol(i.name,e,i.scope,e.result),t.findNext(this,e)}},r)}}),E.addGrammarElement("pseudoCommand",function(e,t,n){var r=n.token(1);if(!r||!r.op||"."!==r.value&&"("!==r.value)return null;for(var o=e.requireElement("primaryExpression",n),a=o.root,i=o;null!=a.root;)i=i.root,a=a.root;if("functionCall"!==o.type&&e.raiseParseError(n,"Pseudo-commands must be function calls"),"functionCall"===i.type&&null==i.root.root)if(n.matchAnyToken("the","to","on","with","into","from","at"))var u=e.requireElement("expression",n);else n.matchToken("me")&&(u=e.requireElement("implicitMeTarget",n));if(u)var l={type:"pseudoCommand",root:u,argExressions:i.argExressions,args:[u,i.argExressions],op:function(e,n,r){t.nullCheck(n,u);var o=n[i.root.name];return t.nullCheck(o,i),o.hyperfunc&&r.push(e),e.result=o.apply(n,r),t.findNext(l,e)},execute:function(e){return t.unifiedExec(this,e)}};else l={type:"pseudoCommand",expr:o,args:[o],op:function(e,n){return e.result=n,t.findNext(l,e)},execute:function(e){return t.unifiedExec(this,e)}};return l});var O=function(e,t,n,r,o){var a="symbol"===r.type,i="attributeRef"===r.type,u="styleRef"===r.type,l="arrayIndex"===r.type;i||u||a||null!=r.root||e.raiseParseError(n,"Can only put directly into symbols, not references");var s=null,c=null;if(a);else if(i||u){s=e.requireElement("implicitMeTarget",n);var f=r}else l?(c=r.firstIndex,s=r.root):(c=r.prop?r.prop.value:null,f=r.attribute,s=r.root);var m={target:r,symbolWrite:a,value:o,args:[s,c,o],op:function(e,n,o,i){return a?t.setSymbol(r.name,e,r.scope,i):(t.nullCheck(n,s),l?n[o]=i:t.implicitLoop(n,function(e){f?"attributeRef"===f.type?null==i?e.removeAttribute(f.name):e.setAttribute(f.name,i):e.style[f.name]=i:e[o]=i})),t.findNext(this,e)}};return m};E.addCommand("default",function(e,t,n){if(n.matchToken("default")){var r=e.requireElement("assignableExpression",n);n.requireToken("to");var o=e.requireElement("expression",n),a=O(e,t,n,r,o),i={target:r,value:o,setter:a,args:[r],op:function(e,n){return n?t.findNext(this,e):a}};return a.parent=i,i}}),E.addCommand("set",function(e,t,n){if(n.matchToken("set")){if("L_BRACE"===n.currentToken().type){var r=e.requireElement("objectLiteral",n);n.requireToken("on");var o={objectLiteral:r,target:a=e.requireElement("expression",n),args:[r,a],op:function(e,n,r){return c(r,n),t.findNext(this,e)}};return o}try{n.pushFollow("to");var a=e.requireElement("assignableExpression",n)}finally{n.popFollow()}n.requireToken("to");var i=e.requireElement("expression",n);return O(e,t,n,a,i)}}),E.addCommand("if",function(e,t,n){if(n.matchToken("if")){var r=e.requireElement("expression",n);n.matchToken("then");var o=e.parseElement("commandList",n);if(n.matchToken("else")||n.matchToken("otherwise"))var a=e.parseElement("commandList",n);n.hasMore()&&n.requireToken("end");var i={expr:r,trueBranch:o,falseBranch:a,args:[r],op:function(e,n){return n?o:a||t.findNext(this,e)}};return e.setParent(o,i),e.setParent(a,i),i}});var L=function(e,t,n,r){var o,a=t.currentToken();if(t.matchToken("for")||r){var i=t.requireTokenType("IDENTIFIER");o=i.value,t.requireToken("in");var u=e.requireElement("expression",t)}else if(t.matchToken("in"))o="it",u=e.requireElement("expression",t);else if(t.matchToken("while"))var l=e.requireElement("expression",t);else if(t.matchToken("until")){var s=!0;if(t.matchToken("event")){var c=E.requireElement("dotOrColonPath",t,"Expected event name");if(t.matchToken("from"))var f=e.requireElement("expression",t)}else l=e.requireElement("expression",t)}else if(e.commandBoundary(t.currentToken())||"forever"===t.currentToken().value){t.matchToken("forever");var m=!0}else{var p=e.requireElement("expression",t);t.requireToken("times")}if(t.matchToken("index"))var d=(i=t.requireTokenType("IDENTIFIER")).value;var v=e.parseElement("commandList",t);if(v&&c){for(var h=v;h.next;)h=h.next;var y={type:"waitATick",op:function(){return new Promise(function(e){setTimeout(function(){e(n.findNext(y))},0)})}};h.next=y}if(t.hasMore()&&t.requireToken("end"),null==o)var T=o="_implicit_repeat_"+a.start;else T=o+"_"+a.start;var k={identifier:o,indexIdentifier:d,slot:T,expression:u,forever:m,times:p,until:s,event:c,on:f,whileExpr:l,resolveNext:function(){return this},loop:v,args:[l,p],op:function(e,t,r){var a=e.meta.iterators[T],i=!1,u=null;if(this.forever)i=!0;else if(this.until)i=c?!1===e.meta.iterators[T].eventFired:!0!==t;else if(l)i=t;else if(r)i=a.index<r;else{var s=a.iterator.next();i=!s.done,u=s.value}return i?(e.result=a.value?e[o]=u:a.index,d&&(e[d]=a.index),a.index++,v):(e.meta.iterators[T]=null,n.findNext(this.parent,e))}};e.setParent(v,k);var g={name:"repeatInit",args:[u,c,f],op:function(e,t,n,r){var o={index:0,value:t,eventFired:!1};return e.meta.iterators[T]=o,t&&t[Symbol.iterator]&&(o.iterator=t[Symbol.iterator]()),c&&(r||e.me).addEventListener(n,function(t){e.meta.iterators[T].eventFired=!0},{once:!0}),k},execute:function(e){return n.unifiedExec(this,e)}};return e.setParent(k,g),g};if(E.addCommand("repeat",function(e,t,n){if(n.matchToken("repeat"))return L(e,n,t,!1)}),E.addCommand("for",function(e,t,n){if(n.matchToken("for"))return L(e,n,t,!0)}),E.addCommand("continue",function(e,t,n){if(n.matchToken("continue"))return{op:function(t){for(var r=this.parent;;r=r.parent)if(null==r&&e.raiseParseError(n,"Command `continue` cannot be used outside of a `repeat` loop."),null!=r.loop)return r.resolveNext(t)}}}),E.addCommand("break",function(e,t,n){if(n.matchToken("break"))return{op:function(r){for(var o=this.parent;;o=o.parent)if(null==o&&e.raiseParseError(n,"Command `continue` cannot be used outside of a `repeat` loop."),null!=o.loop)return t.findNext(o.parent,r)}}}),E.addGrammarElement("stringLike",function(e,t,n){return E.parseAnyOf(["string","nakedString"],n)}),E.addCommand("append",function(e,t,n){if(n.matchToken("append")){var r,o=e.requireElement("expression",n),a={type:"symbol",evaluate:function(e){return t.resolveSymbol("result",e)}};r=n.matchToken("to")?e.requireElement("expression",n):a;var i=null;"symbol"!==r.type&&"attributeRef"!==r.type&&null==r.root||(i=O(e,t,n,r,a));var u={value:o,target:r,args:[r,o],op:function(e,n,r){if(Array.isArray(n))return n.push(r),t.findNext(this,e);if(n instanceof Element)return n.innerHTML+=r,t.findNext(this,e);if(i)return e.result=(n||"")+r,i;throw Error("Unable to append a value!")},execute:function(e){return t.unifiedExec(this,e)}};return null!=i&&(i.parent=u),u}}),E.addCommand("increment",function(e,t,n){if(n.matchToken("increment")){var r,o=e.parseElement("assignableExpression",n);n.matchToken("by")&&(r=e.requireElement("expression",n));var a={type:"implicitIncrementOp",target:o,args:[o,r],op:function(e,t,n){var r=(t=t?parseFloat(t):0)+(n=n?parseFloat(n):1);return e.result=r,r},evaluate:function(e){return t.unifiedEval(this,e)}};return O(e,t,n,o,a)}}),E.addCommand("decrement",function(e,t,n){if(n.matchToken("decrement")){var r,o=e.parseElement("assignableExpression",n);n.matchToken("by")&&(r=e.requireElement("expression",n));var a={type:"implicitDecrementOp",target:o,args:[o,r],op:function(e,t,n){var r=(t=t?parseFloat(t):0)-(n=n?parseFloat(n):1);return e.result=r,r},evaluate:function(e){return t.unifiedEval(this,e)}};return O(e,t,n,o,a)}}),E.addCommand("fetch",function(e,t,n){if(n.matchToken("fetch")){var r=e.requireElement("stringLike",n);if(n.matchToken("as"))var o=b(n,e);if(n.matchToken("with")&&"{"!==n.currentToken().value)var a=e.parseElement("nakedNamedArgumentList",n);else a=e.parseElement("objectLiteral",n);null==o&&n.matchToken("as")&&(o=b(n,e));var i=o?o.type:"text",u=o?o.conversion:null,l={url:r,argExpressions:a,args:[r,a],op:function(e,n,r){var o=r||{};o.sender=e.me,o.headers=o.headers||{};var a=new AbortController,s=e.me.addEventListener("fetch:abort",function(){a.abort()},{once:!0});o.signal=a.signal,t.triggerEvent(e.me,"hyperscript:beforeFetch",o),t.triggerEvent(e.me,"fetch:beforeRequest",o);var c=!1;return(r=o).timeout&&setTimeout(function(){c||a.abort()},r.timeout),fetch(n,r).then(function(n){var r={response:n};return t.triggerEvent(e.me,"fetch:afterResponse",r),n=r.response,"response"===i?(e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)):"json"===i?n.json().then(function(n){return e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)}):n.text().then(function(n){return u&&(n=t.convertValue(n,u)),"html"===i&&(n=t.convertValue(n,"Fragment")),e.result=n,t.triggerEvent(e.me,"fetch:afterRequest",{result:n}),c=!0,t.findNext(l,e)})}).catch(function(n){throw t.triggerEvent(e.me,"fetch:error",{reason:n}),n}).finally(function(){e.me.removeEventListener("fetch:abort",s)})}};return l}}),"document"in d){var C=Array.from(document.querySelectorAll("script[type='text/hyperscript'][src]"));Promise.all(C.map(function(e){return fetch(e.src).then(function(e){return e.text()})})).then(function(e){return e.forEach(T.evaluate)}).then(function(){var e;e=function(){var e,t;(t=(e=document.querySelector('meta[name="htmx-config"]'))?function(e){try{return JSON.parse(e)}catch(e){return t=e,console.error?console.error(t):console.log&&console.log("ERROR: ",t),null}var t}(e.content):null)&&(p.config=c(p.config,t)),T.processNode(document.documentElement),document.addEventListener("htmx:load",function(e){T.processNode(e.detail.elt)})},"loading"!==document.readyState?setTimeout(e):document.addEventListener("DOMContentLoaded",e)})}var A=p=c(function(e,t){return T.evaluate(e,t)},{internals:{lexer:h,parser:E,runtime:T},ElementCollection:v,addFeature:function(e,t){E.addFeature(e,t)},addCommand:function(e,t){E.addCommand(e,t)},addLeafExpression:function(e,t){E.addLeafExpression(e,t)},addIndirectExpression:function(e,t){E.addIndirectExpression(e,t)},evaluate:T.evaluate.bind(T),parse:T.parse.bind(T),processNode:T.processNode.bind(T),config:{attributes:"_, script, data-script",defaultTransition:"all 500ms ease-in",disableSelector:"[disable-scripting], [data-disable-scripting]",conversions:y}});return function(e){e.addCommand("settle",function(e,t,n){if(n.matchToken("settle")){if(e.commandBoundary(n.currentToken()))r=e.requireElement("implicitMeTarget",n);else var r=e.requireElement("expression",n);var o={type:"settleCmd",args:[r],op:function(e,n){t.nullCheck(n,r);var a=null,i=!1,u=new Promise(function(e){a=e});return n.addEventListener("transitionstart",function(){i=!0},{once:!0}),setTimeout(function(){i||a(t.findNext(o,e))},500),n.addEventListener("transitionend",function(){a(t.findNext(o,e))},{once:!0}),u},execute:function(e){return t.unifiedExec(this,e)}};return o}}),e.addCommand("add",function(e,t,n){if(n.matchToken("add")){var r=e.parseElement("classRef",n),o=null,a=null;if(null==r)null==(o=e.parseElement("attributeRef",n))&&null==(a=e.parseElement("styleLiteral",n))&&e.raiseParseError(n,"Expected either a class reference or attribute expression");else for(var i=[r];r=e.parseElement("classRef",n);)i.push(r);if(n.matchToken("to"))var u=e.requireElement("expression",n);else u=e.requireElement("implicitMeTarget",n);if(n.matchToken("when")){a&&e.raiseParseError(n,"Only class and properties are supported with a when clause");var l=e.requireElement("expression",n)}return i?{classRefs:i,to:u,args:[u,i],op:function(e,n,r){return t.nullCheck(n,u),t.forEach(r,function(r){t.implicitLoop(n,function(n){l?(e.result=n,t.evaluateNoPromise(l,e)?n instanceof Element&&n.classList.add(r.className):n instanceof Element&&n.classList.remove(r.className),e.result=null):n instanceof Element&&n.classList.add(r.className)})}),t.findNext(this,e)}}:o?{type:"addCmd",attributeRef:o,to:u,args:[u],op:function(e,n,r){return t.nullCheck(n,u),t.implicitLoop(n,function(n){l?(e.result=n,t.evaluateNoPromise(l,e)?n.setAttribute(o.name,o.value):n.removeAttribute(o.name),e.result=null):n.setAttribute(o.name,o.value)}),t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}:{type:"addCmd",cssDeclaration:a,to:u,args:[u,a],op:function(e,n,r){return t.nullCheck(n,u),t.implicitLoop(n,function(e){e.style.cssText+=r}),t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}}}),e.internals.parser.addGrammarElement("styleLiteral",function(e,t,n){if(n.matchOpToken("{")){for(var r=[""],o=[];n.hasMore();){if(n.matchOpToken("\\"))n.consumeToken();else{if(n.matchOpToken("}"))break;if(n.matchToken("$")){var a=n.matchOpToken("{"),i=e.parseElement("expression",n);a&&n.requireOpToken("}"),o.push(i),r.push("")}else{var u=n.consumeToken();r[r.length-1]+=n.source.substring(u.start,u.end)}}r[r.length-1]+=n.lastWhitespace()}return{type:"styleLiteral",args:[o],op:function(e,t){var n="";return r.forEach(function(e,r){n+=e,r in t&&(n+=t[r])}),n},evaluate:function(e){return t.unifiedEval(this,e)}}}}),e.addCommand("remove",function(e,t,n){if(n.matchToken("remove")){var r=e.parseElement("classRef",n),o=null,a=null;if(null==r)null==(o=e.parseElement("attributeRef",n))&&null==(a=e.parseElement("expression",n))&&e.raiseParseError(n,"Expected either a class reference, attribute expression or value expression");else for(var i=[r];r=e.parseElement("classRef",n);)i.push(r);if(n.matchToken("from"))var u=e.requireElement("expression",n);else u=e.requireElement("implicitMeTarget",n);return a?{elementExpr:a,from:u,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(e){e.parentElement&&e.parentElement.removeChild(e)}),t.findNext(this,e)}}:{classRefs:i,attributeRef:o,elementExpr:a,from:u,args:[i,u],op:function(e,n,r){return t.nullCheck(r,u),n?t.forEach(n,function(e){t.implicitLoop(r,function(t){t.classList.remove(e.className)})}):t.implicitLoop(r,function(e){e.removeAttribute(o.name)}),t.findNext(this,e)}}}}),e.addCommand("toggle",function(e,t,n){if(n.matchToken("toggle")){if(n.matchAnyToken("the","my"),"STYLE_REF"===n.currentToken().type){var o=n.consumeToken().value.substr(1),a=!0,i=r(e,n,o);if(n.matchToken("of")){n.pushFollow("with");try{var u=e.requireElement("expression",n)}finally{n.popFollow()}}else u=e.requireElement("implicitMeTarget",n)}else if(n.matchToken("between")){var l=!0,s=e.parseElement("classRef",n);n.requireToken("and");var c=e.requireElement("classRef",n)}else{s=e.parseElement("classRef",n);var f=null;if(null==s)null==(f=e.parseElement("attributeRef",n))&&e.raiseParseError(n,"Expected either a class reference or attribute expression");else for(var m=[s];s=e.parseElement("classRef",n);)m.push(s)}if(!0!==a&&(u=n.matchToken("on")?e.requireElement("expression",n):e.requireElement("implicitMeTarget",n)),n.matchToken("for"))var p=e.requireElement("expression",n);else if(n.matchToken("until")){var d=e.requireElement("dotOrColonPath",n,"Expected event name");if(n.matchToken("from"))var v=e.requireElement("expression",n)}var h={classRef:s,classRef2:c,classRefs:m,attributeRef:f,on:u,time:p,evt:d,from:v,toggle:function(e,n,r,o){t.nullCheck(e,u),a?t.implicitLoop(e,function(e){i("toggle",e)}):l?t.implicitLoop(e,function(e){e.classList.contains(n.className)?(e.classList.remove(n.className),e.classList.add(r.className)):(e.classList.add(n.className),e.classList.remove(r.className))}):o?t.forEach(o,function(n){t.implicitLoop(e,function(e){e.classList.toggle(n.className)})}):t.forEach(e,function(e){e.hasAttribute(f.name)?e.removeAttribute(f.name):e.setAttribute(f.name,f.value)})},args:[u,p,d,v,s,c,m],op:function(e,n,r,o,a,i,u,l){return r?new Promise(function(o){h.toggle(n,i,u,l),setTimeout(function(){h.toggle(n,i,u,l),o(t.findNext(h,e))},r)}):o?new Promise(function(r){(a||e.me).addEventListener(o,function(){h.toggle(n,i,u,l),r(t.findNext(h,e))},{once:!0}),h.toggle(n,i,u,l)}):(this.toggle(n,i,u,l),t.findNext(h,e))}};return h}});var t={display:function(n,r,o){if(o)r.style.display=o;else if("toggle"===n)"none"===getComputedStyle(r).display?t.display("show",r,o):t.display("hide",r,o);else if("hide"===n){var a=e.internals.runtime.getInternalData(r);null==a.originalDisplay&&(a.originalDisplay=r.style.display),r.style.display="none"}else{var i=e.internals.runtime.getInternalData(r);i.originalDisplay&&"none"!==i.originalDisplay?r.style.display=i.originalDisplay:r.style.removeProperty("display")}},visibility:function(e,n,r){r?n.style.visibility=r:"toggle"===e?"hidden"===getComputedStyle(n).visibility?t.visibility("show",n,r):t.visibility("hide",n,r):n.style.visibility="hide"===e?"hidden":"visible"},opacity:function(e,n,r){r?n.style.opacity=r:"toggle"===e?"0"===getComputedStyle(n).opacity?t.opacity("show",n,r):t.opacity("hide",n,r):n.style.opacity="hide"===e?"0":"1"}},n=function(e,t,n){var r=n.currentToken();return"when"===r.value||"with"===r.value||e.commandBoundary(r)?e.parseElement("implicitMeTarget",n):e.parseElement("expression",n)},r=function(n,r,o){var a=e.config.defaultHideShowStrategy,i=t;e.config.hideShowStrategies&&(i=c(i,e.config.hideShowStrategies));var u=i[o=o||a||"display"];return null==u&&n.raiseParseError(r,"Unknown show/hide strategy : "+o),u};function o(t,n,r,o){if(null!=r)var a=t.resolveSymbol(r,n);else a=n;if(a instanceof Element||a instanceof HTMLDocument){for(;a.firstChild;)a.removeChild(a.firstChild);a.append(e.internals.runtime.convertValue(o,"Fragment"))}else{if(null==r)throw"Don't know how to put a value into "+typeof n;t.setSymbol(r,n,null,o)}}function a(e,t,n){var r;if(n.matchToken("the")||n.matchToken("element")||n.matchToken("elements")||"CLASS_REF"===n.currentToken().type||"ID_REF"===n.currentToken().type||n.currentToken().op&&"<"===n.currentToken().value){e.possessivesDisabled=!0;try{r=e.parseElement("expression",n)}finally{delete e.possessivesDisabled}n.matchOpToken("'")&&n.requireToken("s")}else if("IDENTIFIER"===n.currentToken().type&&"its"===n.currentToken().value){var o=n.matchToken("its");r={type:"pseudopossessiveIts",token:o,name:o.value,evaluate:function(e){return t.resolveSymbol("it",e)}}}else n.matchToken("my")||n.matchToken("me"),r=e.parseElement("implicitMeTarget",n);return r}e.addCommand("hide",function(e,t,o){if(o.matchToken("hide")){var a=n(e,0,o),i=null;o.matchToken("with")&&0===(i=o.requireTokenType("IDENTIFIER","STYLE_REF").value).indexOf("*")&&(i=i.substr(1));var u=r(e,o,i);return{target:a,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(e){u("hide",e)}),t.findNext(this,e)}}}}),e.addCommand("show",function(e,t,o){if(o.matchToken("show")){var a=n(e,0,o),i=null;o.matchToken("with")&&0===(i=o.requireTokenType("IDENTIFIER","STYLE_REF").value).indexOf("*")&&(i=i.substr(1));var u=null;if(o.matchOpToken(":")){var l=o.consumeUntilWhitespace();o.matchTokenType("WHITESPACE"),u=l.map(function(e){return e.value}).join("")}if(o.matchToken("when"))var s=e.requireElement("expression",o);var c=r(e,o,i);return{target:a,when:s,args:[a],op:function(e,n){return t.nullCheck(n,a),t.implicitLoop(n,function(n){s?(e.result=n,t.evaluateNoPromise(s,e)?c("show",n,u):c("hide",n),e.result=null):c("show",n,u)}),t.findNext(this,e)}}}}),e.addCommand("take",function(e,t,n){if(n.matchToken("take")){var r=e.requireElement("classRef",n);if(n.matchToken("from"))var o=e.requireElement("expression",n);else o=r;if(n.matchToken("for"))var a=e.requireElement("expression",n);else a=e.requireElement("implicitMeTarget",n);return{classRef:r,from:o,forElt:a,args:[r,o,a],op:function(e,n,r,i){t.nullCheck(r,o),t.nullCheck(i,a);var u=n.className;return t.implicitLoop(r,function(e){e.classList.remove(u)}),t.implicitLoop(i,function(e){e.classList.add(u)}),t.findNext(this,e)}}}}),e.addCommand("put",function(e,t,n){if(n.matchToken("put")){var r=e.requireElement("expression",n),a=n.matchAnyToken("into","before","after");null==a&&n.matchToken("at")&&(n.matchToken("the"),a=n.matchAnyToken("start","end"),n.requireToken("of")),null==a&&e.raiseParseError(n,"Expected one of 'into', 'before', 'at start of', 'at end of', 'after'");var i=e.requireElement("expression",n),u=a.value,l=!1,s=!1,c=null,f=null;if("arrayIndex"===i.type&&"into"===u)l=!0,f=i.prop,c=i.root;else if(i.prop&&i.root&&"into"===u)f=i.prop.value,c=i.root;else if("symbol"===i.type&&"into"===u)s=!0,f=i.name;else if("attributeRef"===i.type&&"into"===u){var m=!0;f=i.name,c=e.requireElement("implicitMeTarget",n)}else if("styleRef"===i.type&&"into"===u){var p=!0;f=i.name,c=e.requireElement("implicitMeTarget",n)}else i.attribute&&"into"===u?(m="attributeRef"===i.attribute.type,p="styleRef"===i.attribute.type,f=i.attribute.name,c=i.root):c=i;var d={target:i,operation:u,symbolWrite:s,value:r,args:[c,f,r],op:function(e,n,r,a){if(s)o(t,e,r,a);else if(t.nullCheck(n,c),"into"===u)m?t.implicitLoop(n,function(e){e.setAttribute(r,a)}):p?t.implicitLoop(n,function(e){e.style[r]=a}):l?n[r]=a:t.implicitLoop(n,function(e){o(t,e,r,a)});else{var i="before"===u?Element.prototype.before:"after"===u?Element.prototype.after:"start"===u?Element.prototype.prepend:Element.prototype.append;t.implicitLoop(n,function(e){i.call(e,a instanceof Node?a:t.convertValue(a,"Fragment"))})}return t.findNext(this,e)}};return d}}),e.addCommand("transition",function(t,n,r){if(r.matchToken("transition")){for(var o=a(t,n,r),i=[],u=[],l=[],s=r.currentToken();!t.commandBoundary(s)&&"over"!==s.value&&"using"!==s.value;)"STYLE_REF"===r.currentToken().type?function(){var e=r.consumeToken().value.substr(1);i.push({type:"styleRefValue",evaluate:function(){return e}})}():i.push(t.requireElement("stringLike",r)),r.matchToken("from")?u.push(t.requireElement("expression",r)):u.push(null),r.requireToken("to"),r.matchToken("initial")?l.push({type:"initial_literal",evaluate:function(){return"initial"}}):l.push(t.requireElement("expression",r)),s=r.currentToken();if(r.matchToken("over"))var c=t.requireElement("expression",r);else if(r.matchToken("using"))var f=t.requireElement("expression",r);var m={to:l,args:[o,i,u,l,f,c],op:function(t,r,a,i,u,l,s){n.nullCheck(r,o);var c=[];return n.implicitLoop(r,function(t){var r=new Promise(function(r,o){var c=t.style.transition;t.style.transition=s?"all "+s+"ms ease-in":l||e.config.defaultTransition;for(var f=n.getInternalData(t),m=getComputedStyle(t),p={},d=0;d<m.length;d++){var v=m[d];p[v]=m[v]}for(f.initalStyles||(f.initalStyles=p),d=0;d<a.length;d++){var h=a[d],E=i[d];t.style[h]="computed"===E||null==E?p[h]:E}var y=!1,T=!1;t.addEventListener("transitionend",function(){T||(t.style.transition=c,T=!0,r())},{once:!0}),t.addEventListener("transitionstart",function(){y=!0},{once:!0}),setTimeout(function(){T||y||(t.style.transition=c,T=!0,r())},100),setTimeout(function(){for(var e=0;e<a.length;e++){var n=a[e],r=u[e];t.style[n]="initial"===r?f.initalStyles[n]:r}},0)});c.push(r)}),Promise.all(c).then(function(){return n.findNext(m,t)})}};return m}}),e.addCommand("measure",function(e,t,n){if(n.matchToken("measure")){var r=a(e,t,n),o=[];if(!e.commandBoundary(n.currentToken()))do{o.push(n.matchTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));return{properties:o,args:[r],op:function(e,n){t.nullCheck(n,r),0 in n&&(n=n[0]);var a=n.getBoundingClientRect(),i={top:n.scrollTop,left:n.scrollLeft,topMax:n.scrollTopMax,leftMax:n.scrollLeftMax,height:n.scrollHeight,width:n.scrollWidth};return e.result={x:a.x,y:a.y,left:a.left,top:a.top,right:a.right,bottom:a.bottom,width:a.width,height:a.height,bounds:a,scrollLeft:i.left,scrollTop:i.top,scrollLeftMax:i.leftMax,scrollTopMax:i.topMax,scrollWidth:i.width,scrollHeight:i.height,scroll:i},t.forEach(o,function(t){if(!(t in e.result))throw"No such measurement as "+t;e[t]=e.result[t]}),t.findNext(this,e)}}}}),e.addLeafExpression("closestExpr",function(e,t,n){if(n.matchToken("closest")){if(n.matchToken("parent"))var r=!0;var o=null;if("ATTRIBUTE_REF"===n.currentToken().type){var a=e.requireElement("attributeRefAccess",n,null);o="["+a.attribute.name+"]"}if(null==o){var i=e.requireElement("expression",n);null==i.css?e.raiseParseError(n,"Expected a CSS expression"):o=i.css}if(n.matchToken("to"))var u=e.parseElement("expression",n);else u=e.parseElement("implicitMeTarget",n);var l={type:"closestExpr",parentSearch:r,expr:i,css:o,to:u,args:[u],op:function(e,n){if(null==n)return null;var a=[];return t.implicitLoop(n,function(e){a.push(r?e.parentElement?e.parentElement.closest(o):null:e.closest(o))}),t.shouldAutoIterate(n)?a:a[0]},evaluate:function(e){return t.unifiedEval(this,e)}};return a?(a.root=l,a.args=[l],a):l}}),e.addCommand("go",function(e,t,n){if(n.matchToken("go")){if(n.matchToken("back"))var r=!0;else if(n.matchToken("to"),n.matchToken("url")){var o=e.requireElement("stringLike",n),a=!0;if(n.matchToken("in")){n.requireToken("new"),n.requireToken("window");var i=!0}}else{n.matchToken("the");var u=n.matchAnyToken("top","middle","bottom"),l=n.matchAnyToken("left","center","right");(u||l)&&n.requireToken("of"),o=e.requireElement("unaryExpression",n);var s=n.matchAnyOpToken("+","-");if(s){n.pushFollow("px");try{var c=e.requireElement("expression",n)}finally{n.popFollow()}}n.matchToken("px");var f=n.matchAnyToken("smoothly","instantly"),m={};u&&("top"===u.value?m.block="start":"bottom"===u.value?m.block="end":"middle"===u.value&&(m.block="center")),l&&("left"===l.value?m.inline="start":"center"===l.value?m.inline="center":"right"===l.value&&(m.inline="end")),f&&("smoothly"===f.value?m.behavior="smooth":"instantly"===f.value&&(m.behavior="instant"))}var p={target:o,args:[o,c],op:function(e,n,o){return r?window.history.back():a?n&&(i?window.open(n):window.location.href=n):t.implicitLoop(n,function(e){if(e===window&&(e=document.body),s){var t=e.getBoundingClientRect(),n=document.createElement("div");if("-"===s.value)var r=-o;else r=o;n.style.position="absolute",n.style.top=t.x+r+"px",n.style.left=t.y+r+"px",n.style.height=t.height+2*r+"px",n.style.width=t.width+2*r+"px",n.style.zIndex=""+Number.MIN_SAFE_INTEGER,n.style.opacity="0",document.body.appendChild(n),setTimeout(function(){document.body.removeChild(n)},100),e=n}e.scrollIntoView(m)}),t.findNext(p,e)}};return p}}),e.config.conversions.dynamicResolvers.push(function(t,n){if("Values"===t||0===t.indexOf("Values:")){var r=t.split(":")[1],o={};if((0,e.internals.runtime.implicitLoop)(n,function(e){var t=i(e);void 0===t?null!=e.querySelectorAll&&e.querySelectorAll("input,select,textarea").forEach(a):o[t.name]=t.value}),r){if("JSON"===r)return JSON.stringify(o);if("Form"===r)return new URLSearchParams(o).toString();throw"Unknown conversion: "+r}return o}function a(e){var t=i(e);null!=t&&(null!=o[t.name]?Array.isArray(o[t.name])&&Array.isArray(t.value)&&(o[t.name]=[].concat(o[t.name],t.value)):o[t.name]=t.value)}function i(e){try{var t={name:e.name,value:e.value};if(null==t.name||null==t.value)return;if("radio"==e.type&&0==e.checked)return;if("checkbox"==e.type&&(0==e.checked?t.value=void 0:"string"==typeof t.value&&(t.value=[t.value])),"select-multiple"==e.type){var n=e.querySelectorAll("option[selected]");t.value=[];for(var r=0;r<n.length;r++)t.value.push(n[r].value)}return t}catch(e){return}}}),e.config.conversions.HTML=function(e){return function e(t){if(t instanceof Array)return t.map(function(t){return e(t)}).join("");if(t instanceof HTMLElement)return t.outerHTML;if(t instanceof NodeList){for(var n="",r=0;r<t.length;r++){var o=t[r];o instanceof HTMLElement&&(n+=o.outerHTML)}return n}return t.toString?t.toString():""}(e)},e.config.conversions.Fragment=function(t){var n=document.createDocumentFragment();return e.internals.runtime.implicitLoop(t,function(e){if(e instanceof Node)n.append(e);else{var t=document.createElement("template");t.innerHTML=e,n.append(t.content)}}),n}}(A),A});
-//# sourceMappingURL=_hyperscript_web.min.js.map
diff --git a/common/static/lib/js/hyperscript-0.9.7.min.js b/common/static/lib/js/hyperscript-0.9.7.min.js
new file mode 100644
index 00000000..4e22b3b8
--- /dev/null
+++ b/common/static/lib/js/hyperscript-0.9.7.min.js
@@ -0,0 +1 @@
+(function(e,t){const r=t(e);if(typeof exports==="object"&&typeof exports["nodeName"]!=="string"){module.exports=r}else{e["_hyperscript"]=r;if("document"in e)e["_hyperscript"].browserInit()}})(typeof self!=="undefined"?self:this,(e=>{"use strict";const t={dynamicResolvers:[function(e,t){if(e==="Fixed"){return Number(t).toFixed()}else if(e.indexOf("Fixed:")===0){let r=e.split(":")[1];return Number(t).toFixed(parseInt(r))}}],String:function(e){if(e.toString){return e.toString()}else{return""+e}},Int:function(e){return parseInt(e)},Float:function(e){return parseFloat(e)},Number:function(e){return Number(e)},Date:function(e){return new Date(e)},Array:function(e){return Array.from(e)},JSON:function(e){return JSON.stringify(e)},Object:function(e){if(e instanceof String){e=e.toString()}if(typeof e==="string"){return JSON.parse(e)}else{return Object.assign({},e)}}};const r={attributes:"_, script, data-script",defaultTransition:"all 500ms ease-in",disableSelector:"[disable-scripting], [data-disable-scripting]",conversions:t};class n{static OP_TABLE={"+":"PLUS","-":"MINUS","*":"MULTIPLY","/":"DIVIDE",".":"PERIOD","..":"ELLIPSIS","\\":"BACKSLASH",":":"COLON","%":"PERCENT","|":"PIPE","!":"EXCLAMATION","?":"QUESTION","#":"POUND","&":"AMPERSAND",$:"DOLLAR",";":"SEMI",",":"COMMA","(":"L_PAREN",")":"R_PAREN","<":"L_ANG",">":"R_ANG","<=":"LTE_ANG",">=":"GTE_ANG","==":"EQ","===":"EQQ","!=":"NEQ","!==":"NEQQ","{":"L_BRACE","}":"R_BRACE","[":"L_BRACKET","]":"R_BRACKET","=":"EQUALS"};static isValidCSSClassChar(e){return n.isAlpha(e)||n.isNumeric(e)||e==="-"||e==="_"||e===":"}static isValidCSSIDChar(e){return n.isAlpha(e)||n.isNumeric(e)||e==="-"||e==="_"||e===":"}static isWhitespace(e){return e===" "||e==="\t"||n.isNewline(e)}static positionString(e){return"[Line: "+e.line+", Column: "+e.column+"]"}static isNewline(e){return e==="\r"||e==="\n"}static isNumeric(e){return e>="0"&&e<="9"}static isAlpha(e){return e>="a"&&e<="z"||e>="A"&&e<="Z"}static isIdentifierChar(e,t){return e==="_"||e==="$"}static isReservedChar(e){return e==="`"||e==="^"}static isValidSingleQuoteStringStart(e){if(e.length>0){var t=e[e.length-1];if(t.type==="IDENTIFIER"||t.type==="CLASS_REF"||t.type==="ID_REF"){return false}if(t.op&&(t.value===">"||t.value===")")){return false}}return true}static tokenize(e,t){var r=[];var a=e;var o=0;var s=0;var u=1;var l="<START>";var c=0;function f(){return t&&c===0}while(o<a.length){if(S()==="-"&&q()==="-"&&(n.isWhitespace(N(2))||N(2)===""||N(2)==="-")||S()==="/"&&q()==="/"){h()}else if(S()==="/"&&q()==="*"){v()}else{if(n.isWhitespace(S())){r.push(C())}else if(!R()&&S()==="."&&(n.isAlpha(q())||q()==="{")){r.push(d())}else if(!R()&&S()==="#"&&(n.isAlpha(q())||q()==="{")){r.push(k())}else if(S()==="["&&q()==="@"){r.push(E())}else if(S()==="@"){r.push(T())}else if(S()==="*"&&n.isAlpha(q())){r.push(y())}else if(n.isAlpha(S())||!f()&&n.isIdentifierChar(S())){r.push(x())}else if(n.isNumeric(S())){r.push(g())}else if(!f()&&(S()==='"'||S()==="`")){r.push(w())}else if(!f()&&S()==="'"){if(n.isValidSingleQuoteStringStart(r)){r.push(w())}else{r.push(b())}}else if(n.OP_TABLE[S()]){if(l==="$"&&S()==="{"){c++}if(S()==="}"){c--}r.push(b())}else if(f()||n.isReservedChar(S())){r.push(p("RESERVED",I()))}else{if(o<a.length){throw Error("Unknown token: "+S()+" ")}}}}return new i(r,[],a);function m(e,t){var r=p(e,t);r.op=true;return r}function p(e,t){return{type:e,value:t||"",start:o,end:o+1,column:s,line:u}}function h(){while(S()&&!n.isNewline(S())){I()}I()}function v(){while(S()&&!(S()==="*"&&q()==="/")){I()}I();I()}function d(){var e=p("CLASS_REF");var t=I();if(S()==="{"){e.template=true;t+=I();while(S()&&S()!=="}"){t+=I()}if(S()!=="}"){throw Error("Unterminated class reference")}else{t+=I()}}else{while(n.isValidCSSClassChar(S())){t+=I()}}e.value=t;e.end=o;return e}function E(){var e=p("ATTRIBUTE_REF");var t=I();while(o<a.length&&S()!=="]"){t+=I()}if(S()==="]"){t+=I()}e.value=t;e.end=o;return e}function T(){var e=p("ATTRIBUTE_REF");var t=I();while(n.isValidCSSIDChar(S())){t+=I()}e.value=t;e.end=o;return e}function y(){var e=p("STYLE_REF");var t=I();while(n.isAlpha(S())||S()==="-"){t+=I()}e.value=t;e.end=o;return e}function k(){var e=p("ID_REF");var t=I();if(S()==="{"){e.template=true;t+=I();while(S()&&S()!=="}"){t+=I()}if(S()!=="}"){throw Error("Unterminated id reference")}else{I()}}else{while(n.isValidCSSIDChar(S())){t+=I()}}e.value=t;e.end=o;return e}function x(){var e=p("IDENTIFIER");var t=I();while(n.isAlpha(S())||n.isNumeric(S())||n.isIdentifierChar(S())){t+=I()}if(S()==="!"&&t==="beep"){t+=I()}e.value=t;e.end=o;return e}function g(){var e=p("NUMBER");var t=I();while(n.isNumeric(S())){t+=I()}if(S()==="."&&n.isNumeric(q())){t+=I()}while(n.isNumeric(S())){t+=I()}e.value=t;e.end=o;return e}function b(){var e=m();var t=I();while(S()&&n.OP_TABLE[t+S()]){t+=I()}e.type=n.OP_TABLE[t];e.value=t;e.end=o;return e}function w(){var e=p("STRING");var t=I();var r="";while(S()&&S()!==t){if(S()==="\\"){I();let e=I();if(e==="b"){r+="\b"}else if(e==="f"){r+="\f"}else if(e==="n"){r+="\n"}else if(e==="r"){r+="\r"}else if(e==="t"){r+="\t"}else if(e==="v"){r+="\v"}else{r+=e}}else{r+=I()}}if(S()!==t){throw Error("Unterminated string at "+n.positionString(e))}else{I()}e.value=r;e.end=o;e.template=t==="`";return e}function S(){return a.charAt(o)}function q(){return a.charAt(o+1)}function N(e=1){return a.charAt(o+e)}function I(){l=S();o++;s++;return l}function R(){return n.isAlpha(l)||n.isNumeric(l)||l===")"||l==='"'||l==="'"||l==="`"||l==="}"||l==="]"}function C(){var e=p("WHITESPACE");var t="";while(S()&&n.isWhitespace(S())){if(n.isNewline(S())){s=0;u++}t+=I()}e.value=t;e.end=o;return e}}tokenize(e,t){return n.tokenize(e,t)}}class i{constructor(e,t,r){this.tokens=e;this.consumed=t;this.source=r;this.consumeWhitespace()}get list(){return this.tokens}_lastConsumed=null;consumeWhitespace(){while(this.token(0,true).type==="WHITESPACE"){this.consumed.push(this.tokens.shift())}}raiseError(e,t){a.raiseParseError(e,t)}requireOpToken(e){var t=this.matchOpToken(e);if(t){return t}else{this.raiseError(this,"Expected '"+e+"' but found '"+this.currentToken().value+"'")}}matchAnyOpToken(e,t,r){for(var n=0;n<arguments.length;n++){var i=arguments[n];var a=this.matchOpToken(i);if(a){return a}}}matchAnyToken(e,t,r){for(var n=0;n<arguments.length;n++){var i=arguments[n];var a=this.matchToken(i);if(a){return a}}}matchOpToken(e){if(this.currentToken()&&this.currentToken().op&&this.currentToken().value===e){return this.consumeToken()}}requireTokenType(e,t,r,n){var i=this.matchTokenType(e,t,r,n);if(i){return i}else{this.raiseError(this,"Expected one of "+JSON.stringify([e,t,r]))}}matchTokenType(e,t,r,n){if(this.currentToken()&&this.currentToken().type&&[e,t,r,n].indexOf(this.currentToken().type)>=0){return this.consumeToken()}}requireToken(e,t){var r=this.matchToken(e,t);if(r){return r}else{this.raiseError(this,"Expected '"+e+"' but found '"+this.currentToken().value+"'")}}peekToken(e,t,r){return this.tokens[t]&&this.tokens[t].value===e&&this.tokens[t].type===r}matchToken(e,t){if(this.follows.indexOf(e)!==-1){return}t=t||"IDENTIFIER";if(this.currentToken()&&this.currentToken().value===e&&this.currentToken().type===t){return this.consumeToken()}}consumeToken(){var e=this.tokens.shift();this.consumed.push(e);this._lastConsumed=e;this.consumeWhitespace();return e}consumeUntil(e,t){var r=[];var n=this.token(0,true);while((t==null||n.type!==t)&&(e==null||n.value!==e)&&n.type!=="EOF"){var i=this.tokens.shift();this.consumed.push(i);r.push(n);n=this.token(0,true)}this.consumeWhitespace();return r}lastWhitespace(){if(this.consumed[this.consumed.length-1]&&this.consumed[this.consumed.length-1].type==="WHITESPACE"){return this.consumed[this.consumed.length-1].value}else{return""}}consumeUntilWhitespace(){return this.consumeUntil(null,"WHITESPACE")}hasMore(){return this.tokens.length>0}token(e,t){var r;var n=0;do{if(!t){while(this.tokens[n]&&this.tokens[n].type==="WHITESPACE"){n++}}r=this.tokens[n];e--;n++}while(e>-1);if(r){return r}else{return{type:"EOF",value:"<<<EOF>>>"}}}currentToken(){return this.token(0)}lastMatch(){return this._lastConsumed}static sourceFor=function(){return this.programSource.substring(this.startToken.start,this.endToken.end)};static lineFor=function(){return this.programSource.split("\n")[this.startToken.line-1]};follows=[];pushFollow(e){this.follows.push(e)}popFollow(){this.follows.pop()}clearFollows(){var e=this.follows;this.follows=[];return e}restoreFollows(e){this.follows=e}}class a{constructor(e){this.runtime=e;this.possessivesDisabled=false;this.addGrammarElement("feature",(function(e,t,r){if(r.matchOpToken("(")){var n=e.requireElement("feature",r);r.requireOpToken(")");return n}var i=e.FEATURES[r.currentToken().value||""];if(i){return i(e,t,r)}}));this.addGrammarElement("command",(function(e,t,r){if(r.matchOpToken("(")){const t=e.requireElement("command",r);r.requireOpToken(")");return t}var n=e.COMMANDS[r.currentToken().value||""];let i;if(n){i=n(e,t,r)}else if(r.currentToken().type==="IDENTIFIER"){i=e.parseElement("pseudoCommand",r)}if(i){return e.parseElement("indirectStatement",r,i)}return i}));this.addGrammarElement("commandList",(function(e,t,r){var n=e.parseElement("command",r);if(n){r.matchToken("then");const t=e.parseElement("commandList",r);if(t)n.next=t;return n}}));this.addGrammarElement("leaf",(function(e,t,r){var n=e.parseAnyOf(e.LEAF_EXPRESSIONS,r);if(n==null){return e.parseElement("symbol",r)}return n}));this.addGrammarElement("indirectExpression",(function(e,t,r,n){for(var i=0;i<e.INDIRECT_EXPRESSIONS.length;i++){var a=e.INDIRECT_EXPRESSIONS[i];n.endToken=r.lastMatch();var o=e.parseElement(a,r,n);if(o){return o}}return n}));this.addGrammarElement("indirectStatement",(function(e,t,r,n){if(r.matchToken("unless")){n.endToken=r.lastMatch();var i=e.requireElement("expression",r);var a={type:"unlessStatementModifier",args:[i],op:function(e,t){if(t){return this.next}else{return n}},execute:function(e){return t.unifiedExec(this,e)}};n.parent=a;return a}return n}));this.addGrammarElement("primaryExpression",(function(e,t,r){var n=e.parseElement("leaf",r);if(n){return e.parseElement("indirectExpression",r,n)}e.raiseParseError(r,"Unexpected value: "+r.currentToken().value)}))}use(e){e(this);return this}GRAMMAR={};COMMANDS={};FEATURES={};LEAF_EXPRESSIONS=[];INDIRECT_EXPRESSIONS=[];initElt(e,t,r){e.startToken=t;e.sourceFor=i.sourceFor;e.lineFor=i.lineFor;e.programSource=r.source}parseElement(e,t,r=undefined){var n=this.GRAMMAR[e];if(n){var i=t.currentToken();var a=n(this,this.runtime,t,r);if(a){this.initElt(a,i,t);a.endToken=a.endToken||t.lastMatch();var r=a.root;while(r!=null){this.initElt(r,i,t);r=r.root}}return a}}requireElement(e,t,r,n){var i=this.parseElement(e,t,n);if(!i)a.raiseParseError(t,r||"Expected "+e);return i}parseAnyOf(e,t){for(var r=0;r<e.length;r++){var n=e[r];var i=this.parseElement(n,t);if(i){return i}}}addGrammarElement(e,t){this.GRAMMAR[e]=t}addCommand(e,t){var r=e+"Command";var n=function(e,n,i){const a=t(e,n,i);if(a){a.type=r;a.execute=function(e){e.meta.command=a;return n.unifiedExec(this,e)};return a}};this.GRAMMAR[r]=n;this.COMMANDS[e]=n}addFeature(e,t){var r=e+"Feature";var n=function(n,i,a){var o=t(n,i,a);if(o){o.isFeature=true;o.keyword=e;o.type=r;return o}};this.GRAMMAR[r]=n;this.FEATURES[e]=n}addLeafExpression(e,t){this.LEAF_EXPRESSIONS.push(e);this.addGrammarElement(e,t)}addIndirectExpression(e,t){this.INDIRECT_EXPRESSIONS.push(e);this.addGrammarElement(e,t)}static createParserContext(e){var t=e.currentToken();var r=e.source;var n=r.split("\n");var i=t&&t.line?t.line-1:n.length-1;var a=n[i];var o=t&&t.line?t.column:a.length-1;return a+"\n"+" ".repeat(o)+"^^\n\n"}static raiseParseError(e,t){t=(t||"Unexpected Token : "+e.currentToken().value)+"\n\n"+a.createParserContext(e);var r=new Error(t);r["tokens"]=e;throw r}raiseParseError(e,t){a.raiseParseError(e,t)}parseHyperScript(e){var t=this.parseElement("hyperscript",e);if(e.hasMore())this.raiseParseError(e);if(t)return t}setParent(e,t){if(typeof e==="object"){e.parent=t;if(typeof t==="object"){t.children=t.children||new Set;t.children.add(e)}this.setParent(e.next,t)}}commandStart(e){return this.COMMANDS[e.value||""]}featureStart(e){return this.FEATURES[e.value||""]}commandBoundary(e){if(e.value=="end"||e.value=="then"||e.value=="else"||e.value=="otherwise"||e.value==")"||this.commandStart(e)||this.featureStart(e)||e.type=="EOF"){return true}return false}parseStringTemplate(e){var t=[""];do{t.push(e.lastWhitespace());if(e.currentToken().value==="$"){e.consumeToken();var r=e.matchOpToken("{");t.push(this.requireElement("expression",e));if(r){e.requireOpToken("}")}t.push("")}else if(e.currentToken().value==="\\"){e.consumeToken();e.consumeToken()}else{var n=e.consumeToken();t[t.length-1]+=n?n.value:""}}while(e.hasMore());t.push(e.lastWhitespace());return t}ensureTerminated(e){const t=this.runtime;var r={type:"implicitReturn",op:function(e){e.meta.returned=true;if(e.meta.resolve){e.meta.resolve()}return t.HALT},execute:function(e){}};var n=e;while(n.next){n=n.next}n.next=r}}class o{constructor(e,t){this.lexer=e??new n;this.parser=t??new a(this).use(h).use(v);this.parser.runtime=this}matchesSelector(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}makeEvent(t,r){var n;if(e.Event&&typeof e.Event==="function"){n=new Event(t,{bubbles:true,cancelable:true});n["detail"]=r}else{n=document.createEvent("CustomEvent");n.initCustomEvent(t,true,true,r)}return n}triggerEvent(e,t,r,n){r=r||{};r["sender"]=n;var i=this.makeEvent(t,r);var a=e.dispatchEvent(i);return a}isArrayLike(e){return Array.isArray(e)||typeof NodeList!=="undefined"&&(e instanceof NodeList||e instanceof HTMLCollection)}isIterable(e){return typeof e==="object"&&Symbol.iterator in e&&typeof e[Symbol.iterator]==="function"}shouldAutoIterate(e){return e!=null&&e[l]||this.isArrayLike(e)}forEach(e,t){if(e==null){}else if(this.isIterable(e)){for(const r of e){t(r)}}else if(this.isArrayLike(e)){for(var r=0;r<e.length;r++){t(e[r])}}else{t(e)}}implicitLoop(e,t){if(this.shouldAutoIterate(e)){for(const r of e)t(r)}else{t(e)}}wrapArrays(e){var t=[];for(var r=0;r<e.length;r++){var n=e[r];if(Array.isArray(n)){t.push(Promise.all(n))}else{t.push(n)}}return t}unwrapAsyncs(e){for(var t=0;t<e.length;t++){var r=e[t];if(r.asyncWrapper){e[t]=r.value}if(Array.isArray(r)){for(var n=0;n<r.length;n++){var i=r[n];if(i.asyncWrapper){r[n]=i.value}}}}}static HALT={};HALT=o.HALT;unifiedExec(e,t){while(true){try{var r=this.unifiedEval(e,t)}catch(n){if(t.meta.handlingFinally){console.error(" Exception in finally block: ",n);r=o.HALT}else{this.registerHyperTrace(t,n);if(t.meta.errorHandler&&!t.meta.handlingError){t.meta.handlingError=true;t.locals[t.meta.errorSymbol]=n;e=t.meta.errorHandler;continue}else{t.meta.currentException=n;r=o.HALT}}}if(r==null){console.error(e," did not return a next element to execute! context: ",t);return}else if(r.then){r.then((e=>{this.unifiedExec(e,t)})).catch((e=>{this.unifiedExec({op:function(){throw e}},t)}));return}else if(r===o.HALT){if(t.meta.finallyHandler&&!t.meta.handlingFinally){t.meta.handlingFinally=true;e=t.meta.finallyHandler}else{if(t.meta.onHalt){t.meta.onHalt()}if(t.meta.currentException){if(t.meta.reject){t.meta.reject(t.meta.currentException);return}else{throw t.meta.currentException}}else{return}}}else{e=r}}}unifiedEval(e,t){var r=[t];var n=false;var i=false;if(e.args){for(var a=0;a<e.args.length;a++){var o=e.args[a];if(o==null){r.push(null)}else if(Array.isArray(o)){var s=[];for(var u=0;u<o.length;u++){var l=o[u];var c=l?l.evaluate(t):null;if(c){if(c.then){n=true}else if(c.asyncWrapper){i=true}}s.push(c)}r.push(s)}else if(o.evaluate){var c=o.evaluate(t);if(c){if(c.then){n=true}else if(c.asyncWrapper){i=true}}r.push(c)}else{r.push(o)}}}if(n){return new Promise(((t,n)=>{r=this.wrapArrays(r);Promise.all(r).then((function(r){if(i){this.unwrapAsyncs(r)}try{var a=e.op.apply(e,r);t(a)}catch(e){n(e)}})).catch((function(e){n(e)}))}))}else{if(i){this.unwrapAsyncs(r)}return e.op.apply(e,r)}}_scriptAttrs=null;getScriptAttributes(){if(this._scriptAttrs==null){this._scriptAttrs=r.attributes.replace(/ /g,"").split(",")}return this._scriptAttrs}getScript(e){for(var t=0;t<this.getScriptAttributes().length;t++){var r=this.getScriptAttributes()[t];if(e.hasAttribute&&e.hasAttribute(r)){return e.getAttribute(r)}}if(e instanceof HTMLScriptElement&&e.type==="text/hyperscript"){return e.innerText}return null}hyperscriptFeaturesMap=new WeakMap;getHyperscriptFeatures(e){var t=this.hyperscriptFeaturesMap.get(e);if(typeof t==="undefined"){if(e){this.hyperscriptFeaturesMap.set(e,t={})}}return t}addFeatures(e,t){if(e){Object.assign(t.locals,this.getHyperscriptFeatures(e));this.addFeatures(e.parentElement,t)}}makeContext(e,t,r,n){return new s(e,t,r,n,this)}getScriptSelector(){return this.getScriptAttributes().map((function(e){return"["+e+"]"})).join(", ")}convertValue(e,r){var n=t.dynamicResolvers;for(var i=0;i<n.length;i++){var a=n[i];var o=a(r,e);if(o!==undefined){return o}}if(e==null){return null}var s=t[r];if(s){return s(e)}throw"Unknown conversion : "+r}parse(e){const t=this.lexer,r=this.parser;var n=t.tokenize(e);if(this.parser.commandStart(n.currentToken())){var i=r.requireElement("commandList",n);if(n.hasMore())r.raiseParseError(n);r.ensureTerminated(i);return i}else if(r.featureStart(n.currentToken())){var a=r.requireElement("hyperscript",n);if(n.hasMore())r.raiseParseError(n);return a}else{var o=r.requireElement("expression",n);if(n.hasMore())r.raiseParseError(n);return o}}evaluateNoPromise(e,t){let r=e.evaluate(t);if(r.next){throw new Error(i.sourceFor.call(e)+" returned a Promise in a context that they are not allowed.")}return r}evaluate(t,r,n){class i extends EventTarget{constructor(e){super();this.module=e}toString(){return this.module.id}}var a="document"in e?e.document.body:new i(n&&n.module);r=Object.assign(this.makeContext(a,null,a,null),r||{});var o=this.parse(t);if(o.execute){o.execute(r);return r.result}else if(o.apply){o.apply(a,a,n);return this.getHyperscriptFeatures(a)}else{return o.evaluate(r)}function s(){return{}}}processNode(e){var t=this.getScriptSelector();if(this.matchesSelector(e,t)){this.initElement(e,e)}if(e instanceof HTMLScriptElement&&e.type==="text/hyperscript"){this.initElement(e,document.body)}if(e.querySelectorAll){this.forEach(e.querySelectorAll(t+", [type='text/hyperscript']"),(e=>{this.initElement(e,e instanceof HTMLScriptElement&&e.type==="text/hyperscript"?document.body:e)}))}}initElement(e,t){if(e.closest&&e.closest(r.disableSelector)){return}var n=this.getInternalData(e);if(!n.initialized){var i=this.getScript(e);if(i){try{n.initialized=true;n.script=i;const r=this.lexer,s=this.parser;var a=r.tokenize(i);var o=s.parseHyperScript(a);if(!o)return;o.apply(t||e,e);setTimeout((()=>{this.triggerEvent(t||e,"load",{hyperscript:true})}),1)}catch(t){this.triggerEvent(e,"exception",{error:t});console.error("hyperscript errors were found on the following element:",e,"\n\n",t.message,t.stack)}}}}internalDataMap=new WeakMap;getInternalData(e){var t=this.internalDataMap.get(e);if(typeof t==="undefined"){this.internalDataMap.set(e,t={})}return t}typeCheck(e,t,r){if(e==null&&r){return true}var n=Object.prototype.toString.call(e).slice(8,-1);return n===t}getElementScope(e){var t=e.meta&&e.meta.owner;if(t){var r=this.getInternalData(t);var n="elementScope";if(e.meta.feature&&e.meta.feature.behavior){n=e.meta.feature.behavior+"Scope"}var i=c(r,n);return i}else{return{}}}isReservedWord(e){return["meta","it","result","locals","event","target","detail","sender","body"].includes(e)}isHyperscriptContext(e){return e instanceof s}resolveSymbol(t,r,n){if(t==="me"||t==="my"||t==="I"){return r.me}if(t==="it"||t==="its"||t==="result"){return r.result}if(t==="you"||t==="your"||t==="yourself"){return r.you}else{if(n==="global"){return e[t]}else if(n==="element"){var i=this.getElementScope(r);return i[t]}else if(n==="local"){return r.locals[t]}else{if(r.meta&&r.meta.context){var a=r.meta.context[t];if(typeof a!=="undefined"){return a}}if(this.isHyperscriptContext(r)&&!this.isReservedWord(t)){var o=r.locals[t]}else{var o=r[t]}if(typeof o!=="undefined"){return o}else{var i=this.getElementScope(r);o=i[t];if(typeof o!=="undefined"){return o}else{return e[t]}}}}}setSymbol(t,r,n,i){if(n==="global"){e[t]=i}else if(n==="element"){var a=this.getElementScope(r);a[t]=i}else if(n==="local"){r.locals[t]=i}else{if(this.isHyperscriptContext(r)&&!this.isReservedWord(t)&&typeof r.locals[t]!=="undefined"){r.locals[t]=i}else{var a=this.getElementScope(r);var o=a[t];if(typeof o!=="undefined"){a[t]=i}else{if(this.isHyperscriptContext(r)&&!this.isReservedWord(t)){r.locals[t]=i}else{r[t]=i}}}}}findNext(e,t){if(e){if(e.resolveNext){return e.resolveNext(t)}else if(e.next){return e.next}else{return this.findNext(e.parent,t)}}}flatGet(e,t,r){if(e!=null){var n=r(e,t);if(typeof n!=="undefined"){return n}if(this.shouldAutoIterate(e)){var i=[];for(var a of e){var o=r(a,t);i.push(o)}return i}}}resolveProperty(e,t){return this.flatGet(e,t,((e,t)=>e[t]))}resolveAttribute(e,t){return this.flatGet(e,t,((e,t)=>e.getAttribute&&e.getAttribute(t)))}resolveStyle(e,t){return this.flatGet(e,t,((e,t)=>e.style&&e.style[t]))}resolveComputedStyle(e,t){return this.flatGet(e,t,((e,t)=>getComputedStyle(e).getPropertyValue(t)))}assignToNamespace(t,r,n,i){let a;if(typeof document!=="undefined"&&t===document.body){a=e}else{a=this.getHyperscriptFeatures(t)}var o;while((o=r.shift())!==undefined){var s=a[o];if(s==null){s={};a[o]=s}a=s}a[n]=i}getHyperTrace(e,t){var r=[];var n=e;while(n.meta.caller){n=n.meta.caller}if(n.meta.traceMap){return n.meta.traceMap.get(t,r)}}registerHyperTrace(e,t){var r=[];var n=null;while(e!=null){r.push(e);n=e;e=e.meta.caller}if(n.meta.traceMap==null){n.meta.traceMap=new Map}if(!n.meta.traceMap.get(t)){var i={trace:r,print:function(e){e=e||console.error;e("hypertrace /// ");var t=0;for(var n=0;n<r.length;n++){t=Math.max(t,r[n].meta.feature.displayName.length)}for(var n=0;n<r.length;n++){var i=r[n];e("  ->",i.meta.feature.displayName.padEnd(t+2),"-",i.meta.owner)}}};n.meta.traceMap.set(t,i)}}escapeSelector(e){return e.replace(/:/g,(function(e){return"\\"+e}))}nullCheck(e,t){if(e==null){throw new Error("'"+t.sourceFor()+"' is null")}}isEmpty(e){return e==undefined||e.length===0}doesExist(e){if(e==null){return false}if(this.shouldAutoIterate(e)){for(const t of e){return true}}return false}getRootNode(e){if(e&&e instanceof Node){var t=e.getRootNode();if(t instanceof Document||t instanceof ShadowRoot)return t}return document}getEventQueueFor(e,t){let r=this.getInternalData(e);var n=r.eventQueues;if(n==null){n=new Map;r.eventQueues=n}var i=n.get(t);if(i==null){i={queue:[],executing:false};n.set(t,i)}return i}hyperscriptUrl="document"in e?document.currentScript.src:null}class s{constructor(t,r,n,i,a){this.meta={parser:a.parser,lexer:a.lexer,runtime:a,owner:t,feature:r,iterators:{},ctx:this};this.locals={};this.me=n,this.you=undefined;this.result=undefined;this.event=i;this.target=i?i.target:null;this.detail=i?i.detail:null;this.sender=i?i.detail?i.detail.sender:null:null;this.body="document"in e?document.body:null;a.addFeatures(t,this)}}class u{constructor(e,t,r){this._css=e;this.relativeToElement=t;this.escape=r;this[l]=true}get css(){if(this.escape){return o.prototype.escapeSelector(this._css)}else{return this._css}}get className(){return this._css.substr(1)}get id(){return this.className()}contains(e){for(let t of this){if(t.contains(e)){return true}}return false}get length(){return this.selectMatches().length}[Symbol.iterator](){let e=this.selectMatches();return e[Symbol.iterator]()}selectMatches(){let e=o.prototype.getRootNode(this.relativeToElement).querySelectorAll(this.css);return e}}const l=Symbol();function c(e,t){var r=e[t];if(r){return r}else{var n={};e[t]=n;return n}}function f(e){try{return JSON.parse(e)}catch(e){m(e);return null}}function m(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function p(e,t){return new(e.bind.apply(e,[e].concat(t)))}function h(t){t.addLeafExpression("parenthesized",(function(e,t,r){if(r.matchOpToken("(")){var n=r.clearFollows();try{var i=e.requireElement("expression",r)}finally{r.restoreFollows(n)}r.requireOpToken(")");return i}}));t.addLeafExpression("string",(function(e,t,r){var i=r.matchTokenType("STRING");if(!i)return;var a=i.value;var o;if(i.template){var s=n.tokenize(a,true);o=e.parseStringTemplate(s)}else{o=[]}return{type:"string",token:i,args:o,op:function(e){var t="";for(var r=1;r<arguments.length;r++){var n=arguments[r];if(n!==undefined){t+=n}}return t},evaluate:function(e){if(o.length===0){return a}else{return t.unifiedEval(this,e)}}}}));t.addGrammarElement("nakedString",(function(e,t,r){if(r.hasMore()){var n=r.consumeUntilWhitespace();r.matchTokenType("WHITESPACE");return{type:"nakedString",tokens:n,evaluate:function(e){return n.map((function(e){return e.value})).join("")}}}}));t.addLeafExpression("number",(function(e,t,r){var n=r.matchTokenType("NUMBER");if(!n)return;var i=n;var a=parseFloat(n.value);return{type:"number",value:a,numberToken:i,evaluate:function(){return a}}}));t.addLeafExpression("idRef",(function(e,t,r){var i=r.matchTokenType("ID_REF");if(!i)return;if(!i.value)return;if(i.template){var a=i.value.substring(2);var o=n.tokenize(a);var s=e.requireElement("expression",o);return{type:"idRefTemplate",args:[s],op:function(e,r){return t.getRootNode(e.me).getElementById(r)},evaluate:function(e){return t.unifiedEval(this,e)}}}else{const e=i.value.substring(1);return{type:"idRef",css:i.value,value:e,evaluate:function(r){return t.getRootNode(r.me).getElementById(e)}}}}));t.addLeafExpression("classRef",(function(e,t,r){var i=r.matchTokenType("CLASS_REF");if(!i)return;if(!i.value)return;if(i.template){var a=i.value.substring(2);var o=n.tokenize(a);var s=e.requireElement("expression",o);return{type:"classRefTemplate",args:[s],op:function(e,t){return new u("."+t,e.me,true)},evaluate:function(e){return t.unifiedEval(this,e)}}}else{const e=i.value;return{type:"classRef",css:e,evaluate:function(t){return new u(e,t.me,true)}}}}));class r extends u{constructor(e,t,r){super(e,t);this.templateParts=r;this.elements=r.filter((e=>e instanceof Element))}get css(){let e="",t=0;for(const r of this.templateParts){if(r instanceof Element){e+="[data-hs-query-id='"+t+++"']"}else e+=r}return e}[Symbol.iterator](){this.elements.forEach(((e,t)=>e.dataset.hsQueryId=t));const e=super[Symbol.iterator]();this.elements.forEach((e=>e.removeAttribute("data-hs-query-id")));return e}}t.addLeafExpression("queryRef",(function(e,t,i){var a=i.matchOpToken("<");if(!a)return;var o=i.consumeUntil("/");i.requireOpToken("/");i.requireOpToken(">");var s=o.map((function(e){if(e.type==="STRING"){return'"'+e.value+'"'}else{return e.value}})).join("");var l,c,f;if(s.indexOf("$")>=0){l=true;c=n.tokenize(s,true);f=e.parseStringTemplate(c)}return{type:"queryRef",css:s,args:f,op:function(e,...t){if(l){return new r(s,e.me,t)}else{return new u(s,e.me)}},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addLeafExpression("attributeRef",(function(e,t,r){var n=r.matchTokenType("ATTRIBUTE_REF");if(!n)return;if(!n.value)return;var i=n.value;if(i.indexOf("[")===0){var a=i.substring(2,i.length-1)}else{var a=i.substring(1)}var o="["+a+"]";var s=a.split("=");var u=s[0];var l=s[1];if(l){if(l.indexOf('"')===0){l=l.substring(1,l.length-1)}}return{type:"attributeRef",name:u,css:o,value:l,op:function(e){var t=e.you||e.me;if(t){return t.getAttribute(u)}},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addLeafExpression("styleRef",(function(e,t,r){var n=r.matchTokenType("STYLE_REF");if(!n)return;if(!n.value)return;var i=n.value.substr(1);if(i.startsWith("computed-")){i=i.substr("computed-".length);return{type:"computedStyleRef",name:i,op:function(e){var r=e.you||e.me;if(r){return t.resolveComputedStyle(r,i)}},evaluate:function(e){return t.unifiedEval(this,e)}}}else{return{type:"styleRef",name:i,op:function(e){var r=e.you||e.me;if(r){return t.resolveStyle(r,i)}},evaluate:function(e){return t.unifiedEval(this,e)}}}}));t.addGrammarElement("objectKey",(function(e,t,r){var n;if(n=r.matchTokenType("STRING")){return{type:"objectKey",key:n.value,evaluate:function(){return n.value}}}else if(r.matchOpToken("[")){var i=e.parseElement("expression",r);r.requireOpToken("]");return{type:"objectKey",expr:i,args:[i],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}else{var a="";do{n=r.matchTokenType("IDENTIFIER")||r.matchOpToken("-");if(n)a+=n.value}while(n);return{type:"objectKey",key:a,evaluate:function(){return a}}}}));t.addLeafExpression("objectLiteral",(function(e,t,r){if(!r.matchOpToken("{"))return;var n=[];var i=[];if(!r.matchOpToken("}")){do{var a=e.requireElement("objectKey",r);r.requireOpToken(":");var o=e.requireElement("expression",r);i.push(o);n.push(a)}while(r.matchOpToken(","));r.requireOpToken("}")}return{type:"objectLiteral",args:[n,i],op:function(e,t,r){var n={};for(var i=0;i<t.length;i++){n[t[i]]=r[i]}return n},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("nakedNamedArgumentList",(function(e,t,r){var n=[];var i=[];if(r.currentToken().type==="IDENTIFIER"){do{var a=r.requireTokenType("IDENTIFIER");r.requireOpToken(":");var o=e.requireElement("expression",r);i.push(o);n.push({name:a,value:o})}while(r.matchOpToken(","))}return{type:"namedArgumentList",fields:n,args:[i],op:function(e,t){var r={_namedArgList_:true};for(var i=0;i<t.length;i++){var a=n[i];r[a.name.value]=t[i]}return r},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("namedArgumentList",(function(e,t,r){if(!r.matchOpToken("("))return;var n=e.requireElement("nakedNamedArgumentList",r);r.requireOpToken(")");return n}));t.addGrammarElement("symbol",(function(e,t,r){var n="default";if(r.matchToken("global")){n="global"}else if(r.matchToken("element")||r.matchToken("module")){n="element";if(r.matchOpToken("'")){r.requireToken("s")}}else if(r.matchToken("local")){n="local"}let i=r.matchOpToken(":");let a=r.matchTokenType("IDENTIFIER");if(a&&a.value){var o=a.value;if(i){o=":"+o}if(n==="default"){if(o.indexOf("$")===0){n="global"}if(o.indexOf(":")===0){n="element"}}return{type:"symbol",token:a,scope:n,name:o,evaluate:function(e){return t.resolveSymbol(o,e,n)}}}}));t.addGrammarElement("implicitMeTarget",(function(e,t,r){return{type:"implicitMeTarget",evaluate:function(e){return e.you||e.me}}}));t.addLeafExpression("boolean",(function(e,t,r){var n=r.matchToken("true")||r.matchToken("false");if(!n)return;const i=n.value==="true";return{type:"boolean",evaluate:function(e){return i}}}));t.addLeafExpression("null",(function(e,t,r){if(r.matchToken("null")){return{type:"null",evaluate:function(e){return null}}}}));t.addLeafExpression("arrayLiteral",(function(e,t,r){if(!r.matchOpToken("["))return;var n=[];if(!r.matchOpToken("]")){do{var i=e.requireElement("expression",r);n.push(i)}while(r.matchOpToken(","));r.requireOpToken("]")}return{type:"arrayLiteral",values:n,args:[n],op:function(e,t){return t},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addLeafExpression("blockLiteral",(function(e,t,r){if(!r.matchOpToken("\\"))return;var n=[];var i=r.matchTokenType("IDENTIFIER");if(i){n.push(i);while(r.matchOpToken(",")){n.push(r.requireTokenType("IDENTIFIER"))}}r.requireOpToken("-");r.requireOpToken(">");var a=e.requireElement("expression",r);return{type:"blockLiteral",args:n,expr:a,evaluate:function(e){var t=function(){for(var t=0;t<n.length;t++){e.locals[n[t].value]=arguments[t]}return a.evaluate(e)};return t}}}));t.addIndirectExpression("propertyAccess",(function(e,t,r,n){if(!r.matchOpToken("."))return;var i=r.requireTokenType("IDENTIFIER");var a={type:"propertyAccess",root:n,prop:i,args:[n],op:function(e,r){var n=t.resolveProperty(r,i.value);return n},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",r,a)}));t.addIndirectExpression("of",(function(e,t,r,n){if(!r.matchToken("of"))return;var i=e.requireElement("unaryExpression",r);var a=null;var o=n;while(o.root){a=o;o=o.root}if(o.type!=="symbol"&&o.type!=="attributeRef"&&o.type!=="styleRef"&&o.type!=="computedStyleRef"){e.raiseParseError(r,"Cannot take a property of a non-symbol: "+o.type)}var s=o.type==="attributeRef";var u=o.type==="styleRef"||o.type==="computedStyleRef";if(s||u){var l=o}var c=o.name;var f={type:"ofExpression",prop:o.token,root:i,attribute:l,expression:n,args:[i],op:function(e,r){if(s){return t.resolveAttribute(r,c)}else if(u){if(o.type==="computedStyleRef"){return t.resolveComputedStyle(r,c)}else{return t.resolveStyle(r,c)}}else{return t.resolveProperty(r,c)}},evaluate:function(e){return t.unifiedEval(this,e)}};if(o.type==="attributeRef"){f.attribute=o}if(a){a.root=f;a.args=[f]}else{n=f}return e.parseElement("indirectExpression",r,n)}));t.addIndirectExpression("possessive",(function(e,t,r,n){if(e.possessivesDisabled){return}var i=r.matchOpToken("'");if(i||n.type==="symbol"&&(n.name==="my"||n.name==="its"||n.name==="your")&&(r.currentToken().type==="IDENTIFIER"||r.currentToken().type==="ATTRIBUTE_REF"||r.currentToken().type==="STYLE_REF")){if(i){r.requireToken("s")}var a,o,s;a=e.parseElement("attributeRef",r);if(a==null){o=e.parseElement("styleRef",r);if(o==null){s=r.requireTokenType("IDENTIFIER")}}var u={type:"possessive",root:n,attribute:a||o,prop:s,args:[n],op:function(e,r){if(a){var n=t.resolveAttribute(r,a.name)}else if(o){var n;if(o.type==="computedStyleRef"){n=t.resolveComputedStyle(r,o["name"])}else{n=t.resolveStyle(r,o["name"])}}else{var n=t.resolveProperty(r,s.value)}return n},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",r,u)}}));t.addIndirectExpression("inExpression",(function(e,t,r,n){if(!r.matchToken("in"))return;var i=e.requireElement("unaryExpression",r);var a={type:"inExpression",root:n,args:[n,i],op:function(e,r,n){var i=[];if(r.css){t.implicitLoop(n,(function(e){var t=e.querySelectorAll(r.css);for(var n=0;n<t.length;n++){i.push(t[n])}}))}else if(r instanceof Element){var a=false;t.implicitLoop(n,(function(e){if(e.contains(r)){a=true}}));if(a){return r}}else{t.implicitLoop(r,(function(e){t.implicitLoop(n,(function(t){if(e===t){i.push(e)}}))}))}return i},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",r,a)}));t.addIndirectExpression("asExpression",(function(e,t,r,n){if(!r.matchToken("as"))return;r.matchToken("a")||r.matchToken("an");var i=e.requireElement("dotOrColonPath",r).evaluate();var a={type:"asExpression",root:n,args:[n],op:function(e,r){return t.convertValue(r,i)},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",r,a)}));t.addIndirectExpression("functionCall",(function(e,t,r,n){if(!r.matchOpToken("("))return;var i=[];if(!r.matchOpToken(")")){do{i.push(e.requireElement("expression",r))}while(r.matchOpToken(","));r.requireOpToken(")")}if(n.root){var a={type:"functionCall",root:n,argExressions:i,args:[n.root,i],op:function(e,r,i){t.nullCheck(r,n.root);var a=r[n.prop.value];t.nullCheck(a,n);if(a.hyperfunc){i.push(e)}return a.apply(r,i)},evaluate:function(e){return t.unifiedEval(this,e)}}}else{var a={type:"functionCall",root:n,argExressions:i,args:[n,i],op:function(e,r,i){t.nullCheck(r,n);if(r.hyperfunc){i.push(e)}var a=r.apply(null,i);return a},evaluate:function(e){return t.unifiedEval(this,e)}}}return e.parseElement("indirectExpression",r,a)}));t.addIndirectExpression("attributeRefAccess",(function(e,t,r,n){var i=e.parseElement("attributeRef",r);if(!i)return;var a={type:"attributeRefAccess",root:n,attribute:i,args:[n],op:function(e,r){var n=t.resolveAttribute(r,i.name);return n},evaluate:function(e){return t.unifiedEval(this,e)}};return a}));t.addIndirectExpression("arrayIndex",(function(e,t,r,n){if(!r.matchOpToken("["))return;var i=false;var a=false;var o=null;var s=null;if(r.matchOpToken("..")){i=true;o=e.requireElement("expression",r)}else{o=e.requireElement("expression",r);if(r.matchOpToken("..")){a=true;var u=r.currentToken();if(u.type!=="R_BRACKET"){s=e.parseElement("expression",r)}}}r.requireOpToken("]");var l={type:"arrayIndex",root:n,prop:o,firstIndex:o,secondIndex:s,args:[n,o,s],op:function(e,t,r,n){if(t==null){return null}if(i){if(r<0){r=t.length+r}return t.slice(0,r+1)}else if(a){if(n!=null){if(n<0){n=t.length+n}return t.slice(r,n+1)}else{return t.slice(r)}}else{return t[r]}},evaluate:function(e){return t.unifiedEval(this,e)}};return e.parseElement("indirectExpression",r,l)}));var a=["em","ex","cap","ch","ic","rem","lh","rlh","vw","vh","vi","vb","vmin","vmax","cm","mm","Q","pc","pt","px"];t.addGrammarElement("postfixExpression",(function(e,t,r){var n=e.parseElement("primaryExpression",r);let i=r.matchAnyToken.apply(r,a)||r.matchOpToken("%");if(i){return{type:"stringPostfix",postfix:i.value,args:[n],op:function(e,t){return""+t+i.value},evaluate:function(e){return t.unifiedEval(this,e)}}}var o=null;if(r.matchToken("s")||r.matchToken("seconds")){o=1e3}else if(r.matchToken("ms")||r.matchToken("milliseconds")){o=1}if(o){return{type:"timeExpression",time:n,factor:o,args:[n],op:function(e,t){return t*o},evaluate:function(e){return t.unifiedEval(this,e)}}}if(r.matchOpToken(":")){var s=r.requireTokenType("IDENTIFIER");if(!s.value)return;var u=!r.matchOpToken("!");return{type:"typeCheck",typeName:s,nullOk:u,args:[n],op:function(e,r){var n=t.typeCheck(r,this.typeName.value,u);if(n){return r}else{throw new Error("Typecheck failed!  Expected: "+s.value)}},evaluate:function(e){return t.unifiedEval(this,e)}}}else{return n}}));t.addGrammarElement("logicalNot",(function(e,t,r){if(!r.matchToken("not"))return;var n=e.requireElement("unaryExpression",r);return{type:"logicalNot",root:n,args:[n],op:function(e,t){return!t},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("noExpression",(function(e,t,r){if(!r.matchToken("no"))return;var n=e.requireElement("unaryExpression",r);return{type:"noExpression",root:n,args:[n],op:function(e,r){return t.isEmpty(r)},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addLeafExpression("some",(function(e,t,r){if(!r.matchToken("some"))return;var n=e.requireElement("expression",r);return{type:"noExpression",root:n,args:[n],op:function(e,r){return!t.isEmpty(r)},evaluate(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("negativeNumber",(function(e,t,r){if(!r.matchOpToken("-"))return;var n=e.requireElement("unaryExpression",r);return{type:"negativeNumber",root:n,args:[n],op:function(e,t){return-1*t},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("unaryExpression",(function(e,t,r){r.matchToken("the");return e.parseAnyOf(["beepExpression","logicalNot","relativePositionalExpression","positionalExpression","noExpression","negativeNumber","postfixExpression"],r)}));t.addGrammarElement("beepExpression",(function(e,t,r){if(!r.matchToken("beep!"))return;var n=e.parseElement("unaryExpression",r);if(n){n["booped"]=true;var a=n.evaluate;n.evaluate=function(e){let r=a.apply(n,arguments);let o=e.me;if(t.triggerEvent(o,"hyperscript:beep",{element:o,expression:n,value:r})){var s;if(r){if(r instanceof u){s="ElementCollection"}else if(r.constructor){s=r.constructor.name}else{s="unknown"}}else{s="object (null)"}var l=r;if(s==="String"){l='"'+l+'"'}else if(r instanceof u){l=Array.from(r)}console.log("///_ BEEP! The expression ("+i.sourceFor.call(n).substr(6)+") evaluates to:",l,"of type "+s)}return r};return n}}));var s=function(e,t,r,n){var i=t.querySelectorAll(r);for(var a=0;a<i.length;a++){var o=i[a];if(o.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return o}}if(n){return i[0]}};var l=function(e,t,r,n){var i=t.querySelectorAll(r);for(var a=i.length-1;a>=0;a--){var o=i[a];if(o.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}if(n){return i[i.length-1]}};var f=function(e,t,r,n){var i=[];o.prototype.forEach(t,(function(t){if(t.matches(r)||t===e){i.push(t)}}));for(var a=0;a<i.length-1;a++){var s=i[a];if(s===e){return i[a+1]}}if(n){var u=i[0];if(u&&u.matches(r)){return u}}};var m=function(e,t,r,n){return f(e,Array.from(t).reverse(),r,n)};t.addGrammarElement("relativePositionalExpression",(function(e,t,r){var n=r.matchAnyToken("next","previous");if(!n)return;var i=n.value==="next";var a=e.parseElement("expression",r);if(r.matchToken("from")){r.pushFollow("in");try{var o=e.requireElement("unaryExpression",r)}finally{r.popFollow()}}else{var o=e.requireElement("implicitMeTarget",r)}var u=false;var c;if(r.matchToken("in")){u=true;var p=e.requireElement("unaryExpression",r)}else if(r.matchToken("within")){c=e.requireElement("unaryExpression",r)}else{c=document.body}var h=false;if(r.matchToken("with")){r.requireToken("wrapping");h=true}return{type:"relativePositionalExpression",from:o,forwardSearch:i,inSearch:u,wrapping:h,inElt:p,withinElt:c,operator:n.value,args:[a,o,p,c],op:function(e,t,r,n,a){var o=t.css;if(o==null){throw"Expected a CSS value"}if(u){if(n){if(i){return f(r,n,o,h)}else{return m(r,n,o,h)}}}else{if(a){if(i){return s(r,a,o,h)}else{return l(r,a,o,h)}}}},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("positionalExpression",(function(e,t,r){var n=r.matchAnyToken("first","last","random");if(!n)return;r.matchAnyToken("in","from","of");var i=e.requireElement("unaryExpression",r);const a=n.value;return{type:"positionalExpression",rhs:i,operator:n.value,args:[i],op:function(e,t){if(t&&!Array.isArray(t)){if(t.children){t=t.children}else{t=Array.from(t)}}if(t){if(a==="first"){return t[0]}else if(a==="last"){return t[t.length-1]}else if(a==="random"){return t[Math.floor(Math.random()*t.length)]}}},evaluate:function(e){return t.unifiedEval(this,e)}}}));t.addGrammarElement("mathOperator",(function(e,t,r){var n=e.parseElement("unaryExpression",r);var i,a=null;i=r.matchAnyOpToken("+","-","*","/","%");while(i){a=a||i;var o=i.value;if(a.value!==o){e.raiseParseError(r,"You must parenthesize math operations with different operators")}var s=e.parseElement("unaryExpression",r);n={type:"mathOperator",lhs:n,rhs:s,operator:o,args:[n,s],op:function(e,t,r){if(o==="+"){return t+r}else if(o==="-"){return t-r}else if(o==="*"){return t*r}else if(o==="/"){return t/r}else if(o==="%"){return t%r}},evaluate:function(e){return t.unifiedEval(this,e)}};i=r.matchAnyOpToken("+","-","*","/","%")}return n}));t.addGrammarElement("mathExpression",(function(e,t,r){return e.parseAnyOf(["mathOperator","unaryExpression"],r)}));function h(e,t,r){if(t["contains"]){return t.contains(r)}else if(t["includes"]){return t.includes(r)}else{throw Error("The value of "+e.sourceFor()+" does not have a contains or includes method on it")}}function v(e,t,r){if(t["match"]){return!!t.match(r)}else if(t["matches"]){return t.matches(r)}else{throw Error("The value of "+e.sourceFor()+" does not have a match or matches method on it")}}t.addGrammarElement("comparisonOperator",(function(e,t,r){var n=e.parseElement("mathExpression",r);var i=r.matchAnyOpToken("<",">","<=",">=","==","===","!=","!==");var a=i?i.value:null;var o=true;var s=false;if(a==null){if(r.matchToken("is")||r.matchToken("am")){if(r.matchToken("not")){if(r.matchToken("in")){a="not in"}else if(r.matchToken("a")){a="not a";s=true}else if(r.matchToken("empty")){a="not empty";o=false}else{a="!="}}else if(r.matchToken("in")){a="in"}else if(r.matchToken("a")){a="a";s=true}else if(r.matchToken("empty")){a="empty";o=false}else if(r.matchToken("less")){r.requireToken("than");if(r.matchToken("or")){r.requireToken("equal");r.requireToken("to");a="<="}else{a="<"}}else if(r.matchToken("greater")){r.requireToken("than");if(r.matchToken("or")){r.requireToken("equal");r.requireToken("to");a=">="}else{a=">"}}else{a="=="}}else if(r.matchToken("exist")||r.matchToken("exists")){a="exist";o=false}else if(r.matchToken("matches")||r.matchToken("match")){a="match"}else if(r.matchToken("contains")||r.matchToken("contain")){a="contain"}else if(r.matchToken("includes")||r.matchToken("include")){a="include"}else if(r.matchToken("do")||r.matchToken("does")){r.requireToken("not");if(r.matchToken("matches")||r.matchToken("match")){a="not match"}else if(r.matchToken("contains")||r.matchToken("contain")){a="not contain"}else if(r.matchToken("exist")||r.matchToken("exist")){a="not exist";o=false}else if(r.matchToken("include")){a="not include"}else{e.raiseParseError(r,"Expected matches or contains")}}}if(a){var u,l,c;if(s){u=r.requireTokenType("IDENTIFIER");l=!r.matchOpToken("!")}else if(o){c=e.requireElement("mathExpression",r);if(a==="match"||a==="not match"){c=c.css?c.css:c}}var f=n;n={type:"comparisonOperator",operator:a,typeName:u,nullOk:l,lhs:n,rhs:c,args:[n,c],op:function(e,r,n){if(a==="=="){return r==n}else if(a==="!="){return r!=n}if(a==="match"){return r!=null&&v(f,r,n)}if(a==="not match"){return r==null||!v(f,r,n)}if(a==="in"){return n!=null&&h(c,n,r)}if(a==="not in"){return n==null||!h(c,n,r)}if(a==="contain"){return r!=null&&h(f,r,n)}if(a==="not contain"){return r==null||!h(f,r,n)}if(a==="include"){return r!=null&&h(f,r,n)}if(a==="not include"){return r==null||!h(f,r,n)}if(a==="==="){return r===n}else if(a==="!=="){return r!==n}else if(a==="<"){return r<n}else if(a===">"){return r>n}else if(a==="<="){return r<=n}else if(a===">="){return r>=n}else if(a==="empty"){return t.isEmpty(r)}else if(a==="not empty"){return!t.isEmpty(r)}else if(a==="exist"){return t.doesExist(r)}else if(a==="not exist"){return!t.doesExist(r)}else if(a==="a"){return t.typeCheck(r,u.value,l)}else if(a==="not a"){return!t.typeCheck(r,u.value,l)}else{throw"Unknown comparison : "+a}},evaluate:function(e){return t.unifiedEval(this,e)}}}return n}));t.addGrammarElement("comparisonExpression",(function(e,t,r){return e.parseAnyOf(["comparisonOperator","mathExpression"],r)}));t.addGrammarElement("logicalOperator",(function(e,t,r){var n=e.parseElement("comparisonExpression",r);var i,a=null;i=r.matchToken("and")||r.matchToken("or");while(i){a=a||i;if(a.value!==i.value){e.raiseParseError(r,"You must parenthesize logical operations with different operators")}var o=e.requireElement("comparisonExpression",r);const s=i.value;n={type:"logicalOperator",operator:s,lhs:n,rhs:o,args:[n,o],op:function(e,t,r){if(s==="and"){return t&&r}else{return t||r}},evaluate:function(e){return t.unifiedEval(this,e)}};i=r.matchToken("and")||r.matchToken("or")}return n}));t.addGrammarElement("logicalExpression",(function(e,t,r){return e.parseAnyOf(["logicalOperator","mathExpression"],r)}));t.addGrammarElement("asyncExpression",(function(e,t,r){if(r.matchToken("async")){var n=e.requireElement("logicalExpression",r);var i={type:"asyncExpression",value:n,evaluate:function(e){return{asyncWrapper:true,value:this.value.evaluate(e)}}};return i}else{return e.parseElement("logicalExpression",r)}}));t.addGrammarElement("expression",(function(e,t,r){r.matchToken("the");return e.parseElement("asyncExpression",r)}));t.addGrammarElement("assignableExpression",(function(e,t,r){r.matchToken("the");var n=e.parseElement("primaryExpression",r);if(n&&(n.type==="symbol"||n.type==="ofExpression"||n.type==="propertyAccess"||n.type==="attributeRefAccess"||n.type==="attributeRef"||n.type==="styleRef"||n.type==="arrayIndex"||n.type==="possessive")){return n}else{e.raiseParseError(r,"A target expression must be writable.  The expression type '"+(n&&n.type)+"' is not.")}return n}));t.addGrammarElement("hyperscript",(function(e,t,r){var n=[];if(r.hasMore()){while(e.featureStart(r.currentToken())||r.currentToken().value==="("){var i=e.requireElement("feature",r);n.push(i);r.matchToken("end")}}return{type:"hyperscript",features:n,apply:function(e,t,r){for(const i of n){i.install(e,t,r)}}}}));var d=function(e){var t=[];if(e.token(0).value==="("&&(e.token(1).value===")"||e.token(2).value===","||e.token(2).value===")")){e.matchOpToken("(");do{t.push(e.requireTokenType("IDENTIFIER"))}while(e.matchOpToken(","));e.requireOpToken(")")}return t};t.addFeature("on",(function(e,t,r){if(!r.matchToken("on"))return;var n=false;if(r.matchToken("every")){n=true}var i=[];var a=null;do{var o=e.requireElement("eventName",r,"Expected event name");var s=o.evaluate();if(a){a=a+" or "+s}else{a="on "+s}var u=d(r);var l=null;if(r.matchOpToken("[")){l=e.requireElement("expression",r);r.requireOpToken("]")}var c,f,m;if(r.currentToken().type==="NUMBER"){var p=r.consumeToken();if(!p.value)return;c=parseInt(p.value);if(r.matchToken("to")){var h=r.consumeToken();if(!h.value)return;f=parseInt(h.value)}else if(r.matchToken("and")){m=true;r.requireToken("on")}}var v,E;if(s==="intersection"){v={};if(r.matchToken("with")){v["with"]=e.requireElement("expression",r).evaluate()}if(r.matchToken("having")){do{if(r.matchToken("margin")){v["rootMargin"]=e.requireElement("stringLike",r).evaluate()}else if(r.matchToken("threshold")){v["threshold"]=e.requireElement("expression",r).evaluate()}else{e.raiseParseError(r,"Unknown intersection config specification")}}while(r.matchToken("and"))}}else if(s==="mutation"){E={};if(r.matchToken("of")){do{if(r.matchToken("anything")){E["attributes"]=true;E["subtree"]=true;E["characterData"]=true;E["childList"]=true}else if(r.matchToken("childList")){E["childList"]=true}else if(r.matchToken("attributes")){E["attributes"]=true;E["attributeOldValue"]=true}else if(r.matchToken("subtree")){E["subtree"]=true}else if(r.matchToken("characterData")){E["characterData"]=true;E["characterDataOldValue"]=true}else if(r.currentToken().type==="ATTRIBUTE_REF"){var T=r.consumeToken();if(E["attributeFilter"]==null){E["attributeFilter"]=[]}if(T.value.indexOf("@")==0){E["attributeFilter"].push(T.value.substring(1))}else{e.raiseParseError(r,"Only shorthand attribute references are allowed here")}}else{e.raiseParseError(r,"Unknown mutation config specification")}}while(r.matchToken("or"))}else{E["attributes"]=true;E["characterData"]=true;E["childList"]=true}}var y=null;var k=false;if(r.matchToken("from")){if(r.matchToken("elsewhere")){k=true}else{y=e.parseElement("expression",r);if(!y){e.raiseParseError(r,'Expected either target value or "elsewhere".')}}}if(y===null&&k===false&&r.matchToken("elsewhere")){k=true}if(r.matchToken("in")){var x=e.parseElement("unaryExpression",r)}if(r.matchToken("debounced")){r.requireToken("at");var g=e.requireElement("expression",r);var b=g.evaluate({})}else if(r.matchToken("throttled")){r.requireToken("at");var g=e.requireElement("expression",r);var w=g.evaluate({})}i.push({execCount:0,every:n,on:s,args:u,filter:l,from:y,inExpr:x,elsewhere:k,startCount:c,endCount:f,unbounded:m,debounceTime:b,throttleTime:w,mutationSpec:E,intersectionSpec:v,debounced:undefined,lastExec:undefined})}while(r.matchToken("or"));var S=true;if(!n){if(r.matchToken("queue")){if(r.matchToken("all")){var q=true;var S=false}else if(r.matchToken("first")){var N=true}else if(r.matchToken("none")){var I=true}else{r.requireToken("last")}}}var R=e.requireElement("commandList",r);e.ensureTerminated(R);var C,A;if(r.matchToken("catch")){C=r.requireTokenType("IDENTIFIER").value;A=e.requireElement("commandList",r);e.ensureTerminated(A)}if(r.matchToken("finally")){var O=e.requireElement("commandList",r);e.ensureTerminated(O)}var L={displayName:a,events:i,start:R,every:n,execCount:0,errorHandler:A,errorSymbol:C,execute:function(e){let r=t.getEventQueueFor(e.me,L);if(r.executing&&n===false){if(I||N&&r.queue.length>0){return}if(S){r.queue.length=0}r.queue.push(e);return}L.execCount++;r.executing=true;e.meta.onHalt=function(){r.executing=false;var e=r.queue.shift();if(e){setTimeout((function(){L.execute(e)}),1)}};e.meta.reject=function(r){console.error(r.message?r.message:r);var n=t.getHyperTrace(e,r);if(n){n.print()}t.triggerEvent(e.me,"exception",{error:r})};R.execute(e)},install:function(e,r){for(const r of L.events){var n;if(r.elsewhere){n=[document]}else if(r.from){n=r.from.evaluate(t.makeContext(e,L,e,null))}else{n=[e]}t.implicitLoop(n,(function(n){var i=r.on;if(n==null){console.warn("'%s' feature ignored because target does not exists:",a,e);return}if(r.mutationSpec){i="hyperscript:mutation";const e=new MutationObserver((function(e,r){if(!L.executing){t.triggerEvent(n,i,{mutationList:e,observer:r})}}));e.observe(n,r.mutationSpec)}if(r.intersectionSpec){i="hyperscript:insersection";const e=new IntersectionObserver((function(r){for(const o of r){var a={observer:e};a=Object.assign(a,o);a["intersecting"]=o.isIntersecting;t.triggerEvent(n,i,a)}}),r.intersectionSpec);e.observe(n)}var o=n.addEventListener||n.on;o.call(n,i,(function a(o){if(typeof Node!=="undefined"&&e instanceof Node&&n!==e&&!e.isConnected){n.removeEventListener(i,a);return}var s=t.makeContext(e,L,e,o);if(r.elsewhere&&e.contains(o.target)){return}if(r.from){s.result=n}for(const e of r.args){let t=s.event[e.value];if(t!==undefined){s.locals[e.value]=t}else if("detail"in s.event){s.locals[e.value]=s.event["detail"][e.value]}}s.meta.errorHandler=A;s.meta.errorSymbol=C;s.meta.finallyHandler=O;if(r.filter){var u=s.meta.context;s.meta.context=s.event;try{var l=r.filter.evaluate(s);if(l){}else{return}}finally{s.meta.context=u}}if(r.inExpr){var c=o.target;while(true){if(c.matches&&c.matches(r.inExpr.css)){s.result=c;break}else{c=c.parentElement;if(c==null){return}}}}r.execCount++;if(r.startCount){if(r.endCount){if(r.execCount<r.startCount||r.execCount>r.endCount){return}}else if(r.unbounded){if(r.execCount<r.startCount){return}}else if(r.execCount!==r.startCount){return}}if(r.debounceTime){if(r.debounced){clearTimeout(r.debounced)}r.debounced=setTimeout((function(){L.execute(s)}),r.debounceTime);return}if(r.throttleTime){if(r.lastExec&&Date.now()<r.lastExec+r.throttleTime){return}else{r.lastExec=Date.now()}}L.execute(s)}))}))}}};e.setParent(R,L);return L}));t.addFeature("def",(function(e,t,r){if(!r.matchToken("def"))return;var n=e.requireElement("dotOrColonPath",r);var i=n.evaluate();var a=i.split(".");var o=a.pop();var s=[];if(r.matchOpToken("(")){if(r.matchOpToken(")")){}else{do{s.push(r.requireTokenType("IDENTIFIER"))}while(r.matchOpToken(","));r.requireOpToken(")")}}var u=e.requireElement("commandList",r);var l,c;if(r.matchToken("catch")){l=r.requireTokenType("IDENTIFIER").value;c=e.parseElement("commandList",r)}if(r.matchToken("finally")){var f=e.requireElement("commandList",r);e.ensureTerminated(f)}var m={displayName:o+"("+s.map((function(e){return e.value})).join(", ")+")",name:o,args:s,start:u,errorHandler:c,errorSymbol:l,finallyHandler:f,install:function(e,r){var n=function(){var n=t.makeContext(r,m,e,null);n.meta.errorHandler=c;n.meta.errorSymbol=l;n.meta.finallyHandler=f;for(var i=0;i<s.length;i++){var a=s[i];var o=arguments[i];if(a){n.locals[a.value]=o}}n.meta.caller=arguments[s.length];if(n.meta.caller){n.meta.callingCommand=n.meta.caller.meta.command}var p,h=null;var v=new Promise((function(e,t){p=e;h=t}));u.execute(n);if(n.meta.returned){return n.meta.returnValue}else{n.meta.resolve=p;n.meta.reject=h;return v}};n.hyperfunc=true;n.hypername=i;t.assignToNamespace(e,a,o,n)}};e.ensureTerminated(u);if(c){e.ensureTerminated(c)}e.setParent(u,m);return m}));t.addFeature("set",(function(e,t,r){let n=e.parseElement("setCommand",r);if(n){if(n.target.scope!=="element"){e.raiseParseError(r,"variables declared at the feature level must be element scoped.")}let i={start:n,install:function(e,r){n&&n.execute(t.makeContext(e,i,e,null))}};e.ensureTerminated(n);return i}}));t.addFeature("init",(function(e,t,r){if(!r.matchToken("init"))return;var n=r.matchToken("immediately");var i=e.requireElement("commandList",r);var a={start:i,install:function(e,r){let o=function(){i&&i.execute(t.makeContext(e,a,e,null))};if(n){o()}else{setTimeout(o,0)}}};e.ensureTerminated(i);e.setParent(i,a);return a}));t.addFeature("worker",(function(e,t,r){if(r.matchToken("worker")){e.raiseParseError(r,"In order to use the 'worker' feature, include "+"the _hyperscript worker plugin. See "+"https://hyperscript.org/features/worker/ for "+"more info.");return undefined}}));t.addFeature("behavior",(function(t,r,n){if(!n.matchToken("behavior"))return;var i=t.requireElement("dotOrColonPath",n).evaluate();var a=i.split(".");var o=a.pop();var s=[];if(n.matchOpToken("(")&&!n.matchOpToken(")")){do{s.push(n.requireTokenType("IDENTIFIER").value)}while(n.matchOpToken(","));n.requireOpToken(")")}var u=t.requireElement("hyperscript",n);for(var l=0;l<u.features.length;l++){var f=u.features[l];f.behavior=i}return{install:function(t,n){r.assignToNamespace(e.document&&e.document.body,a,o,(function(e,t,n){var a=r.getInternalData(e);var o=c(a,i+"Scope");for(var l=0;l<s.length;l++){o[s[l]]=n[s[l]]}u.apply(e,t)}))}}}));t.addFeature("install",(function(t,r,n){if(!n.matchToken("install"))return;var i=t.requireElement("dotOrColonPath",n).evaluate();var a=i.split(".");var o=t.parseElement("namedArgumentList",n);var s;return s={install:function(t,n){r.unifiedEval({args:[o],op:function(r,o){var s=e;for(var u=0;u<a.length;u++){s=s[a[u]];if(typeof s!=="object"&&typeof s!=="function")throw new Error("No such behavior defined as "+i)}if(!(s instanceof Function))throw new Error(i+" is not a behavior");s(t,n,o)}},r.makeContext(t,s,t,null))}}}));t.addGrammarElement("jsBody",(function(e,t,r){var n=r.currentToken().start;var i=r.currentToken();var a=[];var o="";var s=false;while(r.hasMore()){i=r.consumeToken();var u=r.token(0,true);if(u.type==="IDENTIFIER"&&u.value==="end"){break}if(s){if(i.type==="IDENTIFIER"||i.type==="NUMBER"){o+=i.value}else{if(o!=="")a.push(o);o="";s=false}}else if(i.type==="IDENTIFIER"&&i.value==="function"){s=true}}var l=i.end+1;return{type:"jsBody",exposedFunctionNames:a,jsSource:r.source.substring(n,l)}}));t.addFeature("js",(function(t,r,n){if(!n.matchToken("js"))return;var i=t.requireElement("jsBody",n);var a=i.jsSource+"\nreturn { "+i.exposedFunctionNames.map((function(e){return e+":"+e})).join(",")+" } ";var o=new Function(a);return{jsSource:a,function:o,exposedFunctionNames:i.exposedFunctionNames,install:function(){Object.assign(e,o())}}}));t.addCommand("js",(function(t,r,n){if(!n.matchToken("js"))return;var i=[];if(n.matchOpToken("(")){if(n.matchOpToken(")")){}else{do{var a=n.requireTokenType("IDENTIFIER");i.push(a.value)}while(n.matchOpToken(","));n.requireOpToken(")")}}var o=t.requireElement("jsBody",n);n.matchToken("end");var s=p(Function,i.concat([o.jsSource]));var u={jsSource:o.jsSource,function:s,inputs:i,op:function(t){var n=[];i.forEach((function(e){n.push(r.resolveSymbol(e,t,"default"))}));var a=s.apply(e,n);if(a&&typeof a.then==="function"){return new Promise((function(e){a.then((function(n){t.result=n;e(r.findNext(this,t))}))}))}else{t.result=a;return r.findNext(this,t)}}};return u}));t.addCommand("async",(function(e,t,r){if(!r.matchToken("async"))return;if(r.matchToken("do")){var n=e.requireElement("commandList",r);var i=n;while(i.next)i=i.next;i.next=t.HALT;r.requireToken("end")}else{var n=e.requireElement("command",r)}var a={body:n,op:function(e){setTimeout((function(){n.execute(e)}));return t.findNext(this,e)}};e.setParent(n,a);return a}));t.addCommand("tell",(function(e,t,r){var n=r.currentToken();if(!r.matchToken("tell"))return;var i=e.requireElement("expression",r);var a=e.requireElement("commandList",r);if(r.hasMore()&&!e.featureStart(r.currentToken())){r.requireToken("end")}var o="tell_"+n.start;var s={value:i,body:a,args:[i],resolveNext:function(e){var r=e.meta.iterators[o];if(r.index<r.value.length){e.you=r.value[r.index++];return a}else{e.you=r.originalYou;if(this.next){return this.next}else{return t.findNext(this.parent,e)}}},op:function(e,t){if(t==null){t=[]}else if(!(Array.isArray(t)||t instanceof NodeList)){t=[t]}e.meta.iterators[o]={originalYou:e.you,index:0,value:t};return this.resolveNext(e)}};e.setParent(a,s);return s}));t.addCommand("wait",(function(e,t,r){if(!r.matchToken("wait"))return;var n;if(r.matchToken("for")){r.matchToken("a");var i=[];do{var a=r.token(0);if(a.type==="NUMBER"||a.type==="L_PAREN"){i.push({time:e.requireElement("expression",r).evaluate()})}else{i.push({name:e.requireElement("dotOrColonPath",r,"Expected event name").evaluate(),args:d(r)})}}while(r.matchToken("or"));if(r.matchToken("from")){var o=e.requireElement("expression",r)}n={event:i,on:o,args:[o],op:function(e,r){var n=r?r:e.me;if(!(n instanceof EventTarget))throw new Error("Not a valid event target: "+this.on.sourceFor());return new Promise((r=>{var a=false;for(const s of i){var o=n=>{e.result=n;if(s.args){for(const t of s.args){e.locals[t.value]=n[t.value]||(n.detail?n.detail[t.value]:null)}}if(!a){a=true;r(t.findNext(this,e))}};if(s.name){n.addEventListener(s.name,o,{once:true})}else if(s.time!=null){setTimeout(o,s.time,s.time)}}}))}};return n}else{var s;if(r.matchToken("a")){r.requireToken("tick");s=0}else{s=e.requireElement("expression",r)}n={type:"waitCmd",time:s,args:[s],op:function(e,r){return new Promise((n=>{setTimeout((()=>{n(t.findNext(this,e))}),r)}))},execute:function(e){return t.unifiedExec(this,e)}};return n}}));t.addGrammarElement("dotOrColonPath",(function(e,t,r){var n=r.matchTokenType("IDENTIFIER");if(n){var i=[n.value];var a=r.matchOpToken(".")||r.matchOpToken(":");if(a){do{i.push(r.requireTokenType("IDENTIFIER","NUMBER").value)}while(r.matchOpToken(a.value))}return{type:"dotOrColonPath",path:i,evaluate:function(){return i.join(a?a.value:"")}}}}));t.addGrammarElement("eventName",(function(e,t,r){var n;if(n=r.matchTokenType("STRING")){return{evaluate:function(){return n.value}}}return e.parseElement("dotOrColonPath",r)}));function E(e,t,r,n){var i=t.requireElement("eventName",n);var a=t.parseElement("namedArgumentList",n);if(e==="send"&&n.matchToken("to")||e==="trigger"&&n.matchToken("on")){var o=t.requireElement("expression",n)}else{var o=t.requireElement("implicitMeTarget",n)}var s={eventName:i,details:a,to:o,args:[o,i,a],op:function(e,t,n,i){r.nullCheck(t,o);r.forEach(t,(function(t){r.triggerEvent(t,n,i,e.me)}));return r.findNext(s,e)}};return s}t.addCommand("trigger",(function(e,t,r){if(r.matchToken("trigger")){return E("trigger",e,t,r)}}));t.addCommand("send",(function(e,t,r){if(r.matchToken("send")){return E("send",e,t,r)}}));var T=function(e,t,r,n){if(n){if(e.commandBoundary(r.currentToken())){e.raiseParseError(r,"'return' commands must return a value.  If you do not wish to return a value, use 'exit' instead.")}else{var i=e.requireElement("expression",r)}}var a={value:i,args:[i],op:function(e,r){var n=e.meta.resolve;e.meta.returned=true;e.meta.returnValue=r;if(n){if(r){n(r)}else{n()}}return t.HALT}};return a};t.addCommand("return",(function(e,t,r){if(r.matchToken("return")){return T(e,t,r,true)}}));t.addCommand("exit",(function(e,t,r){if(r.matchToken("exit")){return T(e,t,r,false)}}));t.addCommand("halt",(function(e,t,r){if(r.matchToken("halt")){if(r.matchToken("the")){r.requireToken("event");if(r.matchOpToken("'")){r.requireToken("s")}var n=true}if(r.matchToken("bubbling")){var i=true}else if(r.matchToken("default")){var a=true}var o=T(e,t,r,false);var s={keepExecuting:true,bubbling:i,haltDefault:a,exit:o,op:function(e){if(e.event){if(i){e.event.stopPropagation()}else if(a){e.event.preventDefault()}else{e.event.stopPropagation();e.event.preventDefault()}if(n){return t.findNext(this,e)}else{return o}}}};return s}}));t.addCommand("log",(function(e,t,r){if(!r.matchToken("log"))return;var n=[e.parseElement("expression",r)];while(r.matchOpToken(",")){n.push(e.requireElement("expression",r))}if(r.matchToken("with")){var i=e.requireElement("expression",r)}var a={exprs:n,withExpr:i,args:[i,n],op:function(e,r,n){if(r){r.apply(null,n)}else{console.log.apply(null,n)}return t.findNext(this,e)}};return a}));t.addCommand("throw",(function(e,t,r){if(!r.matchToken("throw"))return;var n=e.requireElement("expression",r);var i={expr:n,args:[n],op:function(e,r){t.registerHyperTrace(e,r);throw r}};return i}));var y=function(e,t,r){var n=e.requireElement("expression",r);var i={expr:n,args:[n],op:function(e,r){e.result=r;return t.findNext(i,e)}};return i};t.addCommand("call",(function(e,t,r){if(!r.matchToken("call"))return;var n=y(e,t,r);if(n.expr&&n.expr.type!=="functionCall"){e.raiseParseError(r,"Must be a function invocation")}return n}));t.addCommand("get",(function(e,t,r){if(r.matchToken("get")){return y(e,t,r)}}));t.addCommand("make",(function(e,t,r){if(!r.matchToken("make"))return;r.matchToken("a")||r.matchToken("an");var n=e.requireElement("expression",r);var i=[];if(n.type!=="queryRef"&&r.matchToken("from")){do{i.push(e.requireElement("expression",r))}while(r.matchOpToken(","))}if(r.matchToken("called")){var a=e.requireElement("symbol",r)}var o;if(n.type==="queryRef"){o={op:function(e){var r,i="div",o,s=[];var u=/(?:(^|#|\.)([^#\. ]+))/g;while(r=u.exec(n.css)){if(r[1]==="")i=r[2].trim();else if(r[1]==="#")o=r[2].trim();else s.push(r[2].trim())}var l=document.createElement(i);if(o!==undefined)l.id=o;for(var c=0;c<s.length;c++){var f=s[c];l.classList.add(f)}e.result=l;if(a){t.setSymbol(a.name,e,a.scope,l)}return t.findNext(this,e)}};return o}else{o={args:[n,i],op:function(e,r,n){e.result=p(r,n);if(a){t.setSymbol(a.name,e,a.scope,e.result)}return t.findNext(this,e)}};return o}}));t.addGrammarElement("pseudoCommand",(function(e,t,r){let n=r.token(1);if(!(n&&n.op&&(n.value==="."||n.value==="("))){return null}var i=e.requireElement("primaryExpression",r);var a=i.root;var o=i;while(a.root!=null){o=o.root;a=a.root}if(i.type!=="functionCall"){e.raiseParseError(r,"Pseudo-commands must be function calls")}if(o.type==="functionCall"&&o.root.root==null){if(r.matchAnyToken("the","to","on","with","into","from","at")){var s=e.requireElement("expression",r)}else if(r.matchToken("me")){var s=e.requireElement("implicitMeTarget",r)}}var u;if(s){u={type:"pseudoCommand",root:s,argExressions:o.argExressions,args:[s,o.argExressions],op:function(e,r,n){t.nullCheck(r,s);var i=r[o.root.name];t.nullCheck(i,o);if(i.hyperfunc){n.push(e)}e.result=i.apply(r,n);return t.findNext(u,e)},execute:function(e){return t.unifiedExec(this,e)}}}else{u={type:"pseudoCommand",expr:i,args:[i],op:function(e,r){e.result=r;return t.findNext(u,e)},execute:function(e){return t.unifiedExec(this,e)}}}return u}));var k=function(e,t,r,n,i){var a=n.type==="symbol";var o=n.type==="attributeRef";var s=n.type==="styleRef";var u=n.type==="arrayIndex";if(!(o||s||a)&&n.root==null){e.raiseParseError(r,"Can only put directly into symbols, not references")}var l=null;var c=null;if(a){}else if(o||s){l=e.requireElement("implicitMeTarget",r);var f=n}else if(u){c=n.firstIndex;l=n.root}else{c=n.prop?n.prop.value:null;var f=n.attribute;l=n.root}var m={target:n,symbolWrite:a,value:i,args:[l,c,i],op:function(e,r,i,o){if(a){t.setSymbol(n.name,e,n.scope,o)}else{t.nullCheck(r,l);if(u){r[i]=o}else{t.implicitLoop(r,(function(e){if(f){if(f.type==="attributeRef"){if(o==null){e.removeAttribute(f.name)}else{e.setAttribute(f.name,o)}}else{e.style[f.name]=o}}else{e[i]=o}}))}}return t.findNext(this,e)}};return m};t.addCommand("default",(function(e,t,r){if(!r.matchToken("default"))return;var n=e.requireElement("assignableExpression",r);r.requireToken("to");var i=e.requireElement("expression",r);var a=k(e,t,r,n,i);var o={target:n,value:i,setter:a,args:[n],op:function(e,r){if(r){return t.findNext(this,e)}else{return a}}};a.parent=o;return o}));t.addCommand("set",(function(e,t,r){if(!r.matchToken("set"))return;if(r.currentToken().type==="L_BRACE"){var n=e.requireElement("objectLiteral",r);r.requireToken("on");var i=e.requireElement("expression",r);var a={objectLiteral:n,target:i,args:[n,i],op:function(e,r,n){Object.assign(n,r);return t.findNext(this,e)}};return a}try{r.pushFollow("to");var i=e.requireElement("assignableExpression",r)}finally{r.popFollow()}r.requireToken("to");var o=e.requireElement("expression",r);return k(e,t,r,i,o)}));t.addCommand("if",(function(e,t,r){if(!r.matchToken("if"))return;var n=e.requireElement("expression",r);r.matchToken("then");var i=e.parseElement("commandList",r);if(r.matchToken("else")||r.matchToken("otherwise")){var a=e.parseElement("commandList",r)}if(r.hasMore()){r.requireToken("end")}var o={expr:n,trueBranch:i,falseBranch:a,args:[n],op:function(e,r){if(r){return i}else if(a){return a}else{return t.findNext(this,e)}}};e.setParent(i,o);e.setParent(a,o);return o}));var x=function(e,t,r,n){var i=t.currentToken();var a;if(t.matchToken("for")||n){var o=t.requireTokenType("IDENTIFIER");a=o.value;t.requireToken("in");var s=e.requireElement("expression",t)}else if(t.matchToken("in")){a="it";var s=e.requireElement("expression",t)}else if(t.matchToken("while")){var u=e.requireElement("expression",t)}else if(t.matchToken("until")){var l=true;if(t.matchToken("event")){var c=e.requireElement("dotOrColonPath",t,"Expected event name");if(t.matchToken("from")){var f=e.requireElement("expression",t)}}else{var u=e.requireElement("expression",t)}}else{if(!e.commandBoundary(t.currentToken())&&t.currentToken().value!=="forever"){var m=e.requireElement("expression",t);t.requireToken("times")}else{t.matchToken("forever");var p=true}}if(t.matchToken("index")){var o=t.requireTokenType("IDENTIFIER");var h=o.value}var v=e.parseElement("commandList",t);if(v&&c){var d=v;while(d.next){d=d.next}var E={type:"waitATick",op:function(){return new Promise((function(e){setTimeout((function(){e(r.findNext(E))}),0)}))}};d.next=E}if(t.hasMore()){t.requireToken("end")}if(a==null){a="_implicit_repeat_"+i.start;var T=a}else{var T=a+"_"+i.start}var y={identifier:a,indexIdentifier:h,slot:T,expression:s,forever:p,times:m,until:l,event:c,on:f,whileExpr:u,resolveNext:function(){return this},loop:v,args:[u,m],op:function(e,t,n){var i=e.meta.iterators[T];var o=false;var s=null;if(this.forever){o=true}else if(this.until){if(c){o=e.meta.iterators[T].eventFired===false}else{o=t!==true}}else if(u){o=t}else if(n){o=i.index<n}else{var l=i.iterator.next();o=!l.done;s=l.value}if(o){if(i.value){e.result=e.locals[a]=s}else{e.result=i.index}if(h){e.locals[h]=i.index}i.index++;return v}else{e.meta.iterators[T]=null;return r.findNext(this.parent,e)}}};e.setParent(v,y);var k={name:"repeatInit",args:[s,c,f],op:function(e,t,r,n){var i={index:0,value:t,eventFired:false};e.meta.iterators[T]=i;if(t&&t[Symbol.iterator]){i.iterator=t[Symbol.iterator]()}if(c){var a=n||e.me;a.addEventListener(r,(function(t){e.meta.iterators[T].eventFired=true}),{once:true})}return y},execute:function(e){return r.unifiedExec(this,e)}};e.setParent(y,k);return k};t.addCommand("repeat",(function(e,t,r){if(r.matchToken("repeat")){return x(e,r,t,false)}}));t.addCommand("for",(function(e,t,r){if(r.matchToken("for")){return x(e,r,t,true)}}));t.addCommand("continue",(function(e,t,r){if(!r.matchToken("continue"))return;var n={op:function(t){for(var n=this.parent;true;n=n.parent){if(n==undefined){e.raiseParseError(r,"Command `continue` cannot be used outside of a `repeat` loop.")}if(n.loop!=undefined){return n.resolveNext(t)}}}};return n}));t.addCommand("break",(function(e,t,r){if(!r.matchToken("break"))return;var n={op:function(n){for(var i=this.parent;true;i=i.parent){if(i==undefined){e.raiseParseError(r,"Command `continue` cannot be used outside of a `repeat` loop.")}if(i.loop!=undefined){return t.findNext(i.parent,n)}}}};return n}));t.addGrammarElement("stringLike",(function(e,t,r){return e.parseAnyOf(["string","nakedString"],r)}));t.addCommand("append",(function(e,t,r){if(!r.matchToken("append"))return;var n=null;var i=e.requireElement("expression",r);var a={type:"symbol",evaluate:function(e){return t.resolveSymbol("result",e)}};if(r.matchToken("to")){n=e.requireElement("expression",r)}else{n=a}var o=null;if(n.type==="symbol"||n.type==="attributeRef"||n.root!=null){o=k(e,t,r,n,a)}var s={value:i,target:n,args:[n,i],op:function(e,r,n){if(Array.isArray(r)){r.push(n);return t.findNext(this,e)}else if(r instanceof Element){r.innerHTML+=n;return t.findNext(this,e)}else if(o){e.result=(r||"")+n;return o}else{throw Error("Unable to append a value!")}},execute:function(e){return t.unifiedExec(this,e)}};if(o!=null){o.parent=s}return s}));function g(e,t,r){r.matchToken("at")||r.matchToken("from");const n={includeStart:true,includeEnd:false};n.from=r.matchToken("start")?0:e.requireElement("expression",r);if(r.matchToken("to")||r.matchOpToken("..")){if(r.matchToken("end")){n.toEnd=true}else{n.to=e.requireElement("expression",r)}}if(r.matchToken("inclusive"))n.includeEnd=true;else if(r.matchToken("exclusive"))n.includeStart=false;return n}class b{constructor(e,t){this.re=e;this.str=t}next(){const e=this.re.exec(this.str);if(e===null)return{done:true};else return{value:e}}}class w{constructor(e,t,r){this.re=e;this.flags=t;this.str=r}[Symbol.iterator](){return new b(new RegExp(this.re,this.flags),this.str)}}t.addCommand("pick",((e,t,r)=>{if(!r.matchToken("pick"))return;r.matchToken("the");if(r.matchToken("item")||r.matchToken("items")||r.matchToken("character")||r.matchToken("characters")){const n=g(e,t,r);r.requireToken("from");const i=e.requireElement("expression",r);return{args:[i,n.from,n.to],op(e,r,i,a){if(n.toEnd)a=r.length;if(!n.includeStart)i++;if(n.includeEnd)a++;if(a==null||a==undefined)a=i+1;e.result=r.slice(i,a);return t.findNext(this,e)}}}if(r.matchToken("match")){r.matchToken("of");const n=e.parseElement("expression",r);let i="";if(r.matchOpToken("|")){i=r.requireToken("identifier").value}r.requireToken("from");const a=e.parseElement("expression",r);return{args:[a,n],op(e,r,n){e.result=new RegExp(n,i).exec(r);return t.findNext(this,e)}}}if(r.matchToken("matches")){r.matchToken("of");const n=e.parseElement("expression",r);let i="gu";if(r.matchOpToken("|")){i="g"+r.requireToken("identifier").value.replace("g","")}console.log("flags",i);r.requireToken("from");const a=e.parseElement("expression",r);return{args:[a,n],op(e,r,n){e.result=new w(n,i,r);return t.findNext(this,e)}}}}));t.addCommand("increment",(function(e,t,r){if(!r.matchToken("increment"))return;var n;var i=e.parseElement("assignableExpression",r);if(r.matchToken("by")){n=e.requireElement("expression",r)}var a={type:"implicitIncrementOp",target:i,args:[i,n],op:function(e,t,r){t=t?parseFloat(t):0;r=n?parseFloat(r):1;var i=t+r;e.result=i;return i},evaluate:function(e){return t.unifiedEval(this,e)}};return k(e,t,r,i,a)}));t.addCommand("decrement",(function(e,t,r){if(!r.matchToken("decrement"))return;var n;var i=e.parseElement("assignableExpression",r);if(r.matchToken("by")){n=e.requireElement("expression",r)}var a={type:"implicitDecrementOp",target:i,args:[i,n],op:function(e,t,r){t=t?parseFloat(t):0;r=n?parseFloat(r):1;var i=t-r;e.result=i;return i},evaluate:function(e){return t.unifiedEval(this,e)}};return k(e,t,r,i,a)}));function S(e,t){var r="text";var n;e.matchToken("a")||e.matchToken("an");if(e.matchToken("json")||e.matchToken("Object")){r="json"}else if(e.matchToken("response")){r="response"}else if(e.matchToken("html")){r="html"}else if(e.matchToken("text")){}else{n=t.requireElement("dotOrColonPath",e).evaluate()}return{type:r,conversion:n}}t.addCommand("fetch",(function(e,t,r){if(!r.matchToken("fetch"))return;var n=e.requireElement("stringLike",r);if(r.matchToken("as")){var i=S(r,e)}if(r.matchToken("with")&&r.currentToken().value!=="{"){var a=e.parseElement("nakedNamedArgumentList",r)}else{var a=e.parseElement("objectLiteral",r)}if(i==null&&r.matchToken("as")){i=S(r,e)}var o=i?i.type:"text";var s=i?i.conversion:null;var u={url:n,argExpressions:a,args:[n,a],op:function(e,r,n){var i=n||{};i["sender"]=e.me;i["headers"]=i["headers"]||{};var a=new AbortController;let l=e.me.addEventListener("fetch:abort",(function(){a.abort()}),{once:true});i["signal"]=a.signal;t.triggerEvent(e.me,"hyperscript:beforeFetch",i);t.triggerEvent(e.me,"fetch:beforeRequest",i);n=i;var c=false;if(n.timeout){setTimeout((function(){if(!c){a.abort()}}),n.timeout)}return fetch(r,n).then((function(r){let n={response:r};t.triggerEvent(e.me,"fetch:afterResponse",n);r=n.response;if(o==="response"){e.result=r;t.triggerEvent(e.me,"fetch:afterRequest",{result:r});c=true;return t.findNext(u,e)}if(o==="json"){return r.json().then((function(r){e.result=r;t.triggerEvent(e.me,"fetch:afterRequest",{result:r});c=true;return t.findNext(u,e)}))}return r.text().then((function(r){if(s)r=t.convertValue(r,s);if(o==="html")r=t.convertValue(r,"Fragment");e.result=r;t.triggerEvent(e.me,"fetch:afterRequest",{result:r});c=true;return t.findNext(u,e)}))})).catch((function(r){t.triggerEvent(e.me,"fetch:error",{reason:r});throw r})).finally((function(){e.me.removeEventListener("fetch:abort",l)}))}};return u}))}function v(e){e.addCommand("settle",(function(e,t,r){if(r.matchToken("settle")){if(!e.commandBoundary(r.currentToken())){var n=e.requireElement("expression",r)}else{var n=e.requireElement("implicitMeTarget",r)}var i={type:"settleCmd",args:[n],op:function(e,r){t.nullCheck(r,n);var a=null;var o=false;var s=false;var u=new Promise((function(e){a=e}));r.addEventListener("transitionstart",(function(){s=true}),{once:true});setTimeout((function(){if(!s&&!o){a(t.findNext(i,e))}}),500);r.addEventListener("transitionend",(function(){if(!o){a(t.findNext(i,e))}}),{once:true});return u},execute:function(e){return t.unifiedExec(this,e)}};return i}}));e.addCommand("add",(function(e,t,r){if(r.matchToken("add")){var n=e.parseElement("classRef",r);var i=null;var a=null;if(n==null){i=e.parseElement("attributeRef",r);if(i==null){a=e.parseElement("styleLiteral",r);if(a==null){e.raiseParseError(r,"Expected either a class reference or attribute expression")}}}else{var o=[n];while(n=e.parseElement("classRef",r)){o.push(n)}}if(r.matchToken("to")){var s=e.requireElement("expression",r)}else{var s=e.requireElement("implicitMeTarget",r)}if(r.matchToken("when")){if(a){e.raiseParseError(r,"Only class and properties are supported with a when clause")}var u=e.requireElement("expression",r)}if(o){return{classRefs:o,to:s,args:[s,o],op:function(e,r,n){t.nullCheck(r,s);t.forEach(n,(function(n){t.implicitLoop(r,(function(r){if(u){e.result=r;let i=t.evaluateNoPromise(u,e);if(i){if(r instanceof Element)r.classList.add(n.className)}else{if(r instanceof Element)r.classList.remove(n.className)}e.result=null}else{if(r instanceof Element)r.classList.add(n.className)}}))}));return t.findNext(this,e)}}}else if(i){return{type:"addCmd",attributeRef:i,to:s,args:[s],op:function(e,r,n){t.nullCheck(r,s);t.implicitLoop(r,(function(r){if(u){e.result=r;let n=t.evaluateNoPromise(u,e);if(n){r.setAttribute(i.name,i.value)}else{r.removeAttribute(i.name)}e.result=null}else{r.setAttribute(i.name,i.value)}}));return t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}}else{return{type:"addCmd",cssDeclaration:a,to:s,args:[s,a],op:function(e,r,n){t.nullCheck(r,s);t.implicitLoop(r,(function(e){e.style.cssText+=n}));return t.findNext(this,e)},execute:function(e){return t.unifiedExec(this,e)}}}}}));e.addGrammarElement("styleLiteral",(function(e,t,r){if(!r.matchOpToken("{"))return;var n=[""];var i=[];while(r.hasMore()){if(r.matchOpToken("\\")){r.consumeToken()}else if(r.matchOpToken("}")){break}else if(r.matchToken("$")){var a=r.matchOpToken("{");var o=e.parseElement("expression",r);if(a)r.requireOpToken("}");i.push(o);n.push("")}else{var s=r.consumeToken();n[n.length-1]+=r.source.substring(s.start,s.end)}n[n.length-1]+=r.lastWhitespace()}return{type:"styleLiteral",args:[i],op:function(e,t){var r="";n.forEach((function(e,n){r+=e;if(n in t)r+=t[n]}));return r},evaluate:function(e){return t.unifiedEval(this,e)}}}));e.addCommand("remove",(function(e,t,r){if(r.matchToken("remove")){var n=e.parseElement("classRef",r);var i=null;var a=null;if(n==null){i=e.parseElement("attributeRef",r);if(i==null){a=e.parseElement("expression",r);if(a==null){e.raiseParseError(r,"Expected either a class reference, attribute expression or value expression")}}}else{var o=[n];while(n=e.parseElement("classRef",r)){o.push(n)}}if(r.matchToken("from")){var s=e.requireElement("expression",r)}else{if(a==null){var s=e.requireElement("implicitMeTarget",r)}}if(a){return{elementExpr:a,from:s,args:[a,s],op:function(e,r,n){t.nullCheck(r,a);t.implicitLoop(r,(function(e){if(e.parentElement&&(n==null||n.contains(e))){e.parentElement.removeChild(e)}}));return t.findNext(this,e)}}}else{return{classRefs:o,attributeRef:i,elementExpr:a,from:s,args:[o,s],op:function(e,r,n){t.nullCheck(n,s);if(r){t.forEach(r,(function(e){t.implicitLoop(n,(function(t){t.classList.remove(e.className)}))}))}else{t.implicitLoop(n,(function(e){e.removeAttribute(i.name)}))}return t.findNext(this,e)}}}}}));e.addCommand("toggle",(function(e,t,r){if(r.matchToken("toggle")){r.matchAnyToken("the","my");if(r.currentToken().type==="STYLE_REF"){let t=r.consumeToken();var n=t.value.substr(1);var a=true;var o=i(e,r,n);if(r.matchToken("of")){r.pushFollow("with");try{var s=e.requireElement("expression",r)}finally{r.popFollow()}}else{var s=e.requireElement("implicitMeTarget",r)}}else if(r.matchToken("between")){var u=true;var l=e.parseElement("classRef",r);r.requireToken("and");var c=e.requireElement("classRef",r)}else{var l=e.parseElement("classRef",r);var f=null;if(l==null){f=e.parseElement("attributeRef",r);if(f==null){e.raiseParseError(r,"Expected either a class reference or attribute expression")}}else{var m=[l];while(l=e.parseElement("classRef",r)){m.push(l)}}}if(a!==true){if(r.matchToken("on")){var s=e.requireElement("expression",r)}else{var s=e.requireElement("implicitMeTarget",r)}}if(r.matchToken("for")){var p=e.requireElement("expression",r)}else if(r.matchToken("until")){var h=e.requireElement("dotOrColonPath",r,"Expected event name");if(r.matchToken("from")){var v=e.requireElement("expression",r)}}var d={classRef:l,classRef2:c,classRefs:m,attributeRef:f,on:s,time:p,evt:h,from:v,toggle:function(e,r,n,i){t.nullCheck(e,s);if(a){t.implicitLoop(e,(function(e){o("toggle",e)}))}else if(u){t.implicitLoop(e,(function(e){if(e.classList.contains(r.className)){e.classList.remove(r.className);e.classList.add(n.className)}else{e.classList.add(r.className);e.classList.remove(n.className)}}))}else if(i){t.forEach(i,(function(r){t.implicitLoop(e,(function(e){e.classList.toggle(r.className)}))}))}else{t.forEach(e,(function(e){if(e.hasAttribute(f.name)){e.removeAttribute(f.name)}else{e.setAttribute(f.name,f.value)}}))}},args:[s,p,h,v,l,c,m],op:function(e,r,n,i,a,o,s,u){if(n){return new Promise((function(i){d.toggle(r,o,s,u);setTimeout((function(){d.toggle(r,o,s,u);i(t.findNext(d,e))}),n)}))}else if(i){return new Promise((function(n){var l=a||e.me;l.addEventListener(i,(function(){d.toggle(r,o,s,u);n(t.findNext(d,e))}),{once:true});d.toggle(r,o,s,u)}))}else{this.toggle(r,o,s,u);return t.findNext(d,e)}}};return d}}));var t={display:function(r,n,i){if(i){n.style.display=i}else if(r==="toggle"){if(getComputedStyle(n).display==="none"){t.display("show",n,i)}else{t.display("hide",n,i)}}else if(r==="hide"){const t=e.runtime.getInternalData(n);if(t.originalDisplay==null){t.originalDisplay=n.style.display}n.style.display="none"}else{const t=e.runtime.getInternalData(n);if(t.originalDisplay&&t.originalDisplay!=="none"){n.style.display=t.originalDisplay}else{n.style.removeProperty("display")}}},visibility:function(e,r,n){if(n){r.style.visibility=n}else if(e==="toggle"){if(getComputedStyle(r).visibility==="hidden"){t.visibility("show",r,n)}else{t.visibility("hide",r,n)}}else if(e==="hide"){r.style.visibility="hidden"}else{r.style.visibility="visible"}},opacity:function(e,r,n){if(n){r.style.opacity=n}else if(e==="toggle"){if(getComputedStyle(r).opacity==="0"){t.opacity("show",r,n)}else{t.opacity("hide",r,n)}}else if(e==="hide"){r.style.opacity="0"}else{r.style.opacity="1"}}};var n=function(e,t,r){var n;var i=r.currentToken();if(i.value==="when"||i.value==="with"||e.commandBoundary(i)){n=e.parseElement("implicitMeTarget",r)}else{n=e.parseElement("expression",r)}return n};var i=function(e,n,i){var a=r.defaultHideShowStrategy;var o=t;if(r.hideShowStrategies){o=Object.assign(o,r.hideShowStrategies)}i=i||a||"display";var s=o[i];if(s==null){e.raiseParseError(n,"Unknown show/hide strategy : "+i)}return s};e.addCommand("hide",(function(e,t,r){if(r.matchToken("hide")){var a=n(e,t,r);var o=null;if(r.matchToken("with")){o=r.requireTokenType("IDENTIFIER","STYLE_REF").value;if(o.indexOf("*")===0){o=o.substr(1)}}var s=i(e,r,o);return{target:a,args:[a],op:function(e,r){t.nullCheck(r,a);t.implicitLoop(r,(function(e){s("hide",e)}));return t.findNext(this,e)}}}}));e.addCommand("show",(function(e,t,r){if(r.matchToken("show")){var a=n(e,t,r);var o=null;if(r.matchToken("with")){o=r.requireTokenType("IDENTIFIER","STYLE_REF").value;if(o.indexOf("*")===0){o=o.substr(1)}}var s=null;if(r.matchOpToken(":")){var u=r.consumeUntilWhitespace();r.matchTokenType("WHITESPACE");s=u.map((function(e){return e.value})).join("")}if(r.matchToken("when")){var l=e.requireElement("expression",r)}var c=i(e,r,o);return{target:a,when:l,args:[a],op:function(e,r){t.nullCheck(r,a);t.implicitLoop(r,(function(r){if(l){e.result=r;let n=t.evaluateNoPromise(l,e);if(n){c("show",r,s)}else{c("hide",r)}e.result=null}else{c("show",r,s)}}));return t.findNext(this,e)}}}}));e.addCommand("take",(function(e,t,r){if(r.matchToken("take")){var n=e.requireElement("classRef",r);if(r.matchToken("from")){var i=e.requireElement("expression",r)}else{var i=n}if(r.matchToken("for")){var a=e.requireElement("expression",r)}else{var a=e.requireElement("implicitMeTarget",r)}var o={classRef:n,from:i,forElt:a,args:[n,i,a],op:function(e,r,n,o){t.nullCheck(n,i);t.nullCheck(o,a);var s=r.className;t.implicitLoop(n,(function(e){e.classList.remove(s)}));t.implicitLoop(o,(function(e){e.classList.add(s)}));return t.findNext(this,e)}};return o}}));function a(t,r,n,i){if(n!=null){var a=t.resolveSymbol(n,r)}else{var a=r}if(a instanceof Element||a instanceof HTMLDocument){while(a.firstChild)a.removeChild(a.firstChild);a.append(e.runtime.convertValue(i,"Fragment"));t.processNode(a)}else{if(n!=null){t.setSymbol(n,r,null,i)}else{throw"Don't know how to put a value into "+typeof r}}}e.addCommand("put",(function(e,t,r){if(r.matchToken("put")){var n=e.requireElement("expression",r);var i=r.matchAnyToken("into","before","after");if(i==null&&r.matchToken("at")){r.matchToken("the");i=r.matchAnyToken("start","end");r.requireToken("of")}if(i==null){e.raiseParseError(r,"Expected one of 'into', 'before', 'at start of', 'at end of', 'after'")}var o=e.requireElement("expression",r);var s=i.value;var u=false;var l=false;var c=null;var f=null;if(o.type==="arrayIndex"&&s==="into"){u=true;f=o.prop;c=o.root}else if(o.prop&&o.root&&s==="into"){f=o.prop.value;c=o.root}else if(o.type==="symbol"&&s==="into"){l=true;f=o.name}else if(o.type==="attributeRef"&&s==="into"){var m=true;f=o.name;c=e.requireElement("implicitMeTarget",r)}else if(o.type==="styleRef"&&s==="into"){var p=true;f=o.name;c=e.requireElement("implicitMeTarget",r)}else if(o.attribute&&s==="into"){var m=o.attribute.type==="attributeRef";var p=o.attribute.type==="styleRef";f=o.attribute.name;c=o.root}else{c=o}var h={target:o,operation:s,symbolWrite:l,value:n,args:[c,f,n],op:function(e,r,n,i){if(l){a(t,e,n,i)}else{t.nullCheck(r,c);if(s==="into"){if(m){t.implicitLoop(r,(function(e){e.setAttribute(n,i)}))}else if(p){t.implicitLoop(r,(function(e){e.style[n]=i}))}else if(u){r[n]=i}else{t.implicitLoop(r,(function(e){a(t,e,n,i)}))}}else{var o=s==="before"?Element.prototype.before:s==="after"?Element.prototype.after:s==="start"?Element.prototype.prepend:s==="end"?Element.prototype.append:Element.prototype.append;t.implicitLoop(r,(function(e){o.call(e,i instanceof Node?i:t.convertValue(i,"Fragment"));if(e.parentElement){t.processNode(e.parentElement)}else{t.processNode(e)}}))}}return t.findNext(this,e)}};return h}}));function o(e,t,r){var n;if(r.matchToken("the")||r.matchToken("element")||r.matchToken("elements")||r.currentToken().type==="CLASS_REF"||r.currentToken().type==="ID_REF"||r.currentToken().op&&r.currentToken().value==="<"){e.possessivesDisabled=true;try{n=e.parseElement("expression",r)}finally{delete e.possessivesDisabled}if(r.matchOpToken("'")){r.requireToken("s")}}else if(r.currentToken().type==="IDENTIFIER"&&r.currentToken().value==="its"){var i=r.matchToken("its");n={type:"pseudopossessiveIts",token:i,name:i.value,evaluate:function(e){return t.resolveSymbol("it",e)}}}else{r.matchToken("my")||r.matchToken("me");n=e.parseElement("implicitMeTarget",r)}return n}e.addCommand("transition",(function(e,t,n){if(n.matchToken("transition")){var i=o(e,t,n);var a=[];var s=[];var u=[];var l=n.currentToken();while(!e.commandBoundary(l)&&l.value!=="over"&&l.value!=="using"){if(n.currentToken().type==="STYLE_REF"){let e=n.consumeToken();let t=e.value.substr(1);a.push({type:"styleRefValue",evaluate:function(){return t}})}else{a.push(e.requireElement("stringLike",n))}if(n.matchToken("from")){s.push(e.requireElement("expression",n))}else{s.push(null)}n.requireToken("to");if(n.matchToken("initial")){u.push({type:"initial_literal",evaluate:function(){return"initial"}})}else{u.push(e.requireElement("expression",n))}l=n.currentToken()}if(n.matchToken("over")){var c=e.requireElement("expression",n)}else if(n.matchToken("using")){var f=e.requireElement("expression",n)}var m={to:u,args:[i,a,s,u,f,c],op:function(e,n,a,o,s,u,l){t.nullCheck(n,i);var c=[];t.implicitLoop(n,(function(e){var n=new Promise((function(n,i){var c=e.style.transition;if(l){e.style.transition="all "+l+"ms ease-in"}else if(u){e.style.transition=u}else{e.style.transition=r.defaultTransition}var f=t.getInternalData(e);var m=getComputedStyle(e);var p={};for(var h=0;h<m.length;h++){var v=m[h];var d=m[v];p[v]=d}if(!f.initalStyles){f.initalStyles=p}for(var h=0;h<a.length;h++){var E=a[h];var T=o[h];if(T==="computed"||T==null){e.style[E]=p[E]}else{e.style[E]=T}}var y=false;var k=false;e.addEventListener("transitionend",(function(){if(!k){e.style.transition=c;k=true;n()}}),{once:true});e.addEventListener("transitionstart",(function(){y=true}),{once:true});setTimeout((function(){if(!k&&!y){e.style.transition=c;k=true;n()}}),100);setTimeout((function(){var t=[];for(var r=0;r<a.length;r++){var n=a[r];var i=s[r];if(i==="initial"){var o=f.initalStyles[n];e.style[n]=o}else{e.style[n]=i}}}),0)}));c.push(n)}));return Promise.all(c).then((function(){return t.findNext(m,e)}))}};return m}}));e.addCommand("measure",(function(e,t,r){if(!r.matchToken("measure"))return;var n=o(e,t,r);var i=[];if(!e.commandBoundary(r.currentToken()))do{i.push(r.matchTokenType("IDENTIFIER").value)}while(r.matchOpToken(","));return{properties:i,args:[n],op:function(e,r){t.nullCheck(r,n);if(0 in r)r=r[0];var a=r.getBoundingClientRect();var o={top:r.scrollTop,left:r.scrollLeft,topMax:r.scrollTopMax,leftMax:r.scrollLeftMax,height:r.scrollHeight,width:r.scrollWidth};e.result={x:a.x,y:a.y,left:a.left,top:a.top,right:a.right,bottom:a.bottom,width:a.width,height:a.height,bounds:a,scrollLeft:o.left,scrollTop:o.top,scrollLeftMax:o.leftMax,scrollTopMax:o.topMax,scrollWidth:o.width,scrollHeight:o.height,scroll:o};t.forEach(i,(function(t){if(t in e.result)e.locals[t]=e.result[t];else throw"No such measurement as "+t}));return t.findNext(this,e)}}}));e.addLeafExpression("closestExpr",(function(e,t,r){if(r.matchToken("closest")){if(r.matchToken("parent")){var n=true}var i=null;if(r.currentToken().type==="ATTRIBUTE_REF"){var a=e.requireElement("attributeRefAccess",r,null);i="["+a.attribute.name+"]"}if(i==null){var o=e.requireElement("expression",r);if(o.css==null){e.raiseParseError(r,"Expected a CSS expression")}else{i=o.css}}if(r.matchToken("to")){var s=e.parseElement("expression",r)}else{var s=e.parseElement("implicitMeTarget",r)}var u={type:"closestExpr",parentSearch:n,expr:o,css:i,to:s,args:[s],op:function(e,r){if(r==null){return null}else{let e=[];t.implicitLoop(r,(function(t){if(n){e.push(t.parentElement?t.parentElement.closest(i):null)}else{e.push(t.closest(i))}}));if(t.shouldAutoIterate(r)){return e}else{return e[0]}}},evaluate:function(e){return t.unifiedEval(this,e)}};if(a){a.root=u;a.args=[u];return a}else{return u}}}));e.addCommand("go",(function(e,t,r){if(r.matchToken("go")){if(r.matchToken("back")){var n=true}else{r.matchToken("to");if(r.matchToken("url")){var i=e.requireElement("stringLike",r);var a=true;if(r.matchToken("in")){r.requireToken("new");r.requireToken("window");var o=true}}else{r.matchToken("the");var s=r.matchAnyToken("top","middle","bottom");var u=r.matchAnyToken("left","center","right");if(s||u){r.requireToken("of")}var i=e.requireElement("unaryExpression",r);var l=r.matchAnyOpToken("+","-");if(l){r.pushFollow("px");try{var c=e.requireElement("expression",r)}finally{r.popFollow()}}r.matchToken("px");var f=r.matchAnyToken("smoothly","instantly");var m={};if(s){if(s.value==="top"){m.block="start"}else if(s.value==="bottom"){m.block="end"}else if(s.value==="middle"){m.block="center"}}if(u){if(u.value==="left"){m.inline="start"}else if(u.value==="center"){m.inline="center"}else if(u.value==="right"){m.inline="end"}}if(f){if(f.value==="smoothly"){m.behavior="smooth"}else if(f.value==="instantly"){m.behavior="instant"}}}}var p={target:i,args:[i,c],op:function(e,r,i){if(n){window.history.back()}else if(a){if(r){if(o){window.open(r)}else{window.location.href=r}}}else{t.implicitLoop(r,(function(e){if(e===window){e=document.body}if(l){var t=e.getBoundingClientRect();let n=document.createElement("div");if(l.value==="-"){var r=-i}else{var r=- -i}n.style.position="absolute";n.style.top=t.x+r+"px";n.style.left=t.y+r+"px";n.style.height=t.height+2*r+"px";n.style.width=t.width+2*r+"px";n.style.zIndex=""+Number.MIN_SAFE_INTEGER;n.style.opacity="0";document.body.appendChild(n);setTimeout((function(){document.body.removeChild(n)}),100);e=n}e.scrollIntoView(m)}))}return t.findNext(p,e)}};return p}}));r.conversions.dynamicResolvers.push((function(t,r){if(!(t==="Values"||t.indexOf("Values:")===0)){return}var n=t.split(":")[1];var i={};var a=e.runtime.implicitLoop.bind(e.runtime);a(r,(function(e){var t=s(e);if(t!==undefined){i[t.name]=t.value;return}if(e.querySelectorAll!=undefined){var r=e.querySelectorAll("input,select,textarea");r.forEach(o)}}));if(n){if(n==="JSON"){return JSON.stringify(i)}else if(n==="Form"){return new URLSearchParams(i).toString()}else{throw"Unknown conversion: "+n}}else{return i}function o(e){var t=s(e);if(t==undefined){return}if(i[t.name]==undefined){i[t.name]=t.value;return}if(Array.isArray(i[t.name])&&Array.isArray(t.value)){i[t.name]=[].concat(i[t.name],t.value);return}}function s(e){try{var t={name:e.name,value:e.value};if(t.name==undefined||t.value==undefined){return undefined}if(e.type=="radio"&&e.checked==false){return undefined}if(e.type=="checkbox"){if(e.checked==false){t.value=undefined}else if(typeof t.value==="string"){t.value=[t.value]}}if(e.type=="select-multiple"){var r=e.querySelectorAll("option[selected]");t.value=[];for(var n=0;n<r.length;n++){t.value.push(r[n].value)}}return t}catch(e){return undefined}}}));r.conversions["HTML"]=function(e){var t=function(e){if(e instanceof Array){return e.map((function(e){return t(e)})).join("")}if(e instanceof HTMLElement){return e.outerHTML}if(e instanceof NodeList){var r="";for(var n=0;n<e.length;n++){var i=e[n];if(i instanceof HTMLElement){r+=i.outerHTML}}return r}if(e.toString){return e.toString()}return""};return t(e)};r.conversions["Fragment"]=function(t){var r=document.createDocumentFragment();e.runtime.implicitLoop(t,(function(e){if(e instanceof Node)r.append(e);else{var t=document.createElement("template");t.innerHTML=e;r.append(t.content)}}));return r}}const d=new o,E=d.lexer,T=d.parser;function y(e,t){return d.evaluate(e,t)}function k(){var t=Array.from(e.document.querySelectorAll("script[type='text/hyperscript'][src]"));Promise.all(t.map((function(e){return fetch(e.src).then((function(e){return e.text()}))}))).then((e=>e.forEach((e=>x(e))))).then((()=>n((function(){a();d.processNode(document.documentElement);e.document.addEventListener("htmx:load",(function(e){d.processNode(e.detail.elt)}))}))));function n(e){if(document.readyState!=="loading"){setTimeout(e)}else{document.addEventListener("DOMContentLoaded",e)}}function i(){var e=document.querySelector('meta[name="htmx-config"]');if(e){return f(e.content)}else{return null}}function a(){var e=i();if(e){Object.assign(r,e)}}}const x=Object.assign(y,{config:r,use(e){e(x)},internals:{lexer:E,parser:T,runtime:d,Lexer:n,Tokens:i,Parser:a,Runtime:o},ElementCollection:u,addFeature:T.addFeature.bind(T),addCommand:T.addCommand.bind(T),addLeafExpression:T.addLeafExpression.bind(T),addIndirectExpression:T.addIndirectExpression.bind(T),evaluate:d.evaluate.bind(d),parse:d.parse.bind(d),processNode:d.processNode.bind(d),browserInit:k});return x}));
diff --git a/common/static/sass/_Vendor.sass b/common/static/sass/_Vendor.sass
index bffa2a2f..acee01ea 100644
--- a/common/static/sass/_Vendor.sass
+++ b/common/static/sass/_Vendor.sass
@@ -4,6 +4,21 @@
     & ul li
         list-style: circle inside
 
+    h1
+        font-size: 2.5em
+    
+    h2
+        font-size: 2.0em
+
+    blockquote
+        border-left: lightgray solid 0.4em
+        padding-left: 0.1em
+        margin-left: 0
+
+    code
+        border-left: $color-primary solid 0.3em
+        padding-left: 0.1em
+
 .rating-star .jq-star
     cursor: unset !important
 
diff --git a/common/templates/common/error.html b/common/templates/common/error.html
index f4290972..7e29db79 100644
--- a/common/templates/common/error.html
+++ b/common/templates/common/error.html
@@ -6,7 +6,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta http-equiv="refresh" content="5;url={% if url %}{{url}}{% else %}{% url 'common:home' %}{% endif %}">
-    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
+    <link rel="stylesheet" href="https://cdn.staticfile.org/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
     <title>{% trans '错误' %}</title>
diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html
index 0144093d..f8f8708b 100644
--- a/common/templates/common/search_result.html
+++ b/common/templates/common/search_result.html
@@ -15,13 +15,13 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '搜索结果' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.4/htmx.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/common/templates/partial/_common_libs.html b/common/templates/partial/_common_libs.html
index 9460e2da..13157a7f 100644
--- a/common/templates/partial/_common_libs.html
+++ b/common/templates/partial/_common_libs.html
@@ -1,23 +1,23 @@
 {% load static %}
 {% if sentry_dsn %}
-<script src="https://static.neodb.social/browser.sentry-cdn.com/7.7.0/bundle.min.js"></script>
+<script src="https://browser.sentry-cdn.com/7.7.0/bundle.min.js"></script>
 <script>
     if (window.Sentry) Sentry.init({
       dsn: "{{ sentry_dsn }}",
-      release: "NeoDB@{{ version_hash }}",
+      release: "{{ version_hash }}",
       environment: "{{ settings_module }}",
       tracesSampleRate: 1.0,
     });
 </script>
 {% endif %}
 {% if jquery %}
-<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
 {% else %}
-<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/cash/8.1.1/cash.min.js"></script>
+<script src="https://cdn.staticfile.org/cash/8.1.1/cash.min.js"></script>
 {% endif %}
-<script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
-<script src="https://static.neodb.social/unpkg.com/hyperscript.org@0.9.7.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.4/htmx.min.js"></script>
+<script src="{% static 'lib/js/hyperscript-0.9.7.min.js' %}"></script>
 <link rel="stylesheet" href="{% static 'css/boofilsic.css' %}">
 <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
-<link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+<link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 <link rel="search"type="application/opensearchdescription+xml" title="{{ site_name }}" href="{% static 'opensearch.xml' %}">
diff --git a/common/templates/partial/_sidebar.html b/common/templates/partial/_sidebar.html
index 90bc2c0d..455bdbac 100644
--- a/common/templates/partial/_sidebar.html
+++ b/common/templates/partial/_sidebar.html
@@ -5,7 +5,7 @@
 {% load oauth_token %}
 {% load truncate %}
 {% load thumb %}
-{% load neo %}
+{% load user_item %}
 <div class="grid__aside grid__aside--reverse-order grid__aside--tablet-column">
     <div class="aside-section-wrapper aside-section-wrapper--no-margin">
         <div class="user-profile" id="userInfoCard">
diff --git a/common/templates/partial/list_item_book.html b/common/templates/partial/list_item_book.html
index 3c5dc9eb..73efa7bf 100644
--- a/common/templates/partial/list_item_book.html
+++ b/common/templates/partial/list_item_book.html
@@ -2,7 +2,7 @@
 {% load highlight %}
 {% load i18n %}
 {% load l10n %}
-{% load neo %}
+{% load user_item %}
 {% current_user_marked_item book as marked %}
 <li class="entity-list__entity">
     <div class="entity-list__entity-img-wrapper">
diff --git a/common/templates/partial/list_item_game.html b/common/templates/partial/list_item_game.html
index 42346910..97db04c8 100644
--- a/common/templates/partial/list_item_game.html
+++ b/common/templates/partial/list_item_game.html
@@ -2,7 +2,7 @@
 {% load highlight %}
 {% load i18n %}
 {% load l10n %}
-{% load neo %}
+{% load user_item %}
 {% current_user_marked_item game as marked %}
 <li class="entity-list__entity">
     <div class="entity-list__entity-img-wrapper">
diff --git a/common/templates/partial/list_item_movie.html b/common/templates/partial/list_item_movie.html
index f20f40f5..6c9a2830 100644
--- a/common/templates/partial/list_item_movie.html
+++ b/common/templates/partial/list_item_movie.html
@@ -3,7 +3,7 @@
 {% load i18n %}
 {% load l10n %}
 {% load humanize %}
-{% load neo %}
+{% load user_item %}
 {% current_user_marked_item movie as marked %}
 <li class="entity-list__entity">
     <div class="entity-list__entity-img-wrapper">
diff --git a/common/templates/partial/list_item_music.html b/common/templates/partial/list_item_music.html
index bcf0d29a..ab9244a1 100644
--- a/common/templates/partial/list_item_music.html
+++ b/common/templates/partial/list_item_music.html
@@ -2,7 +2,7 @@
 {% load highlight %}
 {% load i18n %}
 {% load l10n %}
-{% load neo %}
+{% load user_item %}
 {% current_user_marked_item music as marked %}
 <li class="entity-list__entity">
     <div class="entity-list__entity-img-wrapper">
diff --git a/common/templatetags/mastodon.py b/common/templatetags/mastodon.py
index 0b10f422..77e81315 100644
--- a/common/templatetags/mastodon.py
+++ b/common/templatetags/mastodon.py
@@ -10,3 +10,17 @@ register = template.Library()
 def mastodon(domain):
     url = 'https://' + domain
     return url 
+
+
+@register.simple_tag(takes_context=True)
+def current_user_relationship(context, user):
+    current_user = context['request'].user
+    if current_user and current_user.is_authenticated:
+        if current_user.is_following(user):
+            if current_user.is_followed_by(user):
+                return '互相关注'
+            else:
+                return '已关注'
+        elif current_user.is_followed_by(user):
+            return '被ta关注'
+    return None
\ No newline at end of file
diff --git a/common/templatetags/neo.py b/common/templatetags/neo.py
deleted file mode 100644
index b915b7b9..00000000
--- a/common/templatetags/neo.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from django import template
-import datetime
-from django.utils import timezone
-from collection.models import Collection
-
-
-register = template.Library()
-
-
-@register.simple_tag(takes_context=True)
-def current_user_marked_item(context, item):
-    user = context['request'].user
-    if user and user.is_authenticated:
-        if isinstance(item, Collection) and item.owner == user:
-            return item
-        else:
-            return context['request'].user.get_mark_for_item(item)
-    return None
-
-
-@register.simple_tag(takes_context=True)
-def current_user_relationship(context, user):
-    current_user = context['request'].user
-    if current_user and current_user.is_authenticated:
-        if current_user.is_following(user):
-            if current_user.is_followed_by(user):
-                return '互相关注'
-            else:
-                return '已关注'
-        elif current_user.is_followed_by(user):
-            return '被ta关注'
-    return None
-
-
-@register.filter
-def prettydate(d):
-    diff = timezone.now() - d
-    s = diff.seconds
-    if diff.days > 14 or diff.days < 0:
-        return d.strftime('%Y年%m月%d日')
-    elif diff.days >= 1:
-        return '{} 天前'.format(diff.days)
-    elif s < 120:
-        return '刚刚'
-    elif s < 3600:
-        return '{} 分钟前'.format(s // 60)
-    else:
-        return '{} 小时前'.format(s // 3600)
diff --git a/common/templatetags/prettydate.py b/common/templatetags/prettydate.py
new file mode 100644
index 00000000..4d37c538
--- /dev/null
+++ b/common/templatetags/prettydate.py
@@ -0,0 +1,22 @@
+from django import template
+from django.utils import timezone
+
+
+register = template.Library()
+
+
+@register.filter
+def prettydate(d):
+    # TODO use date and naturaltime instead https://docs.djangoproject.com/en/3.2/ref/contrib/humanize/
+    diff = timezone.now() - d
+    s = diff.seconds
+    if diff.days > 14 or diff.days < 0:
+        return d.strftime('%Y年%m月%d日')
+    elif diff.days >= 1:
+        return '{} 天前'.format(diff.days)
+    elif s < 120:
+        return '刚刚'
+    elif s < 3600:
+        return '{} 分钟前'.format(s // 60)
+    else:
+        return '{} 小时前'.format(s // 3600)
diff --git a/common/templatetags/user_item.py b/common/templatetags/user_item.py
new file mode 100644
index 00000000..2140b914
--- /dev/null
+++ b/common/templatetags/user_item.py
@@ -0,0 +1,19 @@
+from django import template
+from collection.models import Collection
+
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def current_user_marked_item(context, item):
+    # NOTE weird to put business logic in tags
+    user = context['request'].user
+    if user and user.is_authenticated:
+        if isinstance(item, Collection) and item.owner == user:
+            return item
+        else:
+            return context['request'].user.get_mark_for_item(item)
+    return None
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 86f8c09f..01ebbb32 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,7 +26,7 @@ services:
       - DB_USER=postgres
       - DB_PASSWORD=postgres
       - REDIS_HOST=redis
-      - DJANGO_SETTINGS_MODULE=neodb.dev
+      - DJANGO_SETTINGS_MODULE=yoursettings.dev
     depends_on:
       - db
       - redis
diff --git a/games/models.py b/games/models.py
index fba9e639..ba48b905 100644
--- a/games/models.py
+++ b/games/models.py
@@ -145,7 +145,7 @@ class GameReview(Review):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + reverse("games:retrieve_review", args=[self.id])
+        return reverse("games:retrieve_review", args=[self.id])
 
     @property
     def item(self):
diff --git a/games/templates/games/create_update.html b/games/templates/games/create_update.html
index 178089c4..7365d687 100644
--- a/games/templates/games/create_update.html
+++ b/games/templates/games/create_update.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
diff --git a/games/templates/games/create_update_review.html b/games/templates/games/create_update_review.html
index cc9f8a2c..93b9bd55 100644
--- a/games/templates/games/create_update_review.html
+++ b/games/templates/games/create_update_review.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/games/templates/games/delete.html b/games/templates/games/delete.html
index a7a2adde..f7cd512a 100644
--- a/games/templates/games/delete.html
+++ b/games/templates/games/delete.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/games/templates/games/delete_review.html b/games/templates/games/delete_review.html
index f39cdf62..948850b8 100644
--- a/games/templates/games/delete_review.html
+++ b/games/templates/games/delete_review.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
diff --git a/games/templates/games/mark_list.html b/games/templates/games/mark_list.html
index 6f0acffc..d80e6409 100644
--- a/games/templates/games/mark_list.html
+++ b/games/templates/games/mark_list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ game.title }}{% trans '的标记' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
diff --git a/games/templates/games/review_detail.html b/games/templates/games/review_detail.html
index 6b0c357f..fcadc6d4 100644
--- a/games/templates/games/review_detail.html
+++ b/games/templates/games/review_detail.html
@@ -19,12 +19,12 @@
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ game.cover|thumb:'normal' }}">
     <title>{{ site_name }}游戏评论 - {{ review.title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/games/templates/games/review_list.html b/games/templates/games/review_list.html
index 8b05318d..6b84ae7a 100644
--- a/games/templates/games/review_list.html
+++ b/games/templates/games/review_list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ game.title }}{% trans '的评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/games/templates/games/scrape.html b/games/templates/games/scrape.html
index aaaeb99b..0a3370da 100644
--- a/games/templates/games/scrape.html
+++ b/games/templates/games/scrape.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/management/templates/management/detail.html b/management/templates/management/detail.html
index 76150b1d..60142c14 100644
--- a/management/templates/management/detail.html
+++ b/management/templates/management/detail.html
@@ -6,7 +6,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     <title>{{ site_name }} - {{ object.title }}</title>
 </head>
diff --git a/management/templates/management/list.html b/management/templates/management/list.html
index ab5a2b24..c6f45695 100644
--- a/management/templates/management/list.html
+++ b/management/templates/management/list.html
@@ -7,7 +7,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 
     <title>{{ site_name }} - {% trans '公告栏' %}</title>
diff --git a/movies/models.py b/movies/models.py
index ae3a9990..53748cd6 100644
--- a/movies/models.py
+++ b/movies/models.py
@@ -272,7 +272,7 @@ class MovieReview(Review):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + reverse("movies:retrieve_review", args=[self.id])
+        return reverse("movies:retrieve_review", args=[self.id])
 
     @property
     def item(self):
diff --git a/movies/templates/movies/create_update.html b/movies/templates/movies/create_update.html
index 497c9bce..471f664d 100644
--- a/movies/templates/movies/create_update.html
+++ b/movies/templates/movies/create_update.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
diff --git a/movies/templates/movies/create_update_review.html b/movies/templates/movies/create_update_review.html
index 87046277..fca3183b 100644
--- a/movies/templates/movies/create_update_review.html
+++ b/movies/templates/movies/create_update_review.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/movies/templates/movies/delete.html b/movies/templates/movies/delete.html
index 94dfb2ce..a4d8f843 100644
--- a/movies/templates/movies/delete.html
+++ b/movies/templates/movies/delete.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除电影/剧集' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/movies/templates/movies/delete_review.html b/movies/templates/movies/delete_review.html
index db3d3fe0..d51f63cf 100644
--- a/movies/templates/movies/delete_review.html
+++ b/movies/templates/movies/delete_review.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
diff --git a/movies/templates/movies/mark_list.html b/movies/templates/movies/mark_list.html
index 27cc0e1e..20da04e6 100644
--- a/movies/templates/movies/mark_list.html
+++ b/movies/templates/movies/mark_list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ movie.title }}{% trans '的标记' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
diff --git a/movies/templates/movies/review_detail.html b/movies/templates/movies/review_detail.html
index 4a83227e..53e213f3 100644
--- a/movies/templates/movies/review_detail.html
+++ b/movies/templates/movies/review_detail.html
@@ -19,12 +19,12 @@
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ movie.cover|thumb:'normal' }}">
     <title>{{ site_name }}影评 - {{ review.title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/movies/templates/movies/review_list.html b/movies/templates/movies/review_list.html
index daedf296..d9541285 100644
--- a/movies/templates/movies/review_list.html
+++ b/movies/templates/movies/review_list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ movie.title }}{% trans '的评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/movies/templates/movies/scrape.html b/movies/templates/movies/scrape.html
index 0387cc10..32a94f8a 100644
--- a/movies/templates/movies/scrape.html
+++ b/movies/templates/movies/scrape.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/models.py b/music/models.py
index 131f9a14..e5c91e30 100644
--- a/music/models.py
+++ b/music/models.py
@@ -191,7 +191,7 @@ class SongReview(Review):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + reverse("music:retrieve_song_review", args=[self.id])
+        return reverse("music:retrieve_song_review", args=[self.id])
 
     @property
     def item(self):
@@ -242,7 +242,7 @@ class AlbumReview(Review):
 
     @property
     def url(self):
-        return settings.APP_WEBSITE + reverse("music:retrieve_album_review", args=[self.id])
+        return reverse("music:retrieve_album_review", args=[self.id])
 
     @property
     def item(self):
diff --git a/music/templates/music/album_mark_list.html b/music/templates/music/album_mark_list.html
index 49e4b670..afc59663 100644
--- a/music/templates/music/album_mark_list.html
+++ b/music/templates/music/album_mark_list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ album.title }}{% trans '的标记' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
diff --git a/music/templates/music/album_review_detail.html b/music/templates/music/album_review_detail.html
index 3e6e8fdd..33ca6f28 100644
--- a/music/templates/music/album_review_detail.html
+++ b/music/templates/music/album_review_detail.html
@@ -19,12 +19,12 @@
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ album.cover|thumb:'normal' }}">
     <title>{{ site_name }}乐评 - {{ review.title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/music/templates/music/album_review_list.html b/music/templates/music/album_review_list.html
index 40ce5b6d..88035e96 100644
--- a/music/templates/music/album_review_list.html
+++ b/music/templates/music/album_review_list.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ album.title }}{% trans '的评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/music/templates/music/create_update_album.html b/music/templates/music/create_update_album.html
index 111c60a1..cceb04d9 100644
--- a/music/templates/music/create_update_album.html
+++ b/music/templates/music/create_update_album.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
diff --git a/music/templates/music/create_update_album_review.html b/music/templates/music/create_update_album_review.html
index 440bce52..fab74aac 100644
--- a/music/templates/music/create_update_album_review.html
+++ b/music/templates/music/create_update_album_review.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/music/templates/music/create_update_song.html b/music/templates/music/create_update_song.html
index 4a4e8b49..9338ea29 100644
--- a/music/templates/music/create_update_song.html
+++ b/music/templates/music/create_update_song.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
 
diff --git a/music/templates/music/create_update_song_review.html b/music/templates/music/create_update_song_review.html
index ec74d0bb..a26ecfd1 100644
--- a/music/templates/music/create_update_song_review.html
+++ b/music/templates/music/create_update_song_review.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update_review.js' %}"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/music/templates/music/delete_album.html b/music/templates/music/delete_album.html
index 8539d736..fa7929a9 100644
--- a/music/templates/music/delete_album.html
+++ b/music/templates/music/delete_album.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除音乐' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/music/templates/music/delete_album_review.html b/music/templates/music/delete_album_review.html
index b67d065e..1c2cf8b5 100644
--- a/music/templates/music/delete_album_review.html
+++ b/music/templates/music/delete_album_review.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
diff --git a/music/templates/music/delete_song.html b/music/templates/music/delete_song.html
index 1be0112c..5fc7a2bc 100644
--- a/music/templates/music/delete_song.html
+++ b/music/templates/music/delete_song.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除音乐' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/music/templates/music/delete_song_review.html b/music/templates/music/delete_song_review.html
index d2d610f3..3b983290 100644
--- a/music/templates/music/delete_song_review.html
+++ b/music/templates/music/delete_song_review.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '删除评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
     
diff --git a/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html
index b4fc9aba..c58faf7a 100644
--- a/music/templates/music/scrape_album.html
+++ b/music/templates/music/scrape_album.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/templates/music/scrape_song.html b/music/templates/music/scrape_song.html
index 2ffa1b3b..83557681 100644
--- a/music/templates/music/scrape_song.html
+++ b/music/templates/music/scrape_song.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '从豆瓣获取数据' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/scrape.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/music/templates/music/song_mark_list.html b/music/templates/music/song_mark_list.html
index a5fcf18b..16499d75 100644
--- a/music/templates/music/song_mark_list.html
+++ b/music/templates/music/song_mark_list.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ song.title }}{% trans '的标记' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
diff --git a/music/templates/music/song_review_detail.html b/music/templates/music/song_review_detail.html
index e261fa91..ea3608ee 100644
--- a/music/templates/music/song_review_detail.html
+++ b/music/templates/music/song_review_detail.html
@@ -19,12 +19,12 @@
     <meta property="og:url" content="{{ request.build_absolute_uri }}">
     <meta property="og:image" content="{{ song.cover|thumb:'normal' }}">
     <title>{{ site_name }}乐评 - {{ review.title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/music/templates/music/song_review_list.html b/music/templates/music/song_review_list.html
index 5926eba6..bc59bd58 100644
--- a/music/templates/music/song_review_list.html
+++ b/music/templates/music/song_review_list.html
@@ -14,7 +14,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ song.title }}{% trans '的评论' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
diff --git a/requirements.txt b/requirements.txt
index d3177ab4..4f2dd417 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,20 +7,15 @@ django-rq
 django-simple-history
 django-hijack
 django-user-messages
-django-slack
-meilisearch
 easy-thumbnails
 lxml
 openpyxl
 psycopg2
 requests
 filetype
-setproctitle
 tqdm
 opencc
 dnspython
 typesense
 markdownify
-sentry-sdk
-gitpython
 igdb-api-v4
diff --git a/timeline/templates/timeline_data.html b/timeline/templates/timeline_data.html
index 5b014f39..5d9559c5 100644
--- a/timeline/templates/timeline_data.html
+++ b/timeline/templates/timeline_data.html
@@ -6,7 +6,7 @@
 {% load oauth_token %}
 {% load truncate %}
 {% load thumb %}
-{% load neo %}
+{% load prettydate %}
 
 {% for activity in activities %}
 {% current_user_marked_item activity.target.item as marked %}
diff --git a/users/templates/users/home.html b/users/templates/users/home.html
index ddad07d3..96ab3d16 100644
--- a/users/templates/users/home.html
+++ b/users/templates/users/home.html
@@ -19,8 +19,8 @@
 
     {% include "partial/_common_libs.html" with jquery=1 %}
 
-    <script src="{% static 'js/mastodon.js' %}"></script>
-    <script src="{% static 'js/home.js' %}"></script>
+    <script src="{% static 'js/mastodon.js' %}" defer></script>
+    <script src="{% static 'js/home.js' %}" defer></script>
 </head>
 
 <body>
@@ -634,7 +634,7 @@
                             {% csrf_token %}
                             <input type="hidden" name="layout">
                         </form>
-                        <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/html5sortable.min.js" crossorigin="anonymous"></script>
+                        <script src="https://cdn.staticfile.org/html5sortable/0.13.3/html5sortable.min.js" crossorigin="anonymous"></script>
                         <script src="{% static 'js/sort_layout.js' %}"></script>
                         {% endif %}
                         <script>
diff --git a/users/templates/users/item_list.html b/users/templates/users/item_list.html
index 321ad0fe..595071dc 100644
--- a/users/templates/users/item_list.html
+++ b/users/templates/users/item_list.html
@@ -13,15 +13,15 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {{ user.mastodon_username }} {{ list_title }}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/htmx/1.8.0/htmx.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.8.4/htmx.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <script src="{% static 'js/mastodon.js' %}"></script>
     <script src="{% static 'js/home.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
-    <link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
+    <link rel="stylesheet" href="{% static 'lib/css/collection.css' %}">
 </head>
 
 <body>
diff --git a/users/templates/users/login.html b/users/templates/users/login.html
index d843543c..8ec45949 100644
--- a/users/templates/users/login.html
+++ b/users/templates/users/login.html
@@ -12,10 +12,10 @@
     <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'img/logo_square.jpg' %}">
     <title>{{ site_name }} - {% trans '登录' %}</title>
     {% include "partial/_common_libs.html" %}
-    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
+    <link rel="stylesheet" href="https://cdn.staticfile.org/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/js-cookie/3.0.1/js.cookie.min.js"></script>
+    <script src="https://cdn.staticfile.org/js-cookie/3.0.1/js.cookie.min.js"></script>
     <script> $(document).ready( function() { $('.delayed').remove(); $('#loginButton').prop("disabled", false); } ); </script>
     <style type="text/css">
         .delayed {
@@ -67,7 +67,6 @@
                 placeholder="实例域名(不含@和@之前的部分),如mastodon.social"
                 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
             <input type='submit' value="{% trans '授权登录' %}" id="loginButton" disabled />
-            <br><a target="_blank" href="https://about.neodb.social/doc/howto/">{% trans '了解更多' %}</a>
             <script type="text/javascript">if (Cookies.get('mastodon_domain')) $('#domain').val(Cookies.get('mastodon_domain'));</script>
             {% else %}            
             <select name="domain" placeholder="test">
diff --git a/users/templates/users/manage_report.html b/users/templates/users/manage_report.html
index f077a434..ee2270eb 100644
--- a/users/templates/users/manage_report.html
+++ b/users/templates/users/manage_report.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '管理举报' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/users/templates/users/register.html b/users/templates/users/register.html
index cb63b5c6..deda3bb3 100644
--- a/users/templates/users/register.html
+++ b/users/templates/users/register.html
@@ -6,7 +6,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.min.css">
+    <link rel="stylesheet" href="https://cdn.staticfile.org/milligram/1.4.1/milligram.min.css">
     <link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
     <link rel="stylesheet" href="{% static 'css/boofilsic_box.css' %}">
     <title>{{ site_name }} - {% trans '注册' %}</title>
diff --git a/users/templates/users/report.html b/users/templates/users/report.html
index ea47b64a..4ad3fe4a 100644
--- a/users/templates/users/report.html
+++ b/users/templates/users/report.html
@@ -11,7 +11,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - {% trans '举报用户' %}</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'js/create_update.js' %}"></script>
     <link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
 </head>
diff --git a/users/templates/users/tags.html b/users/templates/users/tags.html
index 2d9c738f..c6014a34 100644
--- a/users/templates/users/tags.html
+++ b/users/templates/users/tags.html
@@ -15,7 +15,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ site_name }} - 我的标签</title>
-    <script src="https://static.neodb.social/cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
+    <script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
     <script src="{% static 'lib/js/rating-star.js' %}"></script>
     <script src="{% static 'js/rating-star-readonly.js' %}"></script>
     <link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">

From 6924fdc092d029a7d8cf7df3bfbc67ab1ae385e0 Mon Sep 17 00:00:00 2001
From: doubaniux <goodsir@vivaldi.net>
Date: Sat, 26 Nov 2022 20:36:44 +0100
Subject: [PATCH 5/6] minor fixes

---
 collection/models.py       |  2 ++
 common/models.py           |  5 +++++
 common/search/typesense.py | 18 +++++++++++-------
 mastodon/api.py            |  4 ++--
 users/feeds.py             |  2 +-
 users/tasks.py             |  2 +-
 6 files changed, 22 insertions(+), 11 deletions(-)

diff --git a/collection/models.py b/collection/models.py
index dda20130..ad149f5c 100644
--- a/collection/models.py
+++ b/collection/models.py
@@ -1,4 +1,5 @@
 from django.db import models
+from markdown import markdown
 from common.models import UserOwnedEntity
 from movies.models import Movie
 from books.models import Book
@@ -8,6 +9,7 @@ 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
 
 
diff --git a/common/models.py b/common/models.py
index e897b18b..d7589627 100644
--- a/common/models.py
+++ b/common/models.py
@@ -64,6 +64,11 @@ class Entity(models.Model):
     def url(self):
         return self.get_absolute_url()
 
+    @property
+    def absolute_url(self):
+        """URL with host and protocol"""
+        return settings.APP_WEBSITE + self.url
+
     def get_json(self):
         return {
             'title': self.title,
diff --git a/common/search/typesense.py b/common/search/typesense.py
index 29123de4..e03610cc 100644
--- a/common/search/typesense.py
+++ b/common/search/typesense.py
@@ -1,5 +1,7 @@
+import types
 import logging
 import typesense
+from typesense.exceptions import ObjectNotFound
 from django.conf import settings
 from django.db.models.signals import post_save, post_delete
 
@@ -194,14 +196,16 @@ class Indexer:
             # 'facetsDistribution': ['_class'],
             # 'sort_by': None,
         }
-        # print(q)
-        r = self.instance().collections[INDEX_NAME].documents.search(options)
-        # print(r)
-        import types
         results = types.SimpleNamespace()
-        results.items = list([x for x in map(lambda i: self.item_to_obj(i['document']), r['hits']) if x is not None])
-        results.num_pages = (r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
-        # print(results)
+
+        try:
+            r = self.instance().collections[INDEX_NAME].documents.search(options)
+            results.items = list([x for x in map(lambda i: self.item_to_obj(i['document']), r['hits']) if x is not None])
+            results.num_pages = (r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
+        except ObjectNotFound:
+            results.items = []
+            results.num_pages = 1
+
         return results
 
     @classmethod
diff --git a/mastodon/api.py b/mastodon/api.py
index 63a6990e..66546f5c 100644
--- a/mastodon/api.py
+++ b/mastodon/api.py
@@ -410,7 +410,7 @@ def share_mark(mark):
         visibility = TootVisibilityEnum.UNLISTED
     tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(mark.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
     stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode)
-    content = f"{mark.translated_status}《{mark.item.title}》{stars}\n{mark.item.url}\n{mark.text}{tags}"
+    content = f"{mark.translated_status}《{mark.item.title}》{stars}\n{mark.item.absolute_url}\n{mark.text}{tags}"
     update_id = None
     if mark.shared_link:  # "https://mastodon.social/@username/1234567890"
         r = re.match(r'.+/(\w+)$', mark.shared_link)  # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
@@ -469,7 +469,7 @@ def share_collection(collection, comment, user, visibility_no):
     else:
         visibility = TootVisibilityEnum.UNLISTED
     tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', '收藏单') if user.get_preference().mastodon_append_tag else ''
-    content = f"分享收藏单《{collection.title}》\n{collection.url}\n{comment}{tags}"
+    content = f"分享收藏单《{collection.title}》\n{collection.absolute_url}\n{comment}{tags}"
     response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
     if response and response.status_code in [200, 201]:
         j = response.json()
diff --git a/users/feeds.py b/users/feeds.py
index 16bd045d..ec325249 100644
--- a/users/feeds.py
+++ b/users/feeds.py
@@ -41,7 +41,7 @@ class ReviewFeed(Feed):
         return f"{item.title} - 评论《{item.item.title}》"
 
     def item_description(self, item):
-        target_html = f'<p><a href="{item.item.url}">{item.item.title}</a></p>\n' 
+        target_html = f'<p><a href="{item.item.absolute_url}">{item.item.title}</a></p>\n' 
         html = markdown(item.content)
         return target_html + html
 
diff --git a/users/tasks.py b/users/tasks.py
index a7c17f82..25766c2e 100644
--- a/users/tasks.py
+++ b/users/tasks.py
@@ -135,7 +135,7 @@ def export_marks_task(user):
             my_rating = None  # (mark.rating / 2) if mark.rating else None
             content = review.content
             target_source_url = review.item.source_url
-            target_url = review.item.url
+            target_url = review.item.absolute_url
             line = [title, target, url, timestamp, my_rating, label, content, target_source_url, target_url]
             ws.append(line)
 

From 5e036650c1b36a219c853f0edd0f1dd358a6b600 Mon Sep 17 00:00:00 2001
From: Te Llamas <you@example.com>
Date: Fri, 9 Dec 2022 23:48:41 +0000
Subject: [PATCH 6/6] remove log in js

---
 common/static/js/home.js                  | 3 +--
 common/static/js/mastodon.js              | 1 -
 management/templates/management/list.html | 3 +--
 3 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/common/static/js/home.js b/common/static/js/home.js
index d32ac888..78c66924 100644
--- a/common/static/js/home.js
+++ b/common/static/js/home.js
@@ -224,7 +224,6 @@ $(document).ready( function() {
                     if (data.failed_urls.length > 0) {
                         $(".import-panel__fail-urls").show();
                         data.failed_urls.forEach((v, i) => {
-                            console.log(v)
                             $("#failedUrls").append($("<li>" + v + "</li>"));
                         });
                     }
@@ -242,4 +241,4 @@ $(document).ready( function() {
         });
     }
 
-});
\ No newline at end of file
+});
diff --git a/common/static/js/mastodon.js b/common/static/js/mastodon.js
index 7e62f6d5..02a54679 100644
--- a/common/static/js/mastodon.js
+++ b/common/static/js/mastodon.js
@@ -155,7 +155,6 @@ function getEmojiDict(emoji_list) {
 }
 
 function translateEmojis(text, emoji_list, large) {
-    console.log(text)
     let dict = getEmojiDict(emoji_list);
     let regex = /:(.*?):/g;
     let translation = null
diff --git a/management/templates/management/list.html b/management/templates/management/list.html
index c6f45695..fad80c22 100644
--- a/management/templates/management/list.html
+++ b/management/templates/management/list.html
@@ -111,10 +111,9 @@
         });
         // strip html tags
         document.querySelectorAll(".markdownx-preview").forEach(e => {
-            console.log($(e).text())
             $(e).html($(e).text());
         });
     </script>
 </body>
 
-</html>
\ No newline at end of file
+</html>