├── .dockerignore
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── check.yml
│ ├── codeql-analysis.yml
│ ├── mirror.yml
│ ├── preview-deploy.yml
│ ├── publish-tags.yml
│ ├── publish.yml
│ └── tests.yml
├── .gitignore
├── .gitmodules
├── .pre-commit-config.yaml
├── Dockerfile
├── FEDERATION.md
├── LICENSE
├── README.md
├── SECURITY.md
├── boofilsic
├── __init__.py
├── asgi.py
├── context_processors.py
├── settings.py
├── urls.py
└── wsgi.py
├── catalog
├── __init__.py
├── admin.py
├── apis.py
├── apps.py
├── book
│ ├── models.py
│ ├── tests.py
│ └── utils.py
├── collection
│ └── models.py
├── common
│ ├── __init__.py
│ ├── downloaders.py
│ ├── jsondata.py
│ ├── migrations.py
│ ├── models.py
│ ├── scrapers.py
│ ├── sites.py
│ ├── tests.py
│ └── utils.py
├── forms.py
├── game
│ ├── models.py
│ └── tests.py
├── index.py
├── jobs
│ ├── __init__.py
│ ├── discover.py
│ └── podcast.py
├── management
│ └── commands
│ │ ├── cat.py
│ │ ├── catalog.py
│ │ └── crawl.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0001_initial_0_10.py
│ ├── 0002_fix_soft_deleted_edition.py
│ ├── 0002_initial.py
│ ├── 0003_podcast.py
│ ├── 0004_podcast_no_real_change.py
│ ├── 0005_bookstw.py
│ ├── 0006_auto_20230602_0258.py
│ ├── 0007_performance.py
│ ├── 0008_delete_historicalitem.py
│ ├── 0009_remove_tvepisode_show.py
│ ├── 0010_alter_item_polymorphic_ctype.py
│ ├── 0011_alter_externalresource_id_type_and_more.py
│ ├── 0011_remove_item_last_editor.py
│ ├── 0012_alter_model_i18n.py
│ ├── 0013_migrate_work.py
│ ├── 0014_auto_reindex.py
│ ├── 0015_item_add_is_protected.py
│ ├── 0016_rename_item_primary_lookup_id_type_primary_lookup_id_value_catalog_ite_primary_0a089c_idx_and_more.py
│ ├── 0017_normalize_lang.py
│ └── __init__.py
├── models.py
├── movie
│ ├── models.py
│ └── tests.py
├── music
│ ├── models.py
│ ├── tests.py
│ └── utils.py
├── performance
│ ├── models.py
│ └── tests.py
├── podcast
│ ├── models.py
│ └── tests.py
├── search
│ ├── external.py
│ ├── models.py
│ └── views.py
├── sites
│ ├── __init__.py
│ ├── ao3.py
│ ├── apple_music.py
│ ├── apple_podcast.py
│ ├── bandcamp.py
│ ├── bangumi.py
│ ├── bgg.py
│ ├── bibliotek_dk.py
│ ├── bookstw.py
│ ├── discogs.py
│ ├── douban.py
│ ├── douban_book.py
│ ├── douban_drama.py
│ ├── douban_game.py
│ ├── douban_movie.py
│ ├── douban_music.py
│ ├── fedi.py
│ ├── goodreads.py
│ ├── google_books.py
│ ├── igdb.py
│ ├── imdb.py
│ ├── jjwxc.py
│ ├── qidian.py
│ ├── rss.py
│ ├── spotify.py
│ ├── steam.py
│ ├── tmdb.py
│ └── ypshuo.py
├── static
│ └── js
│ │ └── podcast.js
├── templates
│ ├── _actor.html
│ ├── _crew.html
│ ├── _fetch_failed.html
│ ├── _fetch_refresh.html
│ ├── _item_card.html
│ ├── _item_card_metadata_album.html
│ ├── _item_card_metadata_base.html
│ ├── _item_card_metadata_collection.html
│ ├── _item_card_metadata_edition.html
│ ├── _item_card_metadata_game.html
│ ├── _item_card_metadata_movie.html
│ ├── _item_card_metadata_performance.html
│ ├── _item_card_metadata_performanceproduction.html
│ ├── _item_card_metadata_podcast.html
│ ├── _item_card_metadata_podcastepisode.html
│ ├── _item_card_metadata_tvepisode.html
│ ├── _item_card_metadata_tvseason.html
│ ├── _item_card_metadata_tvshow.html
│ ├── _item_card_metadata_work.html
│ ├── _item_comments.html
│ ├── _item_comments_by_episode.html
│ ├── _item_notes.html
│ ├── _item_reviews.html
│ ├── _item_user_mark_history.html
│ ├── _item_user_pieces.html
│ ├── _language_list.html
│ ├── _people.html
│ ├── _sidebar_edit.html
│ ├── _sidebar_item.html
│ ├── _sidebar_search.html
│ ├── album.html
│ ├── catalog_delete.html
│ ├── catalog_edit.html
│ ├── catalog_history.html
│ ├── catalog_merge.html
│ ├── discover.html
│ ├── edition.html
│ ├── embed_base.html
│ ├── embed_podcast.html
│ ├── external_search_results.html
│ ├── fetch_pending.html
│ ├── game.html
│ ├── item.html
│ ├── item_base.html
│ ├── item_mark_list.html
│ ├── item_review_list.html
│ ├── movie.html
│ ├── performance.html
│ ├── performanceproduction.html
│ ├── podcast.html
│ ├── podcast_episode_data.html
│ ├── podcastepisode.html
│ ├── search_header.html
│ ├── search_results.html
│ ├── tvepisode.html
│ ├── tvseason.html
│ ├── tvshow.html
│ └── work.html
├── tests
│ ├── __init__.py
│ ├── catalog.py
│ └── index.py
├── tv
│ ├── models.py
│ └── tests.py
├── urls.py
├── views.py
└── views_edit.py
├── common
├── __init__.py
├── api.py
├── apps.py
├── config.py
├── forms.py
├── management
│ └── commands
│ │ ├── cron.py
│ │ ├── jobs.py
│ │ └── sitemap.py
├── models
│ ├── __init__.py
│ ├── cron.py
│ ├── index.py
│ ├── lang.py
│ └── misc.py
├── setup.py
├── static
│ ├── img
│ │ ├── avatar.svg
│ │ ├── fediverse.svg
│ │ ├── icon.png
│ │ ├── logo.svg
│ │ ├── logo_square.jpg
│ │ ├── logo_square.svg
│ │ └── missing.png
│ ├── js
│ │ └── sort_layout.js
│ └── scss
│ │ ├── _card.scss
│ │ ├── _common.scss
│ │ ├── _dialog.scss
│ │ ├── _feed.scss
│ │ ├── _footer.scss
│ │ ├── _form.scss
│ │ ├── _gallery.scss
│ │ ├── _header.scss
│ │ ├── _item.scss
│ │ ├── _l10n.scss
│ │ ├── _layout.scss
│ │ ├── _legacy.sass
│ │ ├── _legacy2.scss
│ │ ├── _lightbox.scss
│ │ ├── _login.scss
│ │ ├── _mark.scss
│ │ ├── _markdown.scss
│ │ ├── _post.scss
│ │ ├── _rating.scss
│ │ ├── _sidebar.scss
│ │ ├── _sitelabel.scss
│ │ ├── _tag.scss
│ │ └── neodb.scss
├── templates
│ ├── 400.html
│ ├── 403.html
│ ├── 404.html
│ ├── 500.html
│ ├── 503.html
│ ├── _field.html
│ ├── _footer.html
│ ├── _header.html
│ ├── _pagination.html
│ ├── _sidebar.html
│ ├── _sidebar_anonymous.html
│ ├── common
│ │ ├── error.html
│ │ ├── info.html
│ │ ├── manifest.json.tpl
│ │ └── opensearch.xml.tpl
│ ├── common_libs.html
│ ├── console.html
│ └── widgets
│ │ ├── hstore.html
│ │ ├── image.html
│ │ ├── multi_select.html
│ │ └── tag.html
├── templatetags
│ ├── __init__.py
│ ├── admin_url.py
│ ├── duration.py
│ ├── highlight.py
│ ├── mastodon.py
│ ├── strip_scheme.py
│ ├── thumb.py
│ └── truncate.py
├── tests.py
├── urls.py
├── utils.py
└── views.py
├── compose.yml
├── journal
├── __init__.py
├── admin.py
├── apis
│ ├── __init__.py
│ ├── collection.py
│ ├── note.py
│ ├── post.py
│ ├── review.py
│ ├── shelf.py
│ └── tag.py
├── apps.py
├── exporters
│ ├── __init__.py
│ ├── csv.py
│ ├── doufen.py
│ └── ndjson.py
├── forms.py
├── importers
│ ├── __init__.py
│ ├── base.py
│ ├── csv.py
│ ├── douban.py
│ ├── goodreads.py
│ ├── letterboxd.py
│ ├── ndjson.py
│ └── opml.py
├── management
│ └── commands
│ │ ├── collection.py
│ │ ├── journal.py
│ │ └── top10.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0001_initial_0_10.py
│ ├── 0002_initial.py
│ ├── 0002_note.py
│ ├── 0003_auto_20230113_0506.py
│ ├── 0003_note_progress.py
│ ├── 0004_alter_shelflogentry_timestamp.py
│ ├── 0004_tasks.py
│ ├── 0005_auto_20230114_1134.py
│ ├── 0005_csvexporter.py
│ ├── 0006_auto_20230114_2139.py
│ ├── 0006_csvimporter.py
│ ├── 0007_alter_collection_catalog_item.py
│ ├── 0007_smart_collection.py
│ ├── 0008_alter_shelfmember_unique_together.py
│ ├── 0009_comment_focus_item.py
│ ├── 0010_shelfmember_journal_she_parent__9da946_idx.py
│ ├── 0011_performance.py
│ ├── 0012_alter_piece_polymorphic_ctype_alter_shelf_items.py
│ ├── 0013_remove_comment_focus_item.py
│ ├── 0014_remove_reply_piece_ptr_remove_reply_reply_to_content_and_more.py
│ ├── 0015_use_identity_support_remote_piece.py
│ ├── 0016_piecepost_piece_posts_piecepost_unique_piece_post.py
│ ├── 0017_alter_piece_options_and_more.py
│ ├── 0018_shelflogentrypost_shelflogentry_posts_and_more.py
│ ├── 0019_alter_collection_edited_time_and_more.py
│ ├── 0020_shelflogentry_unique_shelf_log_entry.py
│ ├── 0021_pieceinteraction_pieceinteraction_unique_interaction.py
│ ├── 0022_letterboxdimporter.py
│ ├── 0023_debris.py
│ ├── 0024_i18n.py
│ ├── 0025_pin_tags.py
│ ├── 0026_pinned_tag_index.py
│ └── __init__.py
├── models
│ ├── __init__.py
│ ├── collection.py
│ ├── comment.py
│ ├── common.py
│ ├── index.py
│ ├── itemlist.py
│ ├── like.py
│ ├── mark.py
│ ├── mixins.py
│ ├── note.py
│ ├── rating.py
│ ├── renderers.py
│ ├── review.py
│ ├── shelf.py
│ ├── tag.py
│ └── utils.py
├── static
│ ├── css
│ │ └── calendar_yearview_blocks.css
│ └── js
│ │ ├── calendar_yearview_blocks.js
│ │ └── roughviz.umd.js
├── templates
│ ├── _feature_stats.html
│ ├── _list_item.html
│ ├── _sidebar_search_journal.html
│ ├── _sidebar_user_mark_list.html
│ ├── action_boost_post.html
│ ├── action_delete_post.html
│ ├── action_flag_post.html
│ ├── action_like_post.html
│ ├── action_open_post.html
│ ├── action_pin_post.html
│ ├── action_post_timestamp.html
│ ├── action_reply_piece.html
│ ├── action_reply_post.html
│ ├── action_translate_post.html
│ ├── add_to_collection.html
│ ├── calendar_data.html
│ ├── collection.html
│ ├── collection_edit.html
│ ├── collection_items.html
│ ├── collection_share.html
│ ├── collection_update_item_note.html
│ ├── collection_update_item_note_ok.html
│ ├── comment.html
│ ├── group.html
│ ├── mark.html
│ ├── markdown.html
│ ├── note.html
│ ├── piece_delete.html
│ ├── post_compose.html
│ ├── posts.html
│ ├── profile.html
│ ├── profile_items.html
│ ├── replies.html
│ ├── review.html
│ ├── review_edit.html
│ ├── search_journal.html
│ ├── tag_edit.html
│ ├── user_collection_list.html
│ ├── user_item_list_base.html
│ ├── user_mark_list.html
│ ├── user_review_list.html
│ ├── user_tag_list.html
│ ├── user_tagmember_list.html
│ ├── wrapped.html
│ └── wrapped_share.html
├── templatetags
│ ├── __init__.py
│ ├── collection.py
│ └── user_actions.py
├── tests
│ ├── __init__.py
│ ├── csv.py
│ ├── ndjson.py
│ ├── piece.py
│ ├── rating.py
│ ├── search.py
│ ├── shelf.py
│ └── test_utils.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── collection.py
│ ├── common.py
│ ├── mark.py
│ ├── note.py
│ ├── post.py
│ ├── profile.py
│ ├── review.py
│ ├── search.py
│ ├── tag.py
│ └── wrapped.py
├── legacy
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── tests.py
├── urls.py
└── views.py
├── locale
├── da_DK
│ └── LC_MESSAGES
│ │ └── django.po
├── de
│ └── LC_MESSAGES
│ │ └── django.po
├── django.pot
├── es
│ └── LC_MESSAGES
│ │ └── django.po
├── fr
│ └── LC_MESSAGES
│ │ └── django.po
├── it
│ └── LC_MESSAGES
│ │ └── django.po
├── ja
│ └── LC_MESSAGES
│ │ └── django.po
├── lzh
│ └── LC_MESSAGES
│ │ └── django.po
├── pt
│ └── LC_MESSAGES
│ │ └── django.po
├── pt_BR
│ └── LC_MESSAGES
│ │ └── django.po
├── zh_Hans
│ └── LC_MESSAGES
│ │ └── django.po
└── zh_Hant
│ └── LC_MESSAGES
│ └── django.po
├── manage.py
├── mastodon
├── __init__.py
├── admin.py
├── apps.py
├── auth.py
├── jobs.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_add_api_domain_server_version.py
│ ├── 0003_mastodonapplication_reachable.py
│ ├── 0004_alter_mastodonapplication_api_domain_and_more.py
│ ├── 0005_socialaccount.py
│ └── __init__.py
├── models
│ ├── __init__.py
│ ├── bluesky.py
│ ├── common.py
│ ├── email.py
│ ├── mastodon.py
│ └── threads.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── bluesky.py
│ ├── common.py
│ ├── email.py
│ ├── mastodon.py
│ └── threads.py
├── misc
├── bin
│ ├── neodb-hello
│ ├── neodb-init
│ ├── neodb-manage
│ ├── neodb-version
│ ├── nginx-start
│ └── takahe-manage
├── nginx.conf.d
│ ├── neodb-dev.conf
│ └── neodb.conf
├── wheels-cache
│ └── libsass-0.23.0-cp311-abi3-linux_aarch64.whl
└── www
│ └── robots.txt
├── neodb.env.example
├── pyproject.toml
├── requirements-dev.lock
├── requirements.lock
├── social
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial_0_10.py
│ ├── 0001_initial_0_11.py
│ ├── 0002_delete_localactivity.py
│ └── __init__.py
├── models.py
├── templates
│ ├── _event_post.html
│ ├── event
│ │ ├── _post.html
│ │ ├── announcement.html
│ │ ├── boost.html
│ │ ├── boosted.html
│ │ ├── boosted_collection.html
│ │ ├── boosted_comment.html
│ │ ├── boosted_note.html
│ │ ├── boosted_rating.html
│ │ ├── boosted_review.html
│ │ ├── boosted_shelfmember.html
│ │ ├── follow_requested.html
│ │ ├── followed.html
│ │ ├── identity_created.html
│ │ ├── liked.html
│ │ ├── liked_collection.html
│ │ ├── liked_comment.html
│ │ ├── liked_note.html
│ │ ├── liked_rating.html
│ │ ├── liked_review.html
│ │ ├── liked_shelfmember.html
│ │ ├── mentioned.html
│ │ ├── mentioned_collection.html
│ │ ├── mentioned_comment.html
│ │ ├── mentioned_note.html
│ │ ├── mentioned_rating.html
│ │ ├── mentioned_review.html
│ │ ├── mentioned_shelfmember.html
│ │ └── post.html
│ ├── events.html
│ ├── feed.html
│ ├── feed_events.html
│ ├── notification.html
│ ├── post_question.html
│ ├── search_feed.html
│ └── single_post.html
├── tests.py
├── urls.py
└── views.py
├── takahe
├── __init__.py
├── admin.py
├── ap_handlers.py
├── apps.py
├── db_routes.py
├── html.py
├── jobs.py
├── management
│ └── commands
│ │ └── takahe.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── tests.py
├── uris.py
├── urls.py
├── utils.py
└── views.py
├── test_data
├── https___anchor_fm_s_64d6bbe0_podcast_rss
├── https___api_bgm_tv_v0_subjects_15912
├── https___api_bgm_tv_v0_subjects_224973
├── https___api_bgm_tv_v0_subjects_228086
├── https___api_bgm_tv_v0_subjects_237
├── https___api_bgm_tv_v0_subjects_253
├── https___api_bgm_tv_v0_subjects_342254
├── https___api_bgm_tv_v0_subjects_431295
├── https___api_bgm_tv_v0_subjects_442025
├── https___api_bgm_tv_v0_subjects_7157
├── https___api_discogs_com_masters_469004
├── https___api_discogs_com_releases_13574140
├── https___api_discogs_com_releases_25829341
├── https___api_spotify_com_v1_albums_0I8vpSE1bSmysN2PhmHoQg
├── https___api_spotify_com_v1_albums_65KwtzkJXw7oT819NFWmEP
├── https___api_themoviedb_org_3_find_tt0314979_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt0314979_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt0436992_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt0436992_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt0827573_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt0827573_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt10751754_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt10751754_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt10751820_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt10751820_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1159991_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1159991_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1375666_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1375666_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt15389382_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1699275_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1699275_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1699276_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt1699276_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt21599650_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt7660970_api_key_8964_language_en_US_external_source_imdb_id
├── https___api_themoviedb_org_3_find_tt7660970_api_key_8964_language_zh_CN_external_source_imdb_id
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_de_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_es_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_fr_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_ja_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_ko_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_27205_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_282758_api_key_8964_language_en_US_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_282758_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_282758_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_282758_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_282758_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_de_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_es_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_fr_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_ja_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_ko_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_movie_293767_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_api_key_8964_language_pt_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_season_2_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_season_2_api_key_8964_language_pt_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_season_2_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_season_2_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_season_2_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_1668_season_2_episode_1_api_key_8964_language_en_US_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_de_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_es_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_fr_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_ja_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_ko_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_1_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_1_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_1_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_1_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_de_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_es_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_fr_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_ja_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_ko_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_episode_1_api_key_8964_language_en_US_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_57243_season_4_episode_1_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_60625_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_60625_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_60625_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_60625_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_60625_season_6_episode_1_api_key_8964_language_zh_CN_append_to_response_external_ids
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_de_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_es_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_fr_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_ja_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_ko_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_season_1_episode_1_api_key_8964_language_en_US_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_season_1_episode_1_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_season_1_episode_2_api_key_8964_language_en_US_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_71365_season_1_episode_2_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_86941_api_key_8964_language_en_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_86941_api_key_8964_language_zh_CN_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_86941_api_key_8964_language_zh_HK_append_to_response_external_ids_credits
├── https___api_themoviedb_org_3_tv_86941_api_key_8964_language_zh_TW_append_to_response_external_ids_credits
├── https___app_jjwxc_net_androidapi_novelbasicinfo_novelId_5833245
├── https___archiveofourown_org_works_2080878_view_adult_true
├── https___boardgamegeek_com_xmlapi2_thing_stats_1_type_boardgame_boardgameexpansion_id_167791
├── https___book_douban_com_subject_1089243_
├── https___book_douban_com_subject_2037260_
├── https___book_douban_com_subject_35902899_
├── https___book_douban_com_works_1008677_
├── https___book_qidian_com_info_1010868264_
├── https___feeds_simplecast_com_82FI35Px
├── https___intlanthem_bandcamp_com_album_in_these_times
├── https___itunes_apple_com_lookup_id_1050430296
├── https___m_imdb_com_title_tt0314979_
├── https___m_imdb_com_title_tt0314979_episodes__season_1
├── https___m_imdb_com_title_tt0436992_
├── https___m_imdb_com_title_tt0436992_episodes__season_4
├── https___m_imdb_com_title_tt1205438_
├── https___movie_douban_com_subject_1920763_
├── https___movie_douban_com_subject_26895436_
├── https___movie_douban_com_subject_3541415_
├── https___movie_douban_com_subject_35597581_
├── https___movie_douban_com_subject_3627919_
├── https___movie_douban_com_subject_4296866_
├── https___music_apple_com_cn_album_1284391545
├── https___music_apple_com_fr_album_1284391545
├── https___music_apple_com_jp_album_1284391545
├── https___music_apple_com_kr_album_1284391545
├── https___music_apple_com_us_album_1284391545
├── https___music_douban_com_subject_1401362_
├── https___music_douban_com_subject_33551231_
├── https___open_spotify_com_album_65KwtzkJXw7oT819NFWmEP
├── https___open_spotify_com_oembed_url_https___open_spotify_com_album_65KwtzkJXw7oT819NFWmEP
├── https___podcasts_files_bbci_co_uk_b006qykl_rss
├── https___rsshub_app_ximalaya_album_51101122_0_shownote
├── https___store_steampowered_com_api_appdetails_appids_620
├── https___store_steampowered_com_app_620
├── https___tiaodao_typlog_io_feed_xml
├── https___www_books_com_tw_products_0010947886
├── https___www_digforfire_net_digforfire_radio_feed_xml
├── https___www_douban_com_game_10734307_
├── https___www_douban_com_location_drama_20270776_
├── https___www_douban_com_location_drama_24311571_
├── https___www_douban_com_location_drama_24849279_
├── https___www_douban_com_location_drama_25883969_
├── https___www_goodreads_com_book_show_11798823
├── https___www_goodreads_com_book_show_13079982
├── https___www_goodreads_com_book_show_3597767
├── https___www_goodreads_com_book_show_40961427
├── https___www_goodreads_com_book_show_45064996
├── https___www_goodreads_com_book_show_56821625
├── https___www_goodreads_com_book_show_59952545
├── https___www_goodreads_com_book_show_77566
├── https___www_goodreads_com_work_editions_1272463
├── https___www_goodreads_com_work_editions_1383900
├── https___www_goodreads_com_work_editions_153313
├── https___www_goodreads_com_work_editions_24173962
├── https___www_googleapis_com_books_v1_volumes_hV__zQEACAAJ
├── https___www_googleapis_com_books_v1_volumes_hV__zQEACAAJ_key_8964_E
├── https___www_imdb_com_title_tt10751754_
├── https___www_imdb_com_title_tt10751820_
├── https___www_ypshuo_com_api_novel_getInfo_novelId_1
├── igdb_games_fields____cover_url__genres_name__platforms_name__involved_companies____involved_companies_company_name__where_url____https___www_igdb_com_games_portal_2__
├── igdb_games_fields____cover_url__genres_name__platforms_name__involved_companies____involved_companies_company_name__where_url____https___www_igdb_com_games_the_legend_of_zelda_breath_of_the_wild__
├── igdb_websites_fields____game____where_url____https___store_steampowered_com_app_620__
├── igdb_websites_fields____where_game_url____https___www_igdb_com_games_portal_2__
└── igdb_websites_fields____where_game_url____https___www_igdb_com_games_the_legend_of_zelda_breath_of_the_wild__
└── users
├── __init__.py
├── admin.py
├── apis.py
├── apps.py
├── jobs
├── __init__.py
└── sync.py
├── management
└── commands
│ ├── invite.py
│ ├── task.py
│ └── user.py
├── middlewares.py
├── migrations
├── 0001_initial.py
├── 0001_initial_0_10.py
├── 0002_preference_auto_bookmark_cats.py
├── 0002_preference_default_no_share.py
├── 0003_preference_discover_layout.py
├── 0003_remove_preference_no_anonymous_view.py
├── 0004_alter_preference_classic_homepage.py
├── 0004_remove_user_at_least_one_login_method_and_more.py
├── 0005_add_dedicated_username.py
├── 0005_remove_follow_owner_remove_follow_target_and_more.py
├── 0006_alter_task_type.py
├── 0006_unique_email.py
├── 0007_alter_task_type.py
├── 0007_user_pending_email.py
├── 0007_username_case_insensitive.py
├── 0008_alter_task_type.py
├── 0008_user_at_least_one_login_method.py
├── 0009_add_local_follow.py
├── 0010_add_local_mute_block.py
├── 0011_preference_hidden_categories.py
├── 0012_apidentity.py
├── 0013_init_identity.py
├── 0014_preference_mastodon_skip_relationship_and_more.py
├── 0015_remove_preference_mastodon_publish_public_and_more.py
├── 0015_user_mastodon_last_reachable.py
├── 0016_rename_preference_default_no_share.py
├── 0017_mastodon_site_username_bd2db5_idx.py
├── 0018_apidentity_anonymous_viewable.py
├── 0019_task.py
├── 0020_user_language.py
├── 0021_alter_user_language.py
└── __init__.py
├── models
├── __init__.py
├── apidentity.py
├── preference.py
├── task.py
└── user.py
├── templates
└── users
│ ├── _pref_device.html
│ ├── _profile_social_icons.html
│ ├── account.html
│ ├── announcements.html
│ ├── data.html
│ ├── fetch_identity_failed.html
│ ├── fetch_identity_pending.html
│ ├── fetch_identity_refresh.html
│ ├── home_anonymous.html
│ ├── login.html
│ ├── preferences.html
│ ├── preferences_anonymous.html
│ ├── profile_actions.html
│ ├── register.html
│ ├── relationship_list.html
│ ├── user_task_status.html
│ ├── verify.html
│ ├── verify_email.html
│ └── welcome.html
├── tests.py
├── urls.py
└── views
├── __init__.py
├── account.py
├── actions.py
├── data.py
└── profile.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | .venv
4 | .vscode
5 | .github
6 | .gitignore
7 | .pre-commit-config.yaml
8 | __pycache__
9 | /compose.yml
10 | /compose.override.yml
11 | /Dockerfile
12 | /docs
13 | /mkdocs.yml
14 | /media
15 | /static
16 | /test_data
17 | /neodb
18 | /neodb-takahe/docs
19 | /neodb-takahe/docker
20 | /neodb-takahe/static-collected
21 | /neodb-takahe/takahe/local_settings.py
22 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: neodb
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: ''
6 | assignees: alphatownsman
7 |
8 | ---
9 |
10 | **Description**
11 | A clear and concise description of what the bug is.
12 |
13 |
14 | **Expected Behavior**
15 | What you expected to happen.
16 |
17 |
18 | **Actual Behavior / Steps to Reproduce**
19 | What you expected happens instead.
20 | If applicable, add an unambiguous set of steps to reproduce this bug.
21 |
22 |
23 | **Possible Fix**
24 | Not obligatory, but suggest a fix or reason for the bug.
25 |
26 |
27 | **Additional Context / Screenshots / Server Info / Browser or App**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[Request]"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: code check
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ['3.12']
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v5
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | cache: pip
18 | - name: Run pre-commit
19 | run: |
20 | python -m pip install pre_commit
21 | SKIP=pyright python -m pre_commit run -a --show-diff-on-failure
22 | type-checker:
23 | runs-on: ubuntu-latest
24 | strategy:
25 | matrix:
26 | python-version: ['3.12']
27 | steps:
28 | - uses: actions/checkout@v4
29 | with:
30 | submodules: 'true'
31 | - name: Set up Python ${{ matrix.python-version }}
32 | uses: actions/setup-python@v5
33 | with:
34 | python-version: ${{ matrix.python-version }}
35 | cache: pip
36 | - name: Install dependencies
37 | run: |
38 | python -m pip install -r requirements-dev.lock
39 | python -m pip install -r requirements.lock
40 | - name: Run pyright
41 | run: |
42 | python -m pyright
43 |
--------------------------------------------------------------------------------
/.github/workflows/mirror.yml:
--------------------------------------------------------------------------------
1 | name: Mirror to Codeberg
2 |
3 | on: [push, delete]
4 |
5 | jobs:
6 | to_codeberg:
7 | if: github.repository_owner == 'neodb-social'
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | with:
12 | fetch-depth: 0
13 | - uses: pixta-dev/repository-mirroring-action@v1
14 | with:
15 | target_repo_url:
16 | git@codeberg.org:NeoDB/neodb.git
17 | ssh_private_key:
18 | ${{ secrets.CODEBERG_SSH_PRIVATEKEY }}
19 |
--------------------------------------------------------------------------------
/.github/workflows/preview-deploy.yml:
--------------------------------------------------------------------------------
1 | name: Preview Deployment
2 |
3 | on:
4 | workflow_run:
5 | workflows: ["Development Image"]
6 | types:
7 | - completed
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | deployment:
15 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
16 | name: deployment to preview environment
17 | runs-on: ubuntu-latest
18 | environment: ${{ vars.DEPLOY_ENV }}
19 | steps:
20 | - name: Send start notification
21 | uses: appleboy/discord-action@master
22 | with:
23 | webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
24 | webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
25 | color: "#6848a9"
26 | message: Deployment ${{ github.ref_name }} started
27 | - name: ssh
28 | uses: appleboy/ssh-action@master
29 | with:
30 | host: ${{ secrets.SSH_HOST }}
31 | port: ${{ secrets.SSH_PORT }}
32 | username: ${{ secrets.SSH_USER }}
33 | key: ${{ secrets.SSH_KEY }}
34 | script: ${{ vars.DEPLOY_SCRIPT }}
35 | - name: Send complete notification
36 | uses: appleboy/discord-action@master
37 | with:
38 | webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
39 | webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
40 | color: "#6848a9"
41 | message: Deployment ${{ github.ref_name }} complete
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .venv
3 | /.env
4 | /neodb.env
5 | /compose.override.yml
6 | /typings
7 |
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # VS Code configuration files
14 | .vscode/
15 |
16 | # Zed settings
17 | .zed
18 |
19 | # flake8
20 | tox.ini
21 |
22 | # Local sqlite3 db
23 | *.sqlite3
24 |
25 | # deployed media and static files
26 | /media
27 | /static
28 | /scripts/
29 |
30 | # generated css
31 | /common/static/css/boofilsic.min.css
32 | /common/static/css/boofilsic.css
33 | /common/static/scss/neodb.css
34 |
35 | # debug log file
36 | /log
37 | log
38 |
39 | # conf folders
40 | /conf
41 | /neodb
42 | /playground
43 |
44 | # typesense folder
45 | /typesense-data
46 |
47 | # test coverage
48 | /.coverage
49 |
50 |
51 | # translations
52 | *.mo
53 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "neodb-takahe"]
2 | path = neodb-takahe
3 | url = https://github.com/neodb-social/neodb-incarnator.git
4 | branch = neodb
5 |
--------------------------------------------------------------------------------
/FEDERATION.md:
--------------------------------------------------------------------------------
1 | # Federation
2 |
3 | see [doc](https://neodb.net/internals/federation.md) for FEP-67ff related information.
4 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | 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.
6 |
--------------------------------------------------------------------------------
/boofilsic/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "dev"
2 |
--------------------------------------------------------------------------------
/boofilsic/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for boofilsic project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "boofilsic.settings")
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/boofilsic/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | def site_info(request):
5 | return settings.SITE_INFO
6 |
--------------------------------------------------------------------------------
/boofilsic/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for boofilsic project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "boofilsic.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/catalog/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/catalog/__init__.py
--------------------------------------------------------------------------------
/catalog/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/catalog/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CatalogConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "catalog"
7 |
8 | def ready(self):
9 | # load key modules in proper order, make sure class inject and signal works as expected
10 | from catalog import apis, models, sites # noqa
11 | from catalog.models import init_catalog_audit_log
12 | from journal import models as journal_models # noqa
13 |
14 | # register cron jobs
15 | from catalog.jobs import DiscoverGenerator, PodcastUpdater # noqa
16 |
17 | init_catalog_audit_log()
18 |
--------------------------------------------------------------------------------
/catalog/collection/models.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | from catalog.common import Item, ItemCategory
4 |
5 |
6 | class Collection(Item):
7 | if TYPE_CHECKING:
8 | from journal.models import Collection as JournalCollection
9 |
10 | journal_item: "JournalCollection"
11 | category = ItemCategory.Collection
12 |
13 | @property
14 | def url(self):
15 | return self.journal_item.url if hasattr(self, "journal_item") else super().url
16 |
17 | @property
18 | def owner_id(self):
19 | return self.journal_item.owner_id if self.journal_item else None
20 |
21 | def to_indexable_doc(self):
22 | return {}
23 |
--------------------------------------------------------------------------------
/catalog/common/__init__.py:
--------------------------------------------------------------------------------
1 | from . import jsondata
2 | from .downloaders import *
3 | from .models import *
4 | from .scrapers import *
5 | from .sites import *
6 |
7 | __all__ = ( # noqa
8 | "IdType",
9 | "SiteName",
10 | "ItemCategory",
11 | "AvailableItemCategory",
12 | "Item",
13 | "ExternalResource",
14 | "ResourceContent",
15 | "ParseError",
16 | "AbstractSite",
17 | "SiteManager",
18 | "jsondata",
19 | "PrimaryLookupIdDescriptor",
20 | "LookupIdDescriptor",
21 | "get_mock_mode",
22 | "get_mock_file",
23 | "use_local_response",
24 | "RetryDownloader",
25 | "BasicDownloader",
26 | "BasicDownloader2",
27 | "CachedDownloader",
28 | "ProxiedDownloader",
29 | "BasicImageDownloader",
30 | "ProxiedImageDownloader",
31 | "RESPONSE_OK",
32 | "RESPONSE_NETWORK_ERROR",
33 | "RESPONSE_INVALID_CONTENT",
34 | "RESPONSE_CENSORSHIP",
35 | )
36 |
--------------------------------------------------------------------------------
/catalog/common/scrapers.py:
--------------------------------------------------------------------------------
1 | class ParseError(Exception):
2 | def __init__(self, scraper, field):
3 | msg = f'{type(scraper).__name__}: Error parsing field "{field}" for url {scraper.url}'
4 | super().__init__(msg)
5 |
--------------------------------------------------------------------------------
/catalog/common/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from common.models import (
4 | SITE_PREFERRED_LANGUAGES,
5 | SITE_PREFERRED_LOCALES,
6 | detect_language,
7 | )
8 |
9 |
10 | class CommonTestCase(TestCase):
11 | databases = "__all__"
12 |
13 | def test_detect_lang(self):
14 | lang = detect_language("The Witcher 3: Wild Hunt")
15 | self.assertEqual(lang, "en")
16 | lang = detect_language("巫师3:狂猎")
17 | self.assertEqual(lang, "zh-cn")
18 | lang = detect_language("巫师3:狂猎 The Witcher 3: Wild Hunt")
19 | self.assertEqual(lang, "zh-cn")
20 |
21 | def test_lang_list(self):
22 | self.assertGreaterEqual(len(SITE_PREFERRED_LANGUAGES), 1)
23 | self.assertGreaterEqual(len(SITE_PREFERRED_LOCALES), 1)
24 |
--------------------------------------------------------------------------------
/catalog/common/utils.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.utils import timezone
4 |
5 |
6 | def resource_cover_path(resource, filename):
7 | fn = (
8 | timezone.now().strftime("%Y/%m/%d/")
9 | + str(uuid.uuid4())
10 | + "."
11 | + filename.split(".")[-1]
12 | )
13 | return "item/" + resource.id_type + "/" + fn
14 |
15 |
16 | def item_cover_path(item, filename):
17 | fn = (
18 | timezone.now().strftime("%Y/%m/%d/")
19 | + str(uuid.uuid4())
20 | + "."
21 | + filename.split(".")[-1]
22 | )
23 | return "item/" + item.category + "/" + fn
24 |
25 |
26 | def piece_cover_path(item, filename):
27 | fn = (
28 | timezone.now().strftime("%Y/%m/%d/")
29 | + str(uuid.uuid4())
30 | + "."
31 | + filename.split(".")[-1]
32 | )
33 | return f"user/{item.owner_id or '_'}/{fn}"
34 |
--------------------------------------------------------------------------------
/catalog/jobs/__init__.py:
--------------------------------------------------------------------------------
1 | from .discover import DiscoverGenerator
2 | from .podcast import PodcastUpdater
3 |
4 | __all__ = [
5 | "DiscoverGenerator",
6 | "PodcastUpdater",
7 | ]
8 |
--------------------------------------------------------------------------------
/catalog/jobs/podcast.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from loguru import logger
4 |
5 | from catalog.common.models import IdType
6 | from catalog.models import Podcast
7 | from catalog.sites import RSS
8 | from common.models import BaseJob, JobManager
9 |
10 |
11 | @JobManager.register
12 | class PodcastUpdater(BaseJob):
13 | interval = timedelta(hours=2)
14 |
15 | def run(self):
16 | logger.info("Podcasts update start.")
17 | count = 0
18 | qs = Podcast.objects.filter(
19 | is_deleted=False, merged_to_item__isnull=True
20 | ).order_by("pk")
21 | for p in qs:
22 | if (
23 | p.primary_lookup_id_type == IdType.RSS
24 | and p.primary_lookup_id_value is not None
25 | ):
26 | logger.info(f"updating {p}")
27 | c = p.episodes.count()
28 | site = RSS(p.feed_url)
29 | r = site.scrape_additional_data()
30 | if r:
31 | c2 = p.episodes.count()
32 | logger.info(f"updated {p}, {c2 - c} new episodes.")
33 | count += c2 - c
34 | else:
35 | logger.warning(f"failed to update {p}")
36 | logger.info(f"Podcasts update finished, {count} new episodes total.")
37 |
--------------------------------------------------------------------------------
/catalog/migrations/0002_fix_soft_deleted_edition.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.18 on 2025-02-09 02:40
2 |
3 | import django_rq
4 | from django.db import migrations
5 |
6 | from catalog.common.migrations import fix_20250208
7 |
8 |
9 | # this is a fix for a bug introduced in 0.10
10 | # safe to be excluded in squashed migration in future
11 | def fix_soft_deleted_edition(apps, schema_editor):
12 | django_rq.get_queue("mastodon").enqueue(fix_20250208)
13 | print("(Queued)", end="")
14 |
15 |
16 | class Migration(migrations.Migration):
17 | dependencies = [
18 | ("catalog", "0001_initial_0_10"),
19 | ]
20 |
21 | operations = [
22 | migrations.RunPython(fix_soft_deleted_edition),
23 | ]
24 |
--------------------------------------------------------------------------------
/catalog/migrations/0004_podcast_no_real_change.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-02-03 21:58
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("catalog", "0003_podcast"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="podcastepisode",
15 | name="item_ptr",
16 | field=models.OneToOneField(
17 | auto_created=True,
18 | on_delete=django.db.models.deletion.CASCADE,
19 | parent_link=True,
20 | primary_key=True,
21 | serialize=False,
22 | to="catalog.item",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/catalog/migrations/0008_delete_historicalitem.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.19 on 2023-06-19 14:28
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("catalog", "0007_performance"),
9 | ]
10 |
11 | operations = [
12 | migrations.DeleteModel(
13 | name="HistoricalItem",
14 | ),
15 | ]
16 |
--------------------------------------------------------------------------------
/catalog/migrations/0009_remove_tvepisode_show.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.19 on 2023-06-19 23:47
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("catalog", "0008_delete_historicalitem"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="tvepisode",
14 | name="show",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/catalog/migrations/0010_alter_item_polymorphic_ctype.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-06 22:53
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("contenttypes", "0002_remove_content_type_name"),
10 | ("catalog", "0009_remove_tvepisode_show"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="item",
16 | name="polymorphic_ctype",
17 | field=models.ForeignKey(
18 | editable=False,
19 | null=True,
20 | on_delete=django.db.models.deletion.CASCADE,
21 | related_name="polymorphic_%(app_label)s.%(class)s_set+",
22 | to="contenttypes.contenttype",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/catalog/migrations/0011_remove_item_last_editor.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.4 on 2023-08-11 20:13
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("catalog", "0010_alter_item_polymorphic_ctype"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="item",
14 | name="last_editor",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/catalog/migrations/0013_migrate_work.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.18 on 2025-01-27 05:56
2 |
3 | import django_rq
4 | from django.conf import settings
5 | from django.db import migrations
6 |
7 | from catalog.common.migrations import fix_bangumi_20250420, merge_works_20250301
8 |
9 |
10 | def merge_works(apps, schema_editor):
11 | skips = getattr(settings, "SKIP_MIGRATIONS", [])
12 | if "merge_works" in skips:
13 | print("(Skipped)", end="")
14 | else:
15 | django_rq.get_queue("cron").enqueue(merge_works_20250301)
16 | print("(Queued)", end="")
17 | if "fix_bangumi" in skips:
18 | print("(Skipped)", end="")
19 | else:
20 | django_rq.get_queue("cron").enqueue(fix_bangumi_20250420)
21 | print("(Queued)", end="")
22 |
23 |
24 | class Migration(migrations.Migration):
25 | dependencies = [
26 | ("catalog", "0002_fix_soft_deleted_edition"),
27 | ]
28 |
29 | operations = [
30 | migrations.RunPython(merge_works),
31 | ]
32 |
--------------------------------------------------------------------------------
/catalog/migrations/0014_auto_reindex.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.20 on 2025-04-24 03:46
2 |
3 | import django_rq
4 | from django.conf import settings
5 | from django.db import migrations
6 |
7 | from catalog.common.migrations import reindex_20250424
8 |
9 |
10 | def queue_reindex(apps, schema_editor):
11 | skips = getattr(settings, "SKIP_MIGRATIONS", [])
12 | if "reindex" in skips:
13 | print("(Skipped)", end="")
14 | else:
15 | django_rq.get_queue("cron").enqueue(reindex_20250424)
16 | print("(Queued)", end="")
17 |
18 |
19 | class Migration(migrations.Migration):
20 | dependencies = [
21 | ("catalog", "0013_migrate_work"),
22 | ]
23 |
24 | operations = [
25 | migrations.RunPython(queue_reindex),
26 | ]
27 |
--------------------------------------------------------------------------------
/catalog/migrations/0015_item_add_is_protected.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.20 on 2025-05-07 21:42
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("catalog", "0014_auto_reindex"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="item",
14 | name="is_protected",
15 | field=models.BooleanField(null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/catalog/migrations/0016_rename_item_primary_lookup_id_type_primary_lookup_id_value_catalog_ite_primary_0a089c_idx_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.21 on 2025-05-21 04:39
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("catalog", "0015_item_add_is_protected"),
9 | ]
10 |
11 | operations = [
12 | migrations.RenameIndex(
13 | model_name="item",
14 | new_name="catalog_ite_primary_0a089c_idx",
15 | old_fields=("primary_lookup_id_type", "primary_lookup_id_value"),
16 | ),
17 | migrations.RenameIndex(
18 | model_name="podcastepisode",
19 | new_name="catalog_pod_program_2e4327_idx",
20 | old_fields=("program", "pub_date"),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/catalog/migrations/0017_normalize_lang.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.21 on 2025-05-24 17:00
2 |
3 | from datetime import timedelta
4 |
5 | import django_rq
6 | from django.conf import settings
7 | from django.db import migrations
8 |
9 | from catalog.common.migrations import normalize_language_20250524
10 |
11 |
12 | def queue_job(apps, schema_editor):
13 | skips = getattr(settings, "SKIP_MIGRATIONS", [])
14 | if "normalize_lang" in skips:
15 | print("(Skipped)", end="")
16 | else:
17 | django_rq.get_queue("cron").enqueue_in(
18 | timedelta(seconds=5), normalize_language_20250524
19 | )
20 | print("(Queued)", end="")
21 |
22 |
23 | class Migration(migrations.Migration):
24 | dependencies = [
25 | (
26 | "catalog",
27 | "0016_rename_item_primary_lookup_id_type_primary_lookup_id_value_catalog_ite_primary_0a089c_idx_and_more",
28 | ),
29 | ]
30 |
31 | operations = [
32 | migrations.RunPython(queue_job),
33 | ]
34 |
--------------------------------------------------------------------------------
/catalog/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/catalog/migrations/__init__.py
--------------------------------------------------------------------------------
/catalog/music/utils.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | def upc_to_gtin_13(upc: str):
5 | """
6 | Convert UPC-A to GTIN-13, return None if validation failed
7 |
8 | may add or remove padding 0s from different source
9 | """
10 | s = upc.strip() if upc else ""
11 | if not re.match(r"^\d+$", s):
12 | return None
13 | if len(s) < 13:
14 | s = s.zfill(13)
15 | elif len(s) > 13:
16 | if re.match(r"^0+$", s[0 : len(s) - 13]):
17 | s = s[len(s) - 13 :]
18 | else:
19 | return None
20 | return s
21 |
--------------------------------------------------------------------------------
/catalog/sites/jjwxc.py:
--------------------------------------------------------------------------------
1 | import html
2 |
3 | from catalog.common import *
4 | from catalog.models import *
5 |
6 |
7 | @SiteManager.register
8 | class JJWXC(AbstractSite):
9 | SITE_NAME = SiteName.JJWXC
10 | ID_TYPE = IdType.JJWXC
11 | URL_PATTERNS = [
12 | r"https://www\.jjwxc\.net/onebook\.php\?novelid=(\d+)",
13 | ]
14 | WIKI_PROPERTY_ID = ""
15 | DEFAULT_MODEL = Edition
16 |
17 | @classmethod
18 | def id_to_url(cls, id_value):
19 | return f"https://www.jjwxc.net/onebook.php?novelid={id_value}"
20 |
21 | def scrape(self):
22 | api_url = (
23 | f"https://app.jjwxc.net/androidapi/novelbasicinfo?novelId={self.id_value}"
24 | )
25 | o = BasicDownloader(api_url).download().json()
26 | return ResourceContent(
27 | metadata={
28 | "localized_title": [{"lang": "zh-cn", "text": o["novelName"]}],
29 | "author": [o["authorName"]],
30 | "format": Edition.BookFormat.WEB,
31 | "localized_description": [
32 | {
33 | "lang": "zh-cn",
34 | "text": html.unescape(o["novelIntro"]).replace(" ", "\n"),
35 | }
36 | ],
37 | "cover_image_url": o["novelCover"],
38 | },
39 | )
40 |
--------------------------------------------------------------------------------
/catalog/templates/_actor.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if people %}
3 |
4 | {% if role %}
5 | {% trans role %}:
6 | {% endif %}
7 | {% for p in people %}
8 | {% if forloop.counter <= max %}
9 | {% if not forloop.first %}/{% endif %}
10 | {{ p.name }}
11 | {% if p.role.strip %}(as {{ p.role }}){% endif %}
12 |
13 | {% elif forloop.last %}
14 | …
15 | {% endif %}
16 | {% endfor %}
17 |
18 | {% endif %}
19 |
--------------------------------------------------------------------------------
/catalog/templates/_crew.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% for role, list in people.items %}
3 |
4 | {% trans role %}:
5 | {% for p in list %}
6 | {% if forloop.counter <= max %}
7 | {% if not forloop.first %}/{% endif %}
8 | {{ p }}
9 | {% elif forloop.last %}
10 | …
11 | {% endif %}
12 | {% endfor %}
13 |
14 | {% endfor %}
15 |
--------------------------------------------------------------------------------
/catalog/templates/_fetch_failed.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 | {% trans "Unable to fetch from the link, please check again. Some sites may require login for certain links, please manually create them here." %}
5 |
6 |
--------------------------------------------------------------------------------
/catalog/templates/_fetch_refresh.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% if allow_embed and item.get_embed_link %}
4 |
5 |
6 |
7 | {{ item.display_title }}
8 |
9 | {% if not hide_category %}[{{ item.category.label }}] {% endif %}
10 |
11 | {% for res in item.external_resources.all %}
12 | {{ res.site_label }}
15 | {% endfor %}
16 |
17 |
18 |
19 |
20 |
27 |
28 | {% else %}
29 |
30 |
36 |
37 | {% with "_item_card_metadata_"|add:item.class_name|add:".html" as template %}
38 | {% include template %}
39 | {% endwith %}
40 |
41 |
42 | {% endif %}
43 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_album.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.artist role='' max=2 %}
10 | {% include '_people.html' with people=item.genre role='genre' max=5 %}
11 |
12 | {% endblock brief %}
13 | {% block full %}
14 |
15 | {% if item.release_date %}
16 | {% trans "release date" %} {{ item.release_date }}
17 | {% endif %}
18 | {% include '_people.html' with people=item.company role='publisher' max=2 %}
19 |
20 |
21 |
22 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
23 |
24 |
25 | {% endblock full %}
26 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_collection.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load collection %}
4 | {% load i18n %}
5 | {% block brief %}
6 | {% with collection=item.journal_item %}
7 |
8 |
9 | {{ collection.owner.display_name }}
10 | @{{ collection.owner.handle }}
11 |
12 |
13 | {% for cat, count in collection.get_summary.items %}
14 | {% if count %}
15 | {% prural_items count cat %}
16 | {% endif %}
17 | {% endfor %}
18 |
19 |
20 | {% endwith %}
21 | {% endblock brief %}
22 | {% block full %}
23 |
24 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
25 |
26 | {% endblock full %}
27 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_edition.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.author role='author' max=2 %}
10 | {% include '_people.html' with people=item.translator role='translator' max=2 %}
11 | {% if item.pub_house %}{{ item.pub_house }} {% endif %}
12 | {% if item.pub_year %}
13 |
14 | {{ item.pub_year }}
15 | {% if item.pub_month %}- {{ item.pub_month }}{% endif %}
16 |
17 | {% endif %}
18 | {% include '_people.html' with people=item.additional_title role='other title' max=2 %}
19 |
20 | {% endblock brief %}
21 | {% block full %}
22 |
23 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
24 |
25 | {% endblock full %}
26 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_game.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.additional_title role='other title' max=2 %}
10 | {% if item.release_date %}{{ item.release_date }} {% endif %}
11 |
12 | {% endblock brief %}
13 | {% block full %}
14 |
15 | {% include '_people.html' with people=item.genre role='genre' max=2 %}
16 | {% include '_people.html' with people=item.platform role='platform' max=5 %}
17 | {% include '_people.html' with people=item.developer role='developer' max=2 %}
18 | {% include '_people.html' with people=item.publisher role='publisher' max=2 %}
19 |
20 |
21 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
22 |
23 | {% endblock full %}
24 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_movie.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.director role='director' max=2 %}
10 | {% include '_people.html' with people=item.actor role='actor' max=2 %}
11 |
12 | {% endblock brief %}
13 | {% block full %}
14 | {% include '_people.html' with people=item.additional_title role='other title' max=2 %}
15 |
16 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
17 |
18 | {% endblock full %}
19 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_podcast.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.host role='host' max=5 %}
10 |
11 | {% endblock brief %}
12 | {% block full %}
13 |
14 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
15 |
16 | {% endblock full %}
17 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_podcastepisode.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_tvepisode.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_tvseason.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.director role='director' max=2 %}
10 | {% include '_people.html' with people=item.actor role='actor' max=2 %}
11 |
12 | {% endblock brief %}
13 | {% block full %}
14 |
15 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
16 |
17 | {% endblock full %}
18 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_tvshow.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.director role='director' max=2 %}
10 | {% include '_people.html' with people=item.actor role='actor' max=2 %}
11 |
12 | {% endblock brief %}
13 | {% block full %}
14 | {% include '_people.html' with people=item.additional_title role='other title' max=2 %}
15 |
16 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
17 |
18 | {% endblock full %}
19 |
--------------------------------------------------------------------------------
/catalog/templates/_item_card_metadata_work.html:
--------------------------------------------------------------------------------
1 | {% extends "_item_card_metadata_base.html" %}
2 | {% load humanize %}
3 | {% load i18n %}
4 | {% block brief %}
5 |
6 | {% if item.rating %}
7 | {{ item.rating | floatformat:1 }} ({{ item.rating_count }} {% trans "ratings" %})
8 | {% endif %}
9 | {% include '_people.html' with people=item.author role='author' max=2 %}
10 |
11 | {% endblock brief %}
12 | {% block full %}
13 | {% include '_people.html' with people=item.additional_title role='other title' max=2 %}
14 |
15 | {% if not hide_brief %}{{ item.display_description | linebreaksbr }}{% endif %}
16 |
17 | {% endblock full %}
18 |
--------------------------------------------------------------------------------
/catalog/templates/_item_user_mark_history.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% for log in mark.logs %}
4 |
5 | {{ log.timestamp|date }}
6 | {{ log.action_label }}
7 |
8 |
10 |
11 |
12 |
13 |
14 | {% empty %}
15 | {% trans "no history." %}
16 | {% endfor %}
17 |
18 |
19 |
20 | {% trans "clear mark history" %}
24 |
25 |
{% trans "clear history will not remove current mark" %}
26 |
27 |
--------------------------------------------------------------------------------
/catalog/templates/_language_list.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% if list %}
4 |
5 | {% trans 'language' %}:
6 | {% for p in list %}
7 | {% if forloop.counter <= max %}
8 | {% if not forloop.first %}/{% endif %}
9 | {{ p|code_to_lang }}
10 | {% elif forloop.last %}
11 | …
12 | {% endif %}
13 | {% endfor %}
14 |
15 | {% endif %}
16 |
--------------------------------------------------------------------------------
/catalog/templates/_people.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if people %}
3 |
4 | {% if role %}
5 | {% trans role %}:
6 | {% endif %}
7 | {% for p in people %}
8 | {% if forloop.counter <= max %}
9 | {% if not forloop.first %}/{% endif %}
10 | {{ p }}
11 | {% elif forloop.last %}
12 | …
13 | {% endif %}
14 | {% endfor %}
15 |
16 | {% endif %}
17 |
--------------------------------------------------------------------------------
/catalog/templates/_sidebar_item.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load thumb %}
4 |
5 |
10 |
17 |
18 |
--------------------------------------------------------------------------------
/catalog/templates/embed_base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load humanize %}
5 | {% load mastodon %}
6 | {% load strip_scheme %}
7 | {% load thumb %}
8 | {% get_current_language as LANGUAGE_CODE %}
9 |
10 |
11 |
12 |
13 |
14 |
15 | {% block head %}{% endblock %}
16 | {{ site_name }} - {% trans item.category.label %} | {{ item.display_title }}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/catalog/templates/embed_podcast.html:
--------------------------------------------------------------------------------
1 | {% extends "embed_base.html" %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% load l10n %}
5 | {% load humanize %}
6 | {% load mastodon %}
7 | {% load strip_scheme %}
8 | {% load thumb %}
9 | {% block head %}
10 |
11 |
13 |
23 |
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/catalog/templates/external_search_results.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load humanize %}
5 | {% load mastodon %}
6 | {% load thumb %}
7 |
8 | {% if external_items %}
9 |
10 | {% trans "Some items were found from other websites and instances, click their title to save them locally." %}
11 |
12 | {% endif %}
13 | {% for item in external_items %}
14 |
15 |
{% include "_item_card.html" with item=item %}
16 | {% endfor %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/catalog/templates/fetch_pending.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load humanize %}
5 | {% load mastodon %}
6 | {% load thumb %}
7 | {% get_current_language as LANGUAGE_CODE %}
8 |
9 |
10 |
11 |
12 |
13 | {{ site_name }} - {% trans 'Search results' %}
14 | {% include "common_libs.html" %}
15 |
16 |
17 | {% include '_header.html' %}
18 |
19 |
20 |
21 | {% if job_id %}
22 | {% blocktrans with site_label=site.SITE_NAME.label %}Fetching from {{ site_label }}{% endblocktrans %}
23 |
26 |
27 |
28 | {% else %}
29 | {% trans "System busy, please try again in a minute." %}
30 | {% endif %}
31 |
32 |
33 |
34 | {% include "_sidebar_search.html" %}
35 |
36 |
37 | {% include '_footer.html' %}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/catalog/templates/item.html:
--------------------------------------------------------------------------------
1 | {% extends "item_base.html" %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% load l10n %}
5 | {% load humanize %}
6 | {% load mastodon %}
7 | {% load strip_scheme %}
8 | {% load thumb %}
9 |
10 | {% block details %}{% endblock %}
11 |
12 | {% block sidebar %}{% endblock %}
13 |
--------------------------------------------------------------------------------
/catalog/templates/podcastepisode.html:
--------------------------------------------------------------------------------
1 | {% extends "item_base.html" %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% load l10n %}
5 | {% load humanize %}
6 | {% load mastodon %}
7 | {% load strip_scheme %}
8 | {% load thumb %}
9 | {% block head %}
10 | {% if item.parent_item %}
11 |
13 | {% endif %}
14 |
16 |
18 | {% if item.media_url and item.parent_item %}
19 |
20 |
22 |
23 |
24 | {% endif %}
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/catalog/templates/tvepisode.html:
--------------------------------------------------------------------------------
1 | {% extends "item_base.html" %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% load l10n %}
5 | {% load humanize %}
6 | {% load mastodon %}
7 | {% load strip_scheme %}
8 | {% load thumb %}
9 | {% block head %}
10 | {% if item.parent_item %}
11 |
13 | {% endif %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/catalog/templates/work.html:
--------------------------------------------------------------------------------
1 | {% extends "item_base.html" %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% load l10n %}
5 | {% load humanize %}
6 | {% load mastodon %}
7 | {% load strip_scheme %}
8 | {% load thumb %}
9 |
10 | {% block details %}
11 | {% include '_people.html' with people=item.additional_title role='' max=99 %}
12 | {% include '_people.html' with people=item.author role='author' max=5 %}
13 | {% endblock %}
14 | {% block left_sidebar %}
15 |
16 |
17 | {% trans 'Editions' %}
18 | {% for b in item.editions.all %}
19 |
23 | {% endfor %}
24 |
25 |
26 | {% endblock %}
27 |
28 | {% block sidebar %}{% endblock %}
29 |
--------------------------------------------------------------------------------
/catalog/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .catalog import *
2 | from .index import *
3 |
--------------------------------------------------------------------------------
/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/common/__init__.py
--------------------------------------------------------------------------------
/common/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.core.checks import Tags, register
3 | from django.db.models.signals import post_migrate
4 |
5 |
6 | class CommonConfig(AppConfig):
7 | name = "common"
8 |
9 | def ready(self):
10 | post_migrate.connect(self.setup, sender=self)
11 |
12 | def setup(self, **kwargs):
13 | from .setup import Setup
14 |
15 | if kwargs.get("using", "") == "default":
16 | # only run setup on the default database, not on takahe
17 | Setup().run()
18 |
19 |
20 | @register(Tags.admin, deploy=True)
21 | def setup_check(app_configs, **kwargs):
22 | from .setup import Setup
23 |
24 | return Setup().check()
25 |
--------------------------------------------------------------------------------
/common/config.py:
--------------------------------------------------------------------------------
1 | # how many items are showed in one search result page
2 | ITEMS_PER_PAGE = 20
3 | ITEMS_PER_PAGE_OPTIONS = [20, 40, 80]
4 |
5 | # how many pages links in the pagination
6 | PAGE_LINK_NUMBER = 7
7 |
8 | # max tags on list page
9 | TAG_NUMBER_ON_LIST = 5
10 |
11 | # how many books have in each set at the home page
12 | BOOKS_PER_SET = 5
13 |
14 | # how many movies have in each set at the home page
15 | MOVIES_PER_SET = 5
16 |
17 | # how many music items have in each set at the home page
18 | MUSIC_PER_SET = 5
19 |
20 | # how many games have in each set at the home page
21 | GAMES_PER_SET = 5
22 |
--------------------------------------------------------------------------------
/common/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.forms import ModelForm
3 |
4 |
5 | class NeoModelForm(ModelForm):
6 | def __init__(self, *args, **kwargs):
7 | super().__init__(*args, **kwargs)
8 | # if "uuid" in self.fields:
9 | # if self.instance and self.instance.pk:
10 | # self.fields["uuid"].initial = self.instance.uuid
11 | for visible in self.visible_fields():
12 | w = visible.field.widget
13 | w.attrs["class"] = "widget " + w.__class__.__name__.lower()
14 |
15 |
16 | class PreviewImageInput(forms.FileInput):
17 | template_name = "widgets/image.html"
18 |
19 | def format_value(self, value):
20 | """
21 | Return the file object if it has a defined url attribute.
22 | """
23 | if self.is_initial(value):
24 | if value.url:
25 | return value.url
26 | else:
27 | return
28 |
29 | def is_initial(self, value):
30 | """
31 | Return whether value is considered to be initial value.
32 | """
33 | return bool(value and getattr(value, "url", False))
34 |
--------------------------------------------------------------------------------
/common/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .cron import BaseJob, JobManager
2 | from .index import Index, QueryParser, SearchResult
3 | from .lang import (
4 | LANGUAGE_CHOICES,
5 | LOCALE_CHOICES,
6 | SCRIPT_CHOICES,
7 | SITE_DEFAULT_LANGUAGE,
8 | SITE_PREFERRED_LANGUAGES,
9 | SITE_PREFERRED_LOCALES,
10 | detect_language,
11 | get_current_locales,
12 | )
13 | from .misc import int_, uniq
14 |
15 | __all__ = [
16 | "BaseJob",
17 | "JobManager",
18 | "LANGUAGE_CHOICES",
19 | "LOCALE_CHOICES",
20 | "SCRIPT_CHOICES",
21 | "SITE_DEFAULT_LANGUAGE",
22 | "SITE_PREFERRED_LANGUAGES",
23 | "SITE_PREFERRED_LOCALES",
24 | "detect_language",
25 | "get_current_locales",
26 | "uniq",
27 | "int_",
28 | "Index",
29 | "QueryParser",
30 | "SearchResult",
31 | ]
32 |
--------------------------------------------------------------------------------
/common/models/misc.py:
--------------------------------------------------------------------------------
1 | import re
2 | from datetime import datetime
3 |
4 |
5 | def uniq(ls: list) -> list:
6 | r = []
7 | for i in ls:
8 | if i not in r:
9 | r.append(i)
10 | return r
11 |
12 |
13 | def int_(x, default=0):
14 | return (
15 | x
16 | if isinstance(x, int)
17 | else (int(x) if (isinstance(x, str) and x.isdigit()) else default)
18 | )
19 |
20 |
21 | def datetime_(dt) -> datetime | None:
22 | if not dt:
23 | return None
24 | try:
25 | if re.match(r"\d{4}-\d{1,2}-\d{1,2}", dt):
26 | d = datetime.strptime(dt, "%Y-%m-%d")
27 | elif re.match(r"\d{4}-\d{1,2}", dt):
28 | d = datetime.strptime(dt, "%Y-%m")
29 | elif re.match(r"\d{4}", dt):
30 | d = datetime.strptime(dt, "%Y")
31 | else:
32 | return None
33 | return d
34 | except ValueError:
35 | return None
36 |
--------------------------------------------------------------------------------
/common/static/img/fediverse.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/common/static/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/common/static/img/icon.png
--------------------------------------------------------------------------------
/common/static/img/logo_square.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/common/static/img/logo_square.jpg
--------------------------------------------------------------------------------
/common/static/img/missing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/common/static/img/missing.png
--------------------------------------------------------------------------------
/common/static/scss/_card.scss:
--------------------------------------------------------------------------------
1 | div.item {
2 | display: grid;
3 | grid-template-columns:
4 | calc(4rem * var(--pico-line-height)) auto;
5 | column-gap: var(--pico-block-spacing-horizontal);
6 | align-items: top;
7 | justify-content: left;
8 | word-break: break-all;
9 |
10 | &.player {
11 | display: block;
12 | }
13 |
14 | hgroup {
15 | margin: 0;
16 | }
17 |
18 | h5 small,
19 | hgroup span {
20 | font-weight: 400;
21 | font-size: 80%;
22 | color: var(--pico-muted-color);
23 |
24 | &.category {
25 | opacity: 0.5;
26 | }
27 |
28 | }
29 |
30 | .brief {
31 | display: -webkit-box;
32 | -webkit-line-clamp: 1;
33 | -webkit-box-orient: vertical;
34 | overflow: hidden;
35 | font-size: 80%;
36 |
37 | >span {
38 | margin-right: 1rem;
39 | }
40 | }
41 |
42 | .full>div {
43 | text-overflow: ellipsis;
44 | word-wrap: break-word;
45 | overflow: hidden;
46 | -webkit-line-clamp: 3;
47 | display: -webkit-box;
48 | -webkit-box-orient: vertical;
49 | font-size: 80%;
50 |
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/common/static/scss/_footer.scss:
--------------------------------------------------------------------------------
1 | body>footer {
2 | font-size: 80%;
3 | text-align: center;
4 | margin-bottom: 4px;
5 | width: 60%;
6 | transform: translateX(-50%);
7 | position: absolute;
8 | clear: both;
9 | left: 50%;
10 |
11 | @media (max-width: 768px) {
12 | width: 80%;
13 | }
14 |
15 | >div.grid {
16 | padding-top: 4px;
17 | padding-left: 15%;
18 | padding-right: 15%;
19 |
20 | @media (max-width: 768px) {
21 | grid-template-columns: 1fr 1fr 1fr;
22 | }
23 |
24 |
25 | >div {
26 | white-space: nowrap;
27 | min-width: 15vw;
28 | }
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/common/static/scss/_l10n.scss:
--------------------------------------------------------------------------------
1 | h1, h2, h3, h4, h5 {
2 | :first-letter {
3 | text-transform: capitalize;
4 | }
5 | }
6 |
7 | label, legend, button {
8 | :first-letter {
9 | text-transform: capitalize;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/common/static/scss/_lightbox.scss:
--------------------------------------------------------------------------------
1 | .lightbox {
2 | display: none;
3 | position: fixed;
4 | z-index: 999;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | bottom: 0;
9 | padding: 1em;
10 | background: rgba(0, 0, 0, 0.8);
11 | -webkit-backdrop-filter: var(--pico-modal-overlay-backdrop-filter);
12 | }
13 |
14 | .lightbox:target {
15 | display: block;
16 | }
17 |
18 | .lightbox span {
19 | display: block;
20 | width: 100%;
21 | height: 100%;
22 |
23 | background-position: center;
24 | background-repeat: no-repeat;
25 | background-size: contain;
26 | }
27 |
--------------------------------------------------------------------------------
/common/static/scss/_markdown.scss:
--------------------------------------------------------------------------------
1 | .spoiler {
2 | background: grey;
3 | filter: blur(2px);
4 | cursor: pointer;
5 |
6 | &.revealed {
7 | background: unset;
8 | filter: unset;
9 | color: inherit;
10 | }
11 | }
12 |
13 | .markdown-content {
14 |
15 | h1 {
16 | font-size: 1.5rem
17 | }
18 |
19 | h2 {
20 | font-size: 1.25rem
21 | }
22 |
23 | h3 {
24 | font-size: 1.1rem;
25 | }
26 | }
27 |
28 |
29 | .markdownx-editor {
30 | font-family: monospace;
31 | }
32 |
--------------------------------------------------------------------------------
/common/static/scss/_tag.scss:
--------------------------------------------------------------------------------
1 | .tag-list {
2 | // margin-bottom: var(--pico-spacing);
3 |
4 | span {
5 | // margin: 3px;
6 |
7 | a {
8 | color: white;
9 | background: var(--pico-muted-color);
10 | padding: calc(var(--pico-spacing)/4);
11 | border-radius: calc(var(--pico-spacing)/4);
12 | line-height: 1.2em;
13 | font-size: 80%;
14 | margin: 0.5em !important;
15 | margin-right: 0 !important;
16 | text-decoration: none;
17 | white-space: nowrap;
18 | display: inline-block;
19 | }
20 | }
21 | }
22 |
23 | .votes {
24 | border: 1px solid var(--pico-secondary-background);
25 | background-color: var(--pico-secondary-background);
26 | color: var(--pico-primary-inverse);
27 | border-radius: calc(var(--pico-spacing)/4);
28 | padding-left: 0.5em;
29 | padding-right: 0.5em;
30 | min-width: 3em;
31 | text-align: center;
32 | display: block;
33 | float: right;
34 | }
35 |
--------------------------------------------------------------------------------
/common/static/scss/neodb.scss:
--------------------------------------------------------------------------------
1 | @import '_header.scss';
2 | @import '_feed.scss';
3 | @import '_footer.scss';
4 | @import '_dialog.scss';
5 | @import '_rating.scss';
6 | @import '_mark.scss';
7 | @import '_item.scss';
8 | @import '_layout.scss';
9 | @import '_sitelabel.scss';
10 | @import '_tag.scss';
11 | @import '_markdown.scss';
12 | @import '_legacy.sass';
13 | @import '_legacy2.scss';
14 | @import '_card.scss';
15 | @import '_gallery.scss';
16 | @import '_sidebar.scss';
17 | @import '_common.scss';
18 | @import '_login.scss';
19 | @import '_form.scss';
20 | @import '_post.scss';
21 | @import '_l10n.scss';
22 | @import '_lightbox.scss';
23 |
--------------------------------------------------------------------------------
/common/templates/400.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 | {% get_current_language as LANGUAGE_CODE %}
7 |
8 |
9 |
10 |
11 |
12 | {{ site_name }} - 400
13 |
14 | {% include "common_libs.html" %}
15 |
16 |
17 | {% include "_header.html" %}
18 |
19 |
20 |
21 | 🤷🏻 {{ message }}
22 |
23 | {% blocktrans %}You may have submitted invalid data, or the content may have been deleted by the author.{% endblocktrans %}
24 |
25 | {% blocktrans %}If you believe this is our mistake, please contact us through the link at the bottom of the page.{% endblocktrans %}
26 |
27 | {% if exception.additonal_detail %}
28 | {% for e in exception.additonal_detail %}{{ e }}
{% endfor %}
29 | {% endif %}
30 |
31 |
32 |
33 | {% include "_footer.html" %}
34 |
35 |
36 |
--------------------------------------------------------------------------------
/common/templates/403.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 |
7 |
8 |
9 |
10 |
11 | {{ site_name }} - 403
12 |
13 | {% include "common_libs.html" %}
14 |
15 |
16 | {% include "_header.html" %}
17 |
18 |
19 |
20 | 🙅🏻 {{ message|default:"" }}
21 |
22 | {% blocktrans %}Author may require you to log in before accessing this content, or you do not have permission to view it.{% endblocktrans %}
23 |
24 | {% blocktrans %}If you believe this is our mistake, please contact us through the link at the bottom of the page.{% endblocktrans %}
25 |
26 |
27 | {% include "_footer.html" %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/common/templates/404.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 |
7 |
8 |
9 |
10 |
11 | {{ site_name }} - 404
12 |
13 | {% include "common_libs.html" %}
14 |
15 |
16 | {% include "_header.html" %}
17 |
18 |
19 |
20 | 🤷🏻 {% trans "Something is missing" %}
21 |
22 | {% blocktrans %}You may have visited an incorrect URL, or the content you are looking for has been deleted by the author.{% endblocktrans %}
23 |
24 | {% blocktrans %}If you believe this is our mistake, please contact us through the link at the bottom of the page.{% endblocktrans %}
25 |
26 | {% trans "Go to Home" %}
27 |
28 |
29 | {% include "_footer.html" %}
30 |
31 |
32 |
--------------------------------------------------------------------------------
/common/templates/500.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 |
7 |
8 |
9 |
10 |
11 | {{ site_name }} - 500
12 |
13 | {% include "common_libs.html" %}
14 |
15 |
16 | {% include "_header.html" %}
17 |
18 |
19 |
22 | {% blocktrans %}An internal error occurred. If this error occurs repeatedly, it will be recorded and handled by a human.{% endblocktrans %}
23 |
24 | {% blocktrans %}If you have an urgent situation or any questions, please contact us through the link at the bottom of the page.{% endblocktrans %}
25 |
26 |
27 | {% include "_footer.html" %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/common/templates/_field.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ field.label }}
4 | {% if field.field.required %}(Required) {% endif %}
5 |
6 | {{ field }}
7 | {% if field.help_text %}
8 |
9 | {{ field.help_text|safe|linebreaksbr }}
10 | {% if field.field.required %}(Required) {% endif %}
11 |
12 | {% endif %}
13 | {{ field.errors }}
14 | {% if field.field.widget.input_type == "file" and field.value %}
15 |
18 | {% endif %}
19 |
20 |
--------------------------------------------------------------------------------
/common/templates/_pagination.html:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/common/templates/common/error.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 | {% get_current_language as LANGUAGE_CODE %}
7 |
8 |
9 |
10 |
11 |
12 | {{ site_name }} - {% trans 'Error' %}
13 | {% include "common_libs.html" %}
14 | {% if url %} {% endif %}
15 |
16 |
17 | {% include "_header.html" %}
18 |
19 |
20 |
23 | {{ secondary_msg|default:"" }}
24 |
25 |
26 | {% include "_footer.html" %}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/common/templates/common/info.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 | {% get_current_language as LANGUAGE_CODE %}
7 |
8 |
9 |
10 |
11 |
12 | {{ site_name }}
13 | {% include "common_libs.html" %}
14 |
15 |
16 | {% include "_header.html" %}
17 |
18 |
19 |
22 | {{ secondary_msg|default:"" }}
23 |
24 |
25 | {% include "_footer.html" %}
26 |
27 |
28 |
--------------------------------------------------------------------------------
/common/templates/common/manifest.json.tpl:
--------------------------------------------------------------------------------
1 | {% load i18n %}{
2 | "id": "{{ site_url }}",
3 | "name": "{{ site_name }}",
4 | "short_name": "{{ site_name }}",
5 | "description": "{{ site_description }}",
6 | "categories": ["social"],
7 | "start_url": "/",
8 | "scope": "/",
9 | "display": "standalone",
10 | "icons": [
11 | {
12 | "src": "{{site_url}}{{ site_icon }}",
13 | "type": "image/png",
14 | "sizes": "128x128",
15 | "purpose": "any maskable"
16 | }
17 | ],
18 | "shortcuts": [
19 | {
20 | "name": "{% trans 'Discover' %}",
21 | "url": "{% url 'catalog:discover' %}"
22 | },
23 | {
24 | "name": "{% trans 'Activities' %}",
25 | "url": "{% url 'social:feed' %}"
26 | },
27 | {
28 | "name": "{% trans 'Profile' %}",
29 | "url": "{% url 'common:me' %}"
30 | },
31 | {
32 | "name": "{% trans 'Notifications' %}",
33 | "url": "{% url 'social:notification' %}"
34 | }
35 | ],
36 | "share_target": {
37 | "url_template": "{% url 'common:share' %}?title={title}\u0026text={text}\u0026url={url}",
38 | "action": "{% url 'common:share' %}",
39 | "method": "GET",
40 | "enctype": "application/x-www-form-urlencoded",
41 | "params": {
42 | "title": "title",
43 | "text": "text",
44 | "url": "url"
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/common/templates/common/opensearch.xml.tpl:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {{ site_name }}
4 | {% trans 'book, movie, tv music, game, podcast and etc' %}
5 | UTF-8
6 | {{site_url}}{{ site_icon }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/common/templates/widgets/hstore.html:
--------------------------------------------------------------------------------
1 |
12 |
15 | {% if widget.value != None %}
16 | {% for pair in widget.value %}
17 | {% for k, v in pair.items %}
18 |
19 |
20 | {% endfor %}
21 | {% endfor %}
22 | {% endif %}
23 |
24 |
28 |
34 |
--------------------------------------------------------------------------------
/common/templates/widgets/image.html:
--------------------------------------------------------------------------------
1 |
4 |
9 |
22 |
--------------------------------------------------------------------------------
/common/templates/widgets/multi_select.html:
--------------------------------------------------------------------------------
1 |
3 | {% for group_name, group_choices, group_index in widget.optgroups %}
4 | {% if group_name %}{% endif %}
5 | {% for option in group_choices %}
6 | {% include option.template_name with widget=option %}
7 | {% endfor %}
8 | {% if group_name %} {% endif %}
9 | {% endfor %}
10 |
11 |
21 |
--------------------------------------------------------------------------------
/common/templates/widgets/tag.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
--------------------------------------------------------------------------------
/common/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/common/templatetags/__init__.py
--------------------------------------------------------------------------------
/common/templatetags/admin_url.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 | from django.utils.html import format_html
4 |
5 | register = template.Library()
6 |
7 |
8 | @register.simple_tag
9 | def admin_url():
10 | url = settings.ADMIN_URL
11 | if not url.startswith("/"):
12 | url = "/" + url
13 | if not url.endswith("/"):
14 | url += "/"
15 | return format_html(url)
16 |
--------------------------------------------------------------------------------
/common/templatetags/highlight.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.defaultfilters import stringfilter
3 | from django.utils.safestring import mark_safe
4 |
5 | register = template.Library()
6 |
7 |
8 | # opencc is removed for now due to package installation issues
9 | # to re-enable it, add it to Dockerfile/requirements.txt and uncomment the following lines
10 | # from opencc import OpenCC
11 | # cc = OpenCC("t2s")
12 | def _cc(text):
13 | return text
14 | # return cc.convert(text)
15 |
16 |
17 | @register.filter
18 | @stringfilter
19 | def highlight(text, search):
20 | otext = _cc(text.lower())
21 | sl = len(text)
22 | if sl != len(otext):
23 | return text # in rare cases, the lowered&converted text has a different length
24 | rtext = ""
25 | words = list(set([w for w in _cc(search.strip().lower()).split(" ") if w]))
26 | words.sort(key=len, reverse=True)
27 | i = 0
28 | while i < sl:
29 | m = None
30 | for w in words:
31 | if otext[i : i + len(w)] == w:
32 | m = f"{text[i : i + len(w)]} "
33 | i += len(w)
34 | break
35 | if not m:
36 | m = text[i]
37 | i += 1
38 | rtext += m
39 | return mark_safe(rtext)
40 |
--------------------------------------------------------------------------------
/common/templatetags/strip_scheme.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.defaultfilters import stringfilter
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.filter(is_safe=True)
8 | @stringfilter
9 | def strip_scheme(value):
10 | """Strip the `https://.../` part of urls"""
11 | if value.startswith("https://"):
12 | value = value.lstrip("https://")
13 | elif value.startswith("http://"):
14 | value = value.lstrip("http://")
15 |
16 | if value.endswith("/"):
17 | value = value[0:-1]
18 | return value
19 |
--------------------------------------------------------------------------------
/common/templatetags/thumb.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from easy_thumbnails.templatetags.thumbnail import thumbnail_url
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.filter
8 | def thumb(source, alias):
9 | """
10 | This filter modifies that from `easy_thumbnails` so that
11 | it can neglect .svg file.
12 | """
13 | try:
14 | if source.url.endswith(".svg"):
15 | return source.url
16 | else:
17 | return thumbnail_url(source, alias)
18 | except Exception:
19 | return ""
20 |
--------------------------------------------------------------------------------
/common/templatetags/truncate.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.defaultfilters import stringfilter
3 | from django.utils.text import Truncator
4 |
5 | register = template.Library()
6 |
7 |
8 | @register.filter(is_safe=True)
9 | @stringfilter
10 | def truncate(value, arg):
11 | """Truncate a string after `arg` number of characters."""
12 | try:
13 | length = int(arg)
14 | except ValueError: # Invalid literal for int().
15 | return value # Fail silently.
16 | return Truncator(value).chars(length, truncate="...")
17 |
--------------------------------------------------------------------------------
/common/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, re_path
2 |
3 | from .views import *
4 |
5 | app_name = "common"
6 | urlpatterns = [
7 | path("", home),
8 | path("search", search, name="search"),
9 | path("home/", home, name="home"),
10 | path("site/share", share, name="share"),
11 | path("site/manifest.json", manifest, name="manifest"),
12 | path("site/opensearch.xml", opensearch, name="opensearch"),
13 | path("me/", me, name="me"),
14 | path("nodeinfo//", nodeinfo2),
15 | path("developer/", console, name="developer"),
16 | path("auth/signup/", signup, name="signup"),
17 | path("auth/signup//", signup, name="signup"),
18 | re_path("^~neodb~(?P.+)", ap_redirect),
19 | ]
20 |
--------------------------------------------------------------------------------
/journal/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/journal/__init__.py
--------------------------------------------------------------------------------
/journal/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/journal/apis/__init__.py:
--------------------------------------------------------------------------------
1 | from .collection import * # noqa
2 | from .note import * # noqa
3 | from .review import * # noqa
4 | from .shelf import * # noqa
5 | from .tag import * # noqa
6 | from .post import * # noqa
7 |
--------------------------------------------------------------------------------
/journal/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class JournalConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "journal"
7 |
8 | def ready(self):
9 | from . import apis # noqa
10 |
--------------------------------------------------------------------------------
/journal/exporters/__init__.py:
--------------------------------------------------------------------------------
1 | from .csv import CsvExporter
2 | from .doufen import DoufenExporter
3 | from .ndjson import NdjsonExporter
4 |
5 | __all__ = ["DoufenExporter", "CsvExporter", "NdjsonExporter"]
6 |
--------------------------------------------------------------------------------
/journal/importers/__init__.py:
--------------------------------------------------------------------------------
1 | from .csv import CsvImporter
2 | from .douban import DoubanImporter
3 | from .goodreads import GoodreadsImporter
4 | from .letterboxd import LetterboxdImporter
5 | from .ndjson import NdjsonImporter
6 | from .opml import OPMLImporter
7 |
8 | __all__ = [
9 | "CsvImporter",
10 | "NdjsonImporter",
11 | "LetterboxdImporter",
12 | "OPMLImporter",
13 | "DoubanImporter",
14 | "GoodreadsImporter",
15 | ]
16 |
--------------------------------------------------------------------------------
/journal/migrations/0003_auto_20230113_0506.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-01-12 21:06
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 |
6 | import catalog.common.utils
7 |
8 |
9 | class Migration(migrations.Migration):
10 | dependencies = [
11 | ("catalog", "0002_initial"),
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ("journal", "0002_initial"),
14 | ]
15 |
16 | operations = [
17 | migrations.AlterField(
18 | model_name="collection",
19 | name="cover",
20 | field=models.ImageField(
21 | blank=True,
22 | default="item/default.svg",
23 | upload_to=catalog.common.utils.piece_cover_path,
24 | ),
25 | ),
26 | migrations.AlterUniqueTogether(
27 | name="rating",
28 | unique_together={("owner", "item")},
29 | ),
30 | migrations.AlterUniqueTogether(
31 | name="shelfmember",
32 | unique_together={("parent", "item")},
33 | ),
34 | migrations.AlterUniqueTogether(
35 | name="tagmember",
36 | unique_together={("parent", "item")},
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/journal/migrations/0004_alter_shelflogentry_timestamp.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-01-12 22:10
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0003_auto_20230113_0506"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="shelflogentry",
14 | name="timestamp",
15 | field=models.DateTimeField(),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/journal/migrations/0004_tasks.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.17 on 2024-12-26 06:33
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0006_alter_task_type"),
9 | ("journal", "0003_note_progress"),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name="DoufenExporter",
15 | fields=[],
16 | options={
17 | "proxy": True,
18 | "indexes": [],
19 | "constraints": [],
20 | },
21 | bases=("users.task",),
22 | ),
23 | migrations.CreateModel(
24 | name="DoubanImporter",
25 | fields=[],
26 | options={
27 | "proxy": True,
28 | "indexes": [],
29 | "constraints": [],
30 | },
31 | bases=("users.task",),
32 | ),
33 | migrations.CreateModel(
34 | name="GoodreadsImporter",
35 | fields=[],
36 | options={
37 | "proxy": True,
38 | "indexes": [],
39 | "constraints": [],
40 | },
41 | bases=("users.task",),
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/journal/migrations/0005_csvexporter.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.17 on 2025-01-27 07:42
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0007_alter_task_type"),
9 | ("journal", "0004_tasks"),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name="CsvExporter",
15 | fields=[],
16 | options={
17 | "proxy": True,
18 | "indexes": [],
19 | "constraints": [],
20 | },
21 | bases=("users.task",),
22 | ),
23 | migrations.CreateModel(
24 | name="NdjsonExporter",
25 | fields=[],
26 | options={
27 | "proxy": True,
28 | "indexes": [],
29 | "constraints": [],
30 | },
31 | bases=("users.task",),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/journal/migrations/0006_auto_20230114_2139.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-01-14 13:39
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ("journal", "0005_auto_20230114_1134"),
12 | ]
13 |
14 | operations = [
15 | migrations.RenameField(
16 | model_name="featuredcollection",
17 | old_name="collection",
18 | new_name="target",
19 | ),
20 | migrations.RemoveField(
21 | model_name="featuredcollection",
22 | name="id",
23 | ),
24 | migrations.AddField(
25 | model_name="featuredcollection",
26 | name="piece_ptr",
27 | field=models.OneToOneField(
28 | auto_created=True,
29 | default=0,
30 | on_delete=django.db.models.deletion.CASCADE,
31 | parent_link=True,
32 | primary_key=True,
33 | serialize=False,
34 | to="journal.piece",
35 | ),
36 | preserve_default=False,
37 | ),
38 | migrations.AlterUniqueTogether(
39 | name="featuredcollection",
40 | unique_together={("owner", "target")},
41 | ),
42 | ]
43 |
--------------------------------------------------------------------------------
/journal/migrations/0007_alter_collection_catalog_item.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-01-17 03:51
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("catalog", "0002_initial"),
10 | ("journal", "0006_auto_20230114_2139"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="collection",
16 | name="catalog_item",
17 | field=models.OneToOneField(
18 | on_delete=django.db.models.deletion.PROTECT,
19 | related_name="journal_item",
20 | to="catalog.collection",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/journal/migrations/0007_smart_collection.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.20 on 2025-04-26 07:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0006_csvimporter"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="collection",
14 | name="query",
15 | field=models.CharField(
16 | blank=True,
17 | default=None,
18 | max_length=1000,
19 | null=True,
20 | verbose_name="search query for dynamic collection",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/journal/migrations/0008_alter_shelfmember_unique_together.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-01-23 05:05
2 |
3 | from django.conf import settings
4 | from django.db import migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("catalog", "0002_initial"),
10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11 | ("journal", "0007_alter_collection_catalog_item"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterUniqueTogether(
16 | name="shelfmember",
17 | unique_together={("owner", "item")},
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/journal/migrations/0009_comment_focus_item.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-01-31 20:14
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("catalog", "0002_initial"),
10 | ("journal", "0008_alter_shelfmember_unique_together"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="comment",
16 | name="focus_item",
17 | field=models.ForeignKey(
18 | null=True,
19 | on_delete=django.db.models.deletion.PROTECT,
20 | related_name="focused_comments",
21 | to="catalog.item",
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/journal/migrations/0010_shelfmember_journal_she_parent__9da946_idx.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.18 on 2023-04-21 22:23
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0009_comment_focus_item"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddIndex(
13 | model_name="shelfmember",
14 | index=models.Index(
15 | fields=["parent_id", "visibility", "created_time"],
16 | name="journal_she_parent__9da946_idx",
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/journal/migrations/0012_alter_piece_polymorphic_ctype_alter_shelf_items.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-06 22:53
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("catalog", "0010_alter_item_polymorphic_ctype"),
10 | ("contenttypes", "0002_remove_content_type_name"),
11 | ("journal", "0011_performance"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="piece",
17 | name="polymorphic_ctype",
18 | field=models.ForeignKey(
19 | editable=False,
20 | null=True,
21 | on_delete=django.db.models.deletion.CASCADE,
22 | related_name="polymorphic_%(app_label)s.%(class)s_set+",
23 | to="contenttypes.contenttype",
24 | ),
25 | ),
26 | migrations.AlterField(
27 | model_name="shelf",
28 | name="items",
29 | field=models.ManyToManyField(
30 | related_name="+", through="journal.ShelfMember", to="catalog.item"
31 | ),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/journal/migrations/0013_remove_comment_focus_item.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-12 21:55
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0012_alter_piece_polymorphic_ctype_alter_shelf_items"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="comment",
14 | name="focus_item",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/journal/migrations/0014_remove_reply_piece_ptr_remove_reply_reply_to_content_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.4 on 2023-08-10 18:55
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | (
9 | "journal",
10 | "0013_remove_comment_focus_item",
11 | ),
12 | ]
13 |
14 | operations = [
15 | migrations.RemoveField(
16 | model_name="reply",
17 | name="piece_ptr",
18 | ),
19 | migrations.RemoveField(
20 | model_name="reply",
21 | name="reply_to_content",
22 | ),
23 | migrations.DeleteModel(
24 | name="Memo",
25 | ),
26 | migrations.DeleteModel(
27 | name="Reply",
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/journal/migrations/0017_alter_piece_options_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.4 on 2023-08-26 00:59
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0016_piecepost_piece_posts_piecepost_unique_piece_post"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterModelOptions(
13 | name="piece",
14 | options={"base_manager_name": "objects"},
15 | ),
16 | migrations.RemoveIndex(
17 | model_name="piece",
18 | name="journal_pie_post_id_6a74ff_idx",
19 | ),
20 | migrations.RemoveField(
21 | model_name="piece",
22 | name="post_id",
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/journal/migrations/0020_shelflogentry_unique_shelf_log_entry.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.7 on 2023-11-23 03:23
2 |
3 | from django.db import migrations, models
4 |
5 | _sql = """DELETE
6 | FROM journal_shelflogentry a USING journal_shelflogentry b
7 | WHERE a.ctid < b.ctid
8 | AND a.item_id=b.item_id
9 | AND a.owner_id=b.owner_id
10 | AND a.timestamp=b.timestamp
11 | AND a.shelf_type=b.shelf_type"""
12 |
13 |
14 | class Migration(migrations.Migration):
15 | dependencies = [
16 | ("journal", "0019_alter_collection_edited_time_and_more"),
17 | ]
18 |
19 | operations = [
20 | migrations.RunSQL("SET CONSTRAINTS ALL IMMEDIATE;"),
21 | migrations.RunSQL(
22 | sql=_sql,
23 | reverse_sql=migrations.RunSQL.noop,
24 | ),
25 | migrations.RunSQL("SET CONSTRAINTS ALL DEFERRED;"),
26 | migrations.AddConstraint(
27 | model_name="shelflogentry",
28 | constraint=models.UniqueConstraint(
29 | fields=("owner", "item", "timestamp", "shelf_type"),
30 | name="unique_shelf_log_entry",
31 | ),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/journal/migrations/0022_letterboxdimporter.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.9 on 2024-01-11 01:47
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0019_task"),
9 | ("journal", "0021_pieceinteraction_pieceinteraction_unique_interaction"),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name="LetterboxdImporter",
15 | fields=[],
16 | options={
17 | "proxy": True,
18 | "indexes": [],
19 | "constraints": [],
20 | },
21 | bases=("users.task",),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/journal/migrations/0025_pin_tags.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-06-04 19:29
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0024_i18n"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="tag",
14 | name="pinned",
15 | field=models.BooleanField(default=False, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/journal/migrations/0026_pinned_tag_index.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-06-05 00:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("journal", "0025_pin_tags"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddIndex(
13 | model_name="tag",
14 | index=models.Index(
15 | fields=["owner", "pinned"], name="journal_tag_owner_i_068598_idx"
16 | ),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/journal/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/journal/migrations/__init__.py
--------------------------------------------------------------------------------
/journal/static/css/calendar_yearview_blocks.css:
--------------------------------------------------------------------------------
1 | .svg-tip {
2 | padding: 10px;
3 | background: #191919;
4 | opacity: 0.8;
5 | color: #eee;
6 | font-size: 12px;
7 | position: absolute;
8 | z-index: 99999;
9 | text-align: center;
10 | border-radius: 3px;
11 | }
12 |
13 | .svg-tip:after {
14 | -moz-box-sizing: border-box;
15 | box-sizing: border-box;
16 | position: absolute;
17 | left: 50%;
18 | height: 5px;
19 | width: 5px;
20 | bottom: -10px;
21 | margin: 0 0 0 -5px;
22 | content: " ";
23 | border: 5px solid transparent;
24 | border-top-color: rgba(0,0,0,0.8);
25 | }
26 |
27 | .wday, .month {
28 | font-variant: small-caps;
29 | font-size: 12px;
30 | }
31 |
--------------------------------------------------------------------------------
/journal/templates/_feature_stats.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
8 | {% if featured %}
9 |
12 |
13 |
14 | {% else %}
15 |
17 |
18 |
19 | {% endif %}
20 |
--------------------------------------------------------------------------------
/journal/templates/action_boost_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load user_actions %}
3 | {% boosted_post post as boosted %}
4 |
7 |
11 |
12 |
13 | {% if post.stats.boosts > 1 %}{{ post.stats.boosts }} {% endif %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/journal/templates/action_delete_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
9 | {% if post.piece %}
10 |
11 | {% else %}
12 |
13 | {% endif %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/journal/templates/action_flag_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/journal/templates/action_like_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load user_actions %}
3 | {% liked_post post as liked %}
4 |
7 | {% if liked %}
8 |
11 |
12 | {{ post.stats.likes }}
13 |
14 | {% else %}
15 |
17 |
18 | {% if post.stats.likes %}{{ post.stats.likes }} {% endif %}
19 |
20 | {% endif %}
21 |
22 |
--------------------------------------------------------------------------------
/journal/templates/action_open_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
6 | {% if post.visibility == 1 %}
7 |
8 | {% elif post.visibility == 2 %}
9 |
10 | {% elif post.visibility == 3 %}
11 |
12 | {% elif post.visibility == 4 %}
13 |
14 | {% else %}
15 |
16 | {% endif %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/journal/templates/action_pin_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load user_actions %}
3 | {% if post and request.user.identity.pk == post.author_id %}
4 | {% pinned_post post as pinned %}
5 |
6 |
9 |
10 |
11 |
12 | {% endif %}
13 |
--------------------------------------------------------------------------------
/journal/templates/action_post_timestamp.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 |
4 |
6 | {{ post.edited|default:post.published|naturaldelta }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/journal/templates/action_reply_piece.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
9 |
10 | {% if post.stats.replies %}{{ post.stats.replies }} {% endif %}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/journal/templates/action_reply_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
8 |
9 | {% if post.stats.replies %}{{ post.stats.replies }} {% endif %}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/journal/templates/action_translate_post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/journal/templates/calendar_data.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {{ calendar_data|json_script:"calendar_data" }}
3 |
23 |
--------------------------------------------------------------------------------
/journal/templates/collection_items.html:
--------------------------------------------------------------------------------
1 | {% load thumb %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% for member in members %}
5 | {% include '_list_item.html' with item=member.item mark=item.mark collection_member=member %}
6 | {% if forloop.last and not pagination %}
7 |
11 |
12 |
13 | {% endif %}
14 | {% endfor %}
15 | {% if pagination %}
16 | {% include "_pagination.html" %}
17 | {% endif %}
18 | {% if collection_edit %}
19 |
26 | {% endif %}
27 | {% if msg %}{% endif %}
28 |
--------------------------------------------------------------------------------
/journal/templates/collection_update_item_note.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
10 |
--------------------------------------------------------------------------------
/journal/templates/collection_update_item_note_ok.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
5 |
6 | {{ collection_member.note|default:" " }}
7 |
--------------------------------------------------------------------------------
/journal/templates/markdown.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 | {% trans "Markdown format references" %}
4 |
5 | {% blocktrans %}
6 | Title
7 | =====
8 |
9 | Subtitle
10 | --------
11 |
12 | Paragraphs need to be separated by a blank line
13 |
14 | Indentation at the beginning of the paragraph requires using a Unicode full-width space
15 |
16 | [Link](https://zh.wikipedia.org/wiki/Markdown)
17 | **Bold** *Italic* ==Highlight== ~~Strikethrough~~
18 | ^Super^script ~Sub~script [拼(pīn)音(yīn)]
19 |
20 | Drag and drop an image 
21 |
22 | > Quote
23 | >> Multi-level quote
24 |
25 | Inline >! spoiler warning !< (also in short comments)
26 |
27 | >! Multi-line
28 | >! Spoiler
29 |
30 | ---
31 |
32 | - Bullet
33 | - Points
34 |
35 | content in paragraph with footnote[^1] markup.
36 | [^1]: footnote explain
37 |
38 | ```
39 | code
40 | ```
41 |
42 | Table Header | Second Header
43 | ------------- | -------------
44 | Content Cell | Content Cell
45 | Content Cell | Content Cell
46 | {% endblocktrans %}
47 |
48 |
49 |
--------------------------------------------------------------------------------
/journal/templates/posts.html:
--------------------------------------------------------------------------------
1 | {% load bleach_tags %}
2 | {% load duration %}
3 | {% load humanize %}
4 | {% load i18n %}
5 | {% for post in posts %}
6 | {% include "_event_post.html" %}
7 | {% empty %}
8 | {% trans "nothing so far." %}
9 | {% endfor %}
10 |
--------------------------------------------------------------------------------
/journal/templates/profile_items.html:
--------------------------------------------------------------------------------
1 | {% load thumb %}
2 | {% load i18n %}
3 | {% load l10n %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ title }}
14 |
15 | {{ total }}
16 | {% if show_create_button %}
17 |
18 |
19 |
20 | {% endif %}
21 |
22 |
23 |
37 |
--------------------------------------------------------------------------------
/journal/templates/search_journal.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load humanize %}
5 | {% load mastodon %}
6 | {% load duration %}
7 | {% load thumb %}
8 | {% get_current_language as LANGUAGE_CODE %}
9 |
10 |
11 |
12 |
13 |
14 | {{ site_name }} - {{ request.GET.q }} - {% trans 'Search Results' %}
15 | {% include "common_libs.html" %}
16 |
17 |
18 | {% include '_header.html' %}
19 |
20 |
21 | {% include 'search_header.html' %}
22 |
23 | {% for item in items %}
24 | {% include '_list_item.html' %}
25 | {% empty %}
26 |
{% trans "No items matching your search query." %}
27 | {% endfor %}
28 |
29 | {% include "_pagination.html" %}
30 |
31 | {% block sidebar %}
32 | {% include "_sidebar.html" with show_profile=1 identity=user.identity sidebar_template="_sidebar_search_journal.html" bottom=1 %}
33 | {% endblock %}
34 |
35 | {% include '_footer.html' %}
36 |
37 |
38 |
--------------------------------------------------------------------------------
/journal/templates/user_item_list_base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 | {% get_current_language as LANGUAGE_CODE %}
7 |
8 |
9 |
10 |
11 |
12 | {% block title %}{{ site_name }} - {{ identity.display_name }} {% endblock %}
13 | {% include "common_libs.html" %}
14 |
15 |
16 | {% include "_header.html" %}
17 |
18 |
19 |
20 | {% block head %}{{ identity.display_name }}{% endblock %}
21 |
22 |
23 | {% for member in members %}
24 | {% include "_list_item.html" with item=member.item mark=member.mark hide_category=True %}
25 | {% empty %}
26 |
{% trans 'nothing so far.' %}
27 | {% endfor %}
28 |
29 | {% include "_pagination.html" %}
30 |
31 | {% block sidebar %}
32 | {% include "_sidebar.html" with show_profile=1 %}
33 | {% endblock %}
34 |
35 | {% include "_footer.html" %}
36 |
37 |
38 |
--------------------------------------------------------------------------------
/journal/templates/user_mark_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'user_item_list_base.html' %}
2 | {% load i18n %}
3 | {% block title %}
4 | {{ site_name }} - {{ identity.display_name }} - {% trans 'Marks' %}
5 | {% endblock %}
6 | {% block head %}
7 | {{ identity.display_name }} - {% trans 'Marks' %}
8 | {% endblock %}
9 | {% block sidebar %}
10 | {% include "_sidebar.html" with show_profile=1 sidebar_template="_sidebar_user_mark_list.html" %}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/journal/templates/user_review_list.html:
--------------------------------------------------------------------------------
1 | {% extends "user_item_list_base.html" %}
2 | {% load i18n %}
3 | {% block title %}
4 | {{ site_name }} - {{ identity.display_name }} - {% trans 'Reviews' %}
5 | {% endblock %}
6 | {% block head %}
7 | {{ identity.display_name }} - {% trans 'Review' %}
8 | {% endblock %}
9 | {% block sidebar %}
10 | {% include "_sidebar.html" with show_profile=1 sidebar_template="_sidebar_user_mark_list.html" %}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/journal/templates/user_tagmember_list.html:
--------------------------------------------------------------------------------
1 | {% extends "user_item_list_base.html" %}
2 | {% load i18n %}
3 | {% block title %}
4 | {{ site_name }} - {{ identity.display_name }} - {% trans 'Tags' %} - {{ tag.title }}
5 | {% endblock %}
6 | {% block head %}
7 |
8 | {% if tag.pinned %}
9 |
10 |
11 |
12 | {% endif %}
13 | {% if tag.visibility > 0 %}
14 |
15 |
16 |
17 | {% endif %}
18 | {% if identity.user == request.user %}
19 |
20 |
23 |
24 |
25 |
26 | {% endif %}
27 |
28 |
29 | {{ tag.title }}
30 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/journal/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/journal/templatetags/__init__.py
--------------------------------------------------------------------------------
/journal/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from .csv import *
2 | from .ndjson import *
3 | from .piece import *
4 | from .rating import *
5 | from .search import *
6 | from .shelf import *
7 |
--------------------------------------------------------------------------------
/journal/tests/search.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from catalog.models import Edition
4 | from users.models import User
5 |
6 | from ..models import *
7 |
8 |
9 | class SearchTest(TestCase):
10 | databases = "__all__"
11 |
12 | def setUp(self):
13 | self.book1 = Edition.objects.create(title="Hyperion")
14 | self.book2 = Edition.objects.create(title="Andymion")
15 | self.user1 = User.register(email="x@y.com", username="userx")
16 | self.index = JournalIndex.instance()
17 | self.index.delete_by_owner([self.user1.identity.pk])
18 |
19 | def test_post(self):
20 | mark = Mark(self.user1.identity, self.book1)
21 | mark.update(ShelfType.WISHLIST, "a gentle comment", 9, ["Sci-Fi", "fic"], 0)
22 | mark = Mark(self.user1.identity, self.book2)
23 | mark.update(ShelfType.WISHLIST, "a gentle comment", None, ["nonfic"], 1)
24 | q = JournalQueryParser("gentle")
25 | q.filter_by_owner(self.user1.identity)
26 | r = self.index.search(q)
27 | self.assertEqual(r.total, 2)
28 |
--------------------------------------------------------------------------------
/journal/views/search.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import login_required
2 | from django.shortcuts import render
3 |
4 | from common.models.misc import int_
5 | from common.utils import PageLinksGenerator
6 | from journal.models import JournalIndex, JournalQueryParser
7 |
8 |
9 | @login_required
10 | def search(request):
11 | page = int_(request.GET.get("page"), 1)
12 | q = JournalQueryParser(request.GET.get("q", default=""), page)
13 | q.filter_by_owner(request.user.identity)
14 | q.filter("item_id", ">0")
15 | if q:
16 | index = JournalIndex.instance()
17 | r = index.search(q)
18 | return render(
19 | request,
20 | "search_journal.html",
21 | {
22 | "items": r.items,
23 | "pagination": PageLinksGenerator(r.page, r.pages, request.GET),
24 | },
25 | )
26 | else:
27 | return render(request, "search_journal.html", {"items": []})
28 |
--------------------------------------------------------------------------------
/legacy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/legacy/__init__.py
--------------------------------------------------------------------------------
/legacy/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/legacy/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class LegacyConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "legacy"
7 |
--------------------------------------------------------------------------------
/legacy/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/legacy/migrations/__init__.py
--------------------------------------------------------------------------------
/legacy/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class BookLink(models.Model):
5 | old_id = models.IntegerField(unique=True)
6 | new_uid = models.UUIDField()
7 |
8 |
9 | class MovieLink(models.Model):
10 | old_id = models.IntegerField(unique=True)
11 | new_uid = models.UUIDField()
12 |
13 |
14 | class AlbumLink(models.Model):
15 | old_id = models.IntegerField(unique=True)
16 | new_uid = models.UUIDField()
17 |
18 |
19 | class SongLink(models.Model):
20 | old_id = models.IntegerField(unique=True)
21 | new_uid = models.UUIDField()
22 |
23 |
24 | class GameLink(models.Model):
25 | old_id = models.IntegerField(unique=True)
26 | new_uid = models.UUIDField()
27 |
28 |
29 | class CollectionLink(models.Model):
30 | old_id = models.IntegerField(unique=True)
31 | new_uid = models.UUIDField()
32 |
33 |
34 | class ReviewLink(models.Model):
35 | module = models.CharField(max_length=20)
36 | old_id = models.IntegerField()
37 | new_uid = models.UUIDField()
38 |
39 | class Meta:
40 | unique_together = [["module", "old_id"]]
41 |
--------------------------------------------------------------------------------
/legacy/tests.py:
--------------------------------------------------------------------------------
1 | # Create your tests here.
2 |
--------------------------------------------------------------------------------
/legacy/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .views import *
4 |
5 | app_name = "legacy"
6 | urlpatterns = [
7 | path("books//", book, name="book"),
8 | path("movies//", movie, name="movie"),
9 | path("music/album//", album, name="album"),
10 | path("music/song//", song, name="song"),
11 | path("games//", game, name="game"),
12 | path("collections//", collection, name="collection"),
13 | path("books/review//", book_review, name="book_review"),
14 | path("movies/review//", movie_review, name="movie_review"),
15 | path("music/album/review//", album_review, name="album_review"),
16 | path("music/song/review//", song_review, name="song_review"),
17 | path("games/review//", game_review, name="game_review"),
18 | ]
19 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 |
4 | import os
5 | import sys
6 |
7 |
8 | def main():
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "boofilsic.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/mastodon/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/mastodon/__init__.py
--------------------------------------------------------------------------------
/mastodon/admin.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/mastodon/admin.py
--------------------------------------------------------------------------------
/mastodon/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class MastodonConfig(AppConfig):
5 | name = "mastodon"
6 |
7 | def ready(self):
8 | # register cron jobs
9 | from .jobs import MastodonSiteCheck # noqa
10 |
--------------------------------------------------------------------------------
/mastodon/auth.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.backends import ModelBackend
2 | from django.http import HttpRequest
3 |
4 | from mastodon.models.common import SocialAccount
5 |
6 |
7 | class OAuth2Backend(ModelBackend):
8 | """Used to glue OAuth2 and Django User model"""
9 |
10 | # "authenticate() should check the credentials it gets and returns
11 | # a user object that matches those credentials."
12 | # arg request is an interface specification, not used in this implementation
13 |
14 | def authenticate(
15 | self, request: HttpRequest | None, username=None, password=None, **kwargs
16 | ):
17 | """when username is provided, assume that token is newly obtained and valid"""
18 | account: SocialAccount | None = kwargs.get("social_account", None)
19 | if not account or not account.user:
20 | return None
21 | return account.user if self.user_can_authenticate(account.user) else None
22 |
--------------------------------------------------------------------------------
/mastodon/migrations/0002_add_api_domain_server_version.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-02-14 06:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("mastodon", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="mastodonapplication",
14 | name="api_domain",
15 | field=models.CharField(
16 | blank=True, max_length=100, verbose_name="domain for api call"
17 | ),
18 | ),
19 | migrations.AddField(
20 | model_name="mastodonapplication",
21 | name="server_version",
22 | field=models.CharField(
23 | blank=True, max_length=100, verbose_name="type and verion"
24 | ),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/mastodon/migrations/0003_mastodonapplication_reachable.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.7 on 2023-11-10 20:37
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("mastodon", "0002_add_api_domain_server_version"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="mastodonapplication",
14 | name="disabled",
15 | field=models.BooleanField(default=False),
16 | ),
17 | migrations.AddField(
18 | model_name="mastodonapplication",
19 | name="last_reachable_date",
20 | field=models.DateTimeField(default=None, null=True),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/mastodon/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/mastodon/migrations/__init__.py
--------------------------------------------------------------------------------
/mastodon/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .bluesky import Bluesky, BlueskyAccount
2 | from .common import Platform, SocialAccount
3 | from .email import Email, EmailAccount
4 | from .mastodon import (
5 | Mastodon,
6 | MastodonAccount,
7 | MastodonApplication,
8 | detect_server_info,
9 | verify_client,
10 | )
11 | from .threads import Threads, ThreadsAccount
12 |
13 | __all__ = [
14 | "Bluesky",
15 | "BlueskyAccount",
16 | "Email",
17 | "EmailAccount",
18 | "Mastodon",
19 | "MastodonAccount",
20 | "MastodonApplication",
21 | "Platform",
22 | "SocialAccount",
23 | "Threads",
24 | "ThreadsAccount",
25 | "detect_server_info",
26 | "verify_client",
27 | ]
28 |
--------------------------------------------------------------------------------
/mastodon/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .views import *
4 |
5 | app_name = "mastodon"
6 | urlpatterns = [
7 | # Mastodon
8 | path("login/oauth", mastodon_oauth, name="oauth"),
9 | path("mastodon/login", mastodon_login, name="login"),
10 | path("mastodon/reconnect", mastodon_reconnect, name="reconnect"),
11 | path("mastodon/disconnect", mastodon_disconnect, name="disconnect"),
12 | # Email
13 | path("email/login", email_login, name="email_login"),
14 | path("email/state", email_login_state, name="email_login_state"),
15 | path("email/verify", email_verify, name="email_verify"),
16 | # Threads
17 | path("threads/login", threads_login, name="threads_login"),
18 | path("threads/oauth", threads_oauth, name="threads_oauth"),
19 | path("threads/reconnect", threads_reconnect, name="threads_reconnect"),
20 | path("threads/disconnect", threads_disconnect, name="threads_disconnect"),
21 | path("threads/uninstall", threads_uninstall, name="threads_uninstall"),
22 | path("threads/delete", threads_delete, name="threads_delete"),
23 | # Bluesky
24 | path("bluesky/login", bluesky_login, name="bluesky_login"),
25 | path("bluesky/reconnect", bluesky_reconnect, name="bluesky_reconnect"),
26 | path("bluesky/disconnect", bluesky_disconnect, name="bluesky_disconnect"),
27 | ]
28 |
--------------------------------------------------------------------------------
/mastodon/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .bluesky import *
2 | from .email import *
3 | from .mastodon import *
4 | from .threads import *
5 |
--------------------------------------------------------------------------------
/misc/bin/neodb-hello:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo '\033[0;35m====== NeoDB ======\033[0m'
3 | echo Version: `neodb-version`
4 | echo Your configuration is for ${NEODB_SITE_NAME} on ${NEODB_SITE_DOMAIN}
5 | [[ -z "${NEODB_DEBUG}" ]] || echo DEBUG is ON, showing environment variables:
6 | [[ -z "${NEODB_DEBUG}" ]] || env
7 | [[ -z "${NEODB_DEBUG}" ]] || echo Running some basic checks...
8 | [[ -z "${NEODB_DEBUG}" ]] || neodb-manage check --database default --database takahe --deploy
9 | [[ -z "${NEODB_DEBUG}" ]] || TAKAHE_DATABASE_SERVER="postgres://x@y/z" TAKAHE_SECRET_KEY="t" TAKAHE_MAIN_DOMAIN="x.y" takahe-manage check
10 | [[ -z "${NEODB_DEBUG}" ]] || echo check complete.
11 | cat <
15 | start NeoDB instance: docker compose --profile up -d
16 | stop NeoDB instance: docker compose --profile down -d
17 | update NeoDB instance: docker compose --profile pull
18 |
19 | Please follow instructions on https://neodb.net to configure and run your instance.
20 |
21 | EOF
22 |
--------------------------------------------------------------------------------
/misc/bin/neodb-init:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo '\033[0;35m====== NeoDB ======\033[0m'
3 | echo Version: `neodb-version`
4 | echo Your configuration is for ${NEODB_SITE_NAME} on ${NEODB_SITE_DOMAIN}
5 |
6 | echo NeoDB initializing...
7 |
8 | takahe-manage migrate || exit $?
9 | neodb-manage migrate || exit $?
10 |
11 | echo NeoDB initialization complete.
12 |
--------------------------------------------------------------------------------
/misc/bin/neodb-manage:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd /neodb && exec ${NEODB_VENV}/bin/python manage.py $@
3 |
--------------------------------------------------------------------------------
/misc/bin/neodb-version:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "from django.conf import settings; print(settings.NEODB_VERSION+(' debug:on' if settings.DEBUG else ''))" | neodb-manage shell
3 |
--------------------------------------------------------------------------------
/misc/bin/nginx-start:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | chown app:app /www/media /www/m
3 | envsubst '${NEODB_WEB_SERVER} ${NEODB_API_SERVER} ${TAKAHE_WEB_SERVER}' < $NGINX_CONF > /etc/nginx/conf.d/neodb.conf
4 | exec nginx -g 'daemon off;'
5 |
--------------------------------------------------------------------------------
/misc/bin/takahe-manage:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd /takahe && exec ${TAKAHE_VENV}/bin/python manage.py $@
3 |
--------------------------------------------------------------------------------
/misc/wheels-cache/libsass-0.23.0-cp311-abi3-linux_aarch64.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/misc/wheels-cache/libsass-0.23.0-cp311-abi3-linux_aarch64.whl
--------------------------------------------------------------------------------
/misc/www/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: CCBot
2 | Disallow: /review/
3 |
4 | User-agent: GPTBot
5 | Disallow: /review/
6 |
7 | User-agent: Google-Extended
8 | Disallow: /review/
9 |
10 | User-agent: FacebookBot
11 | Disallow: /review/
12 |
13 | User-agent: Omgilibot
14 | Disallow: /review/
15 |
--------------------------------------------------------------------------------
/neodb.env.example:
--------------------------------------------------------------------------------
1 | # NEODB Configuration
2 |
3 | # copy along with compose.yml, rename this file to .env
4 |
5 | # MUST change these before start the instance for the first time!!
6 | NEODB_SECRET_KEY=change_me
7 | NEODB_SITE_NAME=Example Site
8 | NEODB_SITE_DOMAIN=example.site
9 |
10 | # Change these to customize your site
11 | NEODB_SITE_INTRO=/welcome.html
12 | NEODB_SITE_LOGO=/logo.png
13 | NEODB_SITE_ICON=/icon.png
14 | NEODB_SITE_LINKS=@NiceDB=https://donotban.com/@testie,@NeoDB=https://mastodon.social/@neodb
15 |
16 | # preferred languages
17 | # NEODB_PREFERRED_LANGUAGES=en,es,fr,de,pt,zh,ja,ko
18 |
19 | # To enable push notification, generate a keypair from https://web-push-codelab.glitch.me
20 | # TAKAHE_VAPID_PUBLIC_KEY=
21 | # TAKAHE_VAPID_PRIVATE_KEY=
22 |
23 | # HTTP port your reverse proxy should send request to
24 | # NEODB_PORT=8000
25 |
26 | # Path to store db/media/cache/etc, must be writable
27 | # NEODB_DATA=/var/lib/neodb
28 |
29 | # Scaling parameters
30 | # NEODB_WEB_WORKER_NUM=32
31 | # NEODB_API_WORKER_NUM=16
32 | # NEODB_RQ_WORKER_NUM=8
33 | # TAKAHE_WEB_WORKER_NUM=32
34 | # TAKAHE_STATOR_CONCURRENCY=10
35 | # TAKAHE_STATOR_CONCURRENCY_PER_MODEL=10
36 |
37 | # SHOULD uncomment these if you are doing development:
38 | # NEODB_IMAGE=neodb/neodb:edge
39 | # NEODB_DEBUG=True
40 | # TAKAHE_NO_FEDERATION=True
41 |
42 | # see https://neodb.net/configuration/ for more configuration options
43 |
--------------------------------------------------------------------------------
/social/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/social/__init__.py
--------------------------------------------------------------------------------
/social/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/social/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SocialConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "social"
7 |
8 | def ready(self):
9 | pass
10 |
--------------------------------------------------------------------------------
/social/migrations/0001_initial_0_11.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.21 on 2025-05-21 18:49
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | replaces = [
8 | ("social", "0001_initial_0_10"),
9 | ("social", "0002_delete_localactivity"),
10 | ]
11 |
12 | initial = True
13 |
14 | dependencies = []
15 |
16 | operations = []
17 |
--------------------------------------------------------------------------------
/social/migrations/0002_delete_localactivity.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.21 on 2025-05-21 18:46
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("social", "0001_initial_0_10"),
9 | ]
10 |
11 | operations = [
12 | migrations.DeleteModel(
13 | name="LocalActivity",
14 | ),
15 | ]
16 |
--------------------------------------------------------------------------------
/social/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/social/migrations/__init__.py
--------------------------------------------------------------------------------
/social/templates/event/_post.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% include "action_reply_post.html" %}
4 | {% include "action_like_post.html" %}
5 | {% include "action_boost_post.html" %}
6 | {% include "action_open_post.html" %}
7 |
8 | {% if post.summary %}
9 |
10 | {{ post.summary }}
11 | {{ post.safe_content_local }}
12 |
13 | {% else %}
14 | {{ post.safe_content_local }}
15 | {% endif %}
16 | {% for attachment in post.attachments.all %}
17 |
18 |
19 | {{ attachment.file_display_name }}
20 |
21 |
22 | {% endfor %}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/social/templates/event/announcement.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | unsupported notification: announcement
3 |
--------------------------------------------------------------------------------
/social/templates/event/boost.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | unsupported notification: boost
3 |
--------------------------------------------------------------------------------
/social/templates/event/boosted.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load bleach_tags %}
3 | {% trans 'boosted your post' %}
4 |
5 | {% include "_event_post.html" with post=event.post %}
6 |
7 |
--------------------------------------------------------------------------------
/social/templates/event/boosted_collection.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans with piece_url=event.piece.url piece_title=event.piece.title %}
3 | boosted your collection {{ piece_title }}
4 | {% endblocktrans %}
5 |
--------------------------------------------------------------------------------
/social/templates/event/boosted_comment.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | boosted your comment on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.rating_grade %}{{ event.piece.rating_grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 |
--------------------------------------------------------------------------------
/social/templates/event/boosted_note.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | boosted your note on {{ item_title }}
5 | {% endblocktrans %}
6 | {% if event.piece.progress_value %}
7 | {{ event.piece.progress_display }}
8 | {% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 |
--------------------------------------------------------------------------------
/social/templates/event/boosted_rating.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | boosted your rating on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.grade %}{{ event.piece.grade|rating_star }}{% endif %}
9 |
10 |
11 |
--------------------------------------------------------------------------------
/social/templates/event/boosted_review.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title piece_url=event.piece.url piece_title=event.piece.title %}
3 | boosted your review {{ piece_title }} on {{ item_title }}
4 | {% endblocktrans %}
5 |
--------------------------------------------------------------------------------
/social/templates/event/boosted_shelfmember.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | boosted your mark on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.mark.rating_grade %}{{ event.piece.mark.rating_grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.mark.comment.html|safe }}
11 |
12 |
--------------------------------------------------------------------------------
/social/templates/event/follow_requested.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load bleach_tags %}
3 | {% trans 'requested to follow you' %}
4 |
5 | {% include 'users/profile_actions.html' with show_home=1 identity=event.identity %}
6 | @{{ event.identity.full_handle }}
10 |
11 |
12 | {{ event.identity.summary|bleach:"a,p,span,br"|default:"" }}
13 |
14 |
--------------------------------------------------------------------------------
/social/templates/event/followed.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load bleach_tags %}
3 | {% trans 'followed you' %}
4 |
5 | {% include 'users/profile_actions.html' with show_home=1 identity=event.identity %}
6 | @{{ event.identity.full_handle }}
10 |
11 |
12 | {{ event.identity.summary|bleach:"a,p,span,br"|default:"" }}
13 |
14 |
--------------------------------------------------------------------------------
/social/templates/event/identity_created.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | joined {{ site_name }}
3 |
--------------------------------------------------------------------------------
/social/templates/event/liked.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load bleach_tags %}
3 | {% trans 'liked your post' %}
4 |
5 | {% include "_event_post.html" with post=event.post %}
6 |
7 |
--------------------------------------------------------------------------------
/social/templates/event/liked_collection.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans with piece_url=event.piece.url piece_title=event.piece.title %}
3 | liked your collection {{ piece_title }}
4 | {% endblocktrans %}
5 |
--------------------------------------------------------------------------------
/social/templates/event/liked_comment.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | liked your comment on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.rating_grade %}{{ event.piece.rating_grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 |
--------------------------------------------------------------------------------
/social/templates/event/liked_note.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | liked your note on {{ item_title }}
5 | {% endblocktrans %}
6 | {% if event.piece.progress_value %}
7 | {{ event.piece.progress_display }}
8 | {% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 |
--------------------------------------------------------------------------------
/social/templates/event/liked_rating.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | liked your rating on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.grade %}{{ event.piece.grade|rating_star }}{% endif %}
9 |
10 |
11 |
--------------------------------------------------------------------------------
/social/templates/event/liked_review.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title piece_url=event.piece.url piece_title=event.piece.title %}
3 | liked your review {{ piece_title }} on {{ item_title }}
4 | {% endblocktrans %}
5 |
--------------------------------------------------------------------------------
/social/templates/event/liked_shelfmember.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | liked your mark on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.mark.rating_grade %}{{ event.piece.mark.rating_grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.mark.comment.html|safe }}
11 |
12 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load bleach_tags %}
3 | {% trans 'mentioned you' %}
4 |
5 | {% include "_event_post.html" with post=event.post hide_actions=1 %}
6 |
7 | {% include "_event_post.html" with post=event.reply %}
8 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned_collection.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans with piece_url=event.piece.url piece_title=event.piece.title %}
3 | replied to your collection {{ piece_title }}
4 | {% endblocktrans %}
5 | {% include "_event_post.html" with post=event.reply %}
6 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned_comment.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | replied to your comment on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.rating_grade %}{{ event.piece.rating_grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 | {% include "_event_post.html" with post=event.reply %}
13 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned_note.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | replied to your note on {{ item_title }}
5 | {% endblocktrans %}
6 | {% if event.piece.progress_value %}
7 | {{ event.piece.progress_display }}
8 | {% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 | {% include "_event_post.html" with post=event.reply %}
13 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned_rating.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | replied to your rating on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.grade %}{{ event.piece.grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.html|safe }}
11 |
12 | {% include "_event_post.html" with post=event.reply %}
13 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned_review.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title piece_url=event.piece.url piece_title=event.piece.title %}
3 | replied to your review {{ piece_title }} on {{ item_title }}
4 | {% endblocktrans %}
5 | {% include "_event_post.html" with post=event.reply %}
6 |
--------------------------------------------------------------------------------
/social/templates/event/mentioned_shelfmember.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load duration %}
3 | {% blocktrans with item_url=event.item.url item_title=event.item.display_title %}
4 | replied to your mark on {{ item_title }}
5 | {% endblocktrans %}
6 |
7 |
8 | {% if event.piece.mark.rating_grade %}{{ event.piece.mark.rating_grade|rating_star }}{% endif %}
9 |
10 | {{ event.piece.mark.comment.html|safe }}
11 |
12 | {% include "_event_post.html" with post=event.reply %}
13 |
--------------------------------------------------------------------------------
/social/templates/event/post.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | unsupported notification: post
3 |
--------------------------------------------------------------------------------
/social/templates/feed_events.html:
--------------------------------------------------------------------------------
1 | {% load bleach_tags %}
2 | {% load static %}
3 | {% load i18n %}
4 | {% load l10n %}
5 | {% load humanize %}
6 | {% load mastodon %}
7 | {% load thumb %}
8 | {% load user_actions %}
9 | {% load duration %}
10 | {% for event in events %}
11 | {% include "_event_post.html" with post=event.subject_post piece=event.subject_post.piece item=event.subject_post.item %}
12 | {% if forloop.last %}
13 |
18 |
19 |
20 | {% endif %}
21 | {% empty %}
22 |
23 | {% if request.GET.last or page > 1 %}
24 | {% trans 'nothing more.' %}
25 | {% elif request.GET.q %}
26 | {% trans 'no matching activities.' %}
27 | {% else %}
28 | {% url 'users:data' as import_url %}
29 | {% blocktrans %}Find and mark some books/movies/podcasts/games,
import your data from Goodreads/Letterboxd/Douban, follow some fellow {{ site_name }} users on the fediverse, so their recent activities and yours will show up here.{% endblocktrans %}
30 | {% endif %}
31 |
32 | {% endfor %}
33 |
--------------------------------------------------------------------------------
/social/templates/post_question.html:
--------------------------------------------------------------------------------
1 | {% load user_actions %}
2 | {% load i18n %}
3 | {% post_vote_info post as info %}
4 |
29 |
--------------------------------------------------------------------------------
/social/templates/search_feed.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load mastodon %}
5 | {% load thumb %}
6 | {% get_current_language as LANGUAGE_CODE %}
7 |
8 |
9 |
10 |
11 |
12 | {{ site_name }} - {% trans 'Activities from those you follow' %}
13 | {% include "common_libs.html" %}
14 |
15 |
17 |
18 |
19 |
20 | {% include "_header.html" %}
21 |
22 |
23 | {% include 'search_header.html' %}
24 |
31 |
32 | {% block sidebar %}
33 | {% include "_sidebar.html" with show_profile=1 identity=user.identity sidebar_template="_sidebar_search_journal.html" bottom=1 %}
34 | {% endblock %}
35 |
36 | {% include "_footer.html" %}
37 |
38 |
39 |
--------------------------------------------------------------------------------
/social/tests.py:
--------------------------------------------------------------------------------
1 | from catalog.models import *
2 | from journal.models import *
3 |
4 | from .models import *
5 |
--------------------------------------------------------------------------------
/social/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .views import *
4 |
5 | app_name = "social"
6 | urlpatterns = [
7 | path("", feed, name="feed"),
8 | path("focus", focus, name="focus"),
9 | path("data", data, name="data"),
10 | path("search_data", search_data, name="search_data"),
11 | path("notification", notification, name="notification"),
12 | path("dismiss_notification", dismiss_notification, name="dismiss_notification"),
13 | path("events", events, name="events"),
14 | ]
15 |
--------------------------------------------------------------------------------
/takahe/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/takahe/__init__.py
--------------------------------------------------------------------------------
/takahe/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/takahe/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TakaheConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "takahe"
7 |
8 | def ready(self):
9 | # register cron jobs
10 | from .jobs import TakaheStats # noqa
11 |
--------------------------------------------------------------------------------
/takahe/db_routes.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | _is_testing = "testserver" in settings.ALLOWED_HOSTS
4 |
5 |
6 | class TakaheRouter:
7 | def db_for_read(self, model, **hints):
8 | if model._meta.app_label == "takahe":
9 | return "takahe"
10 | return None
11 |
12 | def db_for_write(self, model, **hints):
13 | if model._meta.app_label == "takahe":
14 | return "takahe"
15 | return None
16 |
17 | def allow_relation(self, obj1, obj2, **hints):
18 | # skip this check but please make sure
19 | # not create relations between takahe models and other apps
20 | if obj1._meta.app_label == "takahe" or obj2._meta.app_label == "takahe":
21 | return obj1._meta.app_label == obj2._meta.app_label
22 | return None
23 |
24 | def allow_migrate(self, db, app_label, model_name=None, **hints):
25 | if app_label == "takahe" or db == "takahe":
26 | return _is_testing and app_label == db
27 | return None
28 |
--------------------------------------------------------------------------------
/takahe/management/commands/takahe.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from loguru import logger
3 | from tqdm import tqdm
4 |
5 | from catalog.common import *
6 | from catalog.common.models import *
7 | from catalog.models import *
8 | from takahe.utils import *
9 | from users.models import User as NeoUser
10 |
11 |
12 | class Command(BaseCommand):
13 | def add_arguments(self, parser):
14 | parser.add_argument(
15 | "--verbose",
16 | action="store_true",
17 | )
18 | parser.add_argument(
19 | "--sync",
20 | action="store_true",
21 | )
22 |
23 | def sync(self):
24 | logger.info("Syncing domain...")
25 | Takahe.get_domain()
26 | logger.info("Syncing users...")
27 | for u in tqdm(NeoUser.objects.filter(is_active=True, username__isnull=False)):
28 | Takahe.init_identity_for_local_user(u)
29 | # Takahe.update_user_following(u)
30 | # Takahe.update_user_muting(u)
31 | # Takahe.update_user_rejecting(u)
32 |
33 | def handle(self, *args, **options):
34 | self.verbose = options["verbose"]
35 |
36 | if options["sync"]:
37 | self.sync()
38 |
39 | self.stdout.write(self.style.SUCCESS("Done."))
40 |
--------------------------------------------------------------------------------
/takahe/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/takahe/migrations/__init__.py
--------------------------------------------------------------------------------
/takahe/tests.py:
--------------------------------------------------------------------------------
1 | # Create your tests here.
2 |
--------------------------------------------------------------------------------
/takahe/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .views import *
4 |
5 | app_name = "takahe"
6 | urlpatterns = [
7 | path("auth/login/", auth_login, name="auth_login"),
8 | path("auth/logout/", auth_logout, name="auth_logout"),
9 | ]
10 |
--------------------------------------------------------------------------------
/test_data/https___anchor_fm_s_64d6bbe0_podcast_rss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/test_data/https___anchor_fm_s_64d6bbe0_podcast_rss
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt0314979_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[{"backdrop_path":"/zJkLznQNzeLfKuD238Czo6jk65X.jpg","id":71365,"name":"Battlestar Galactica","original_name":"Battlestar Galactica","overview":"A re-imagining of the original series in which a \"rag-tag fugitive fleet\" of the last remnants of mankind flees pursuing robots while simultaneously searching for their true home, Earth.","poster_path":"/imTQ4nBdA68TVpLaWhhQJnb7NQh.jpg","media_type":"tv","adult":false,"original_language":"en","genre_ids":[10759,18,10765],"popularity":51.919,"first_air_date":"2003-12-08","vote_average":8.184,"vote_count":801,"origin_country":["CA"]}],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt0314979_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[{"adult":false,"backdrop_path":"/y6EakvqM6pdBX900TzQPkn4v5yF.jpg","id":71365,"name":"太空堡垒卡拉狄加 迷你剧","original_language":"en","original_name":"Battlestar Galactica","overview":"未来世界,人类已由地球移民到其它星系居住;人们为了贪求私欲,便开始研究及制造一种拥有独立思想及感觉的机械人——Cylons来为人类服务。但人们怎样也想不到,这些本应是帮助人类的机械人竟会调 转头对抗自已,因此,人类与自已制造的机械人之战争便开始展开……在经过数十年漫长、死伤无数的战争结朿后,人们本以为可得到和平的生活,但Cylons明白到即使在休战后始终没辨法与人类建立实际的外交关系,故Cylons开始密谋另一个对付人类的计划,这次它们将会制造一些与人类一模一样的机械人作间碟渗入人类世界,从而以里应外合的方法把人类消灭。","poster_path":"/imTQ4nBdA68TVpLaWhhQJnb7NQh.jpg","media_type":"tv","genre_ids":[10759,18,10765],"popularity":18.149,"first_air_date":"2003-12-08","vote_average":8.19,"vote_count":686,"origin_country":[]}],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt0436992_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[{"backdrop_path":"/nfH8SZJVOxcBlFaqqtoqS5hHizG.jpg","id":57243,"name":"Doctor Who","original_name":"Doctor Who","overview":"The Doctor is a Time Lord: a 900 year old alien with 2 hearts, part of a gifted civilization who mastered time travel. The Doctor saves planets for a living—more of a hobby actually, and the Doctor's very, very good at it.","poster_path":"/4edFyasCrkH4MKs6H4mHqlrxA6b.jpg","media_type":"tv","adult":false,"original_language":"en","genre_ids":[10759,18,10765],"popularity":1090.391,"first_air_date":"2005-03-26","vote_average":7.519,"vote_count":2930,"origin_country":["GB"]}],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt0436992_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[{"adult":false,"backdrop_path":"/sRfl6vyzGWutgG0cmXmbChC4iN6.jpg","id":57243,"name":"神秘博士","original_language":"en","original_name":"Doctor Who","overview":"名为“博士”的宇宙最后一个时间领主,有着重生的能力、体力及优越的智力,利用时光机器TARDIS英国传统的蓝色警亭,展开他勇敢的时光冒险之旅,拯救外星生物、地球与时空。","poster_path":"/sz4zF5z9zyFh8Z6g5IQPNq91cI7.jpg","media_type":"tv","genre_ids":[10759,18,10765],"popularity":158.575,"first_air_date":"2005-03-26","vote_average":7.402,"vote_count":2475,"origin_country":["GB"]}],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt0827573_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[{"backdrop_path":"/eCTWG4HzsOQomghw8sRd1qpeOlA.jpg","id":282758,"title":"Doctor Who: The Runaway Bride","original_title":"Doctor Who: The Runaway Bride","overview":"A young bride in the midst of her wedding finds herself mysteriously transported to the TARDIS. The Doctor must discover what her connection is with the Empress of the Racnoss's plan to destroy the world.","poster_path":"/dy7JzhXnDFhQsHRiPXxpu62j3yQ.jpg","media_type":"movie","adult":false,"original_language":"en","genre_ids":[878],"popularity":17.25,"release_date":"2006-12-25","video":false,"vote_average":7.728,"vote_count":224}],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1008547,"name":"The Runaway Bride","overview":"Bride-to-be Donna vanishes as she walks down the aisle to marry boyfriend Lance. To her complete astonishment - and the Doctor's - she reappears in the Tardis. As the Time Lord, still reeling from Rose's departure, investigates how Donna came to be there, the duo uncover a terrifying enemy. How far will the Doctor go to save Earth from the latest alien threat?","media_type":"tv_episode","vote_average":6.925,"vote_count":20,"air_date":"2006-12-25","episode_number":4,"episode_type":"standard","production_code":"NCFT094N","runtime":64,"season_number":0,"show_id":57243,"still_path":"/pncNamTuydXWinybPuMTsBUVjSD.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt0827573_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[{"adult":false,"backdrop_path":"/13qDzilftzRZMUEHcpi57VLqNPw.jpg","id":282758,"title":"神秘博士:逃跑新娘","original_language":"en","original_title":"Doctor Who: The Runaway Bride","overview":"失去了罗斯的博士正在心灰意冷,而正在举行婚礼的多娜却被突然传送到塔迪斯里。博士带坏脾气的多娜返回地球,却被一群外星机器人追杀,塔迪斯上演了一场公路飚车。后来博士发现多娜身上带有异常含量的Huon粒子,而该粒子来源于上一代宇宙霸主。而博士的母星加利弗雷在宇宙中崛起时,已经消灭了所有的Huon粒子。最终博士揭开了一个藏于地球40亿年的秘密。","poster_path":"/gkTCC4VLv8jATM3kouAUK3EaoGd.jpg","media_type":"movie","genre_ids":[878],"popularity":7.214,"release_date":"2006-12-25","video":false,"vote_average":7.739,"vote_count":201}],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1008547,"name":"2006年圣诞特辑:逃跑新娘","overview":"失去了罗斯的博士正在心灰意冷,而正在举行婚礼的多娜却被突然传送到塔迪斯里。博士带坏脾气的多娜返回地球,却被一群外星机器人追杀,塔迪斯上演了一场公路飚车。后来博士发现多娜身上带有异常含量的Huon粒子,而该粒子来源于上一代宇宙霸主。而博士的母星加利弗雷在宇宙中崛起时,已经消灭了所有的Huon粒子。最终博士揭开了一个藏于地球40亿年的秘密。","media_type":"tv_episode","vote_average":6.8,"vote_count":14,"air_date":"2006-12-25","episode_number":4,"production_code":"NCFT094N","runtime":64,"season_number":0,"show_id":57243,"still_path":"/mkJufoqvEBMVvnVUjYlR9lGarZB.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt10751754_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt10751754_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt10751820_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt10751820_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1159991_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":941505,"name":"Partners in Crime","overview":"During an alien emergency in London, a woman called Donna Noble must search for an old friend who can save the day - a man named the Doctor. But can even the Doctor halt the plans of the mysterious Miss Foster?","media_type":"tv_episode","vote_average":7.26,"vote_count":52,"air_date":"2008-04-05","episode_number":1,"episode_type":"standard","production_code":"","runtime":51,"season_number":4,"show_id":57243,"still_path":"/vg5oP1tOzivl4EV7iHiEaKwiZkK.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1159991_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":941505,"name":"活宝搭档","overview":"博士在伦敦发现艾迪派斯公司新产品药物有问题,人类服用后会悄悄的产生土豆状生物,并在夜里1点10分逃走回到保姆身边,于是博士潜入公司决定探查究竟,在探查时遇到了多娜原来Adiposian人丢失了他们的繁育星球,于是跑到地球利用人类做代孕母繁殖宝宝。最后保姆在高空中被抛弃,脂肪球回到了父母身边,博士邀请多娜一同旅行。【Rose从平行宇宙回归】","media_type":"tv_episode","vote_average":7.074,"vote_count":46,"air_date":"2008-04-05","episode_number":1,"production_code":"","runtime":null,"season_number":4,"show_id":57243,"still_path":"/cq1zrCS267vGXa3rCYQkVKNJE9v.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1375666_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[{"backdrop_path":"/8ZTVqvKDQ8emSGUEMjsS4yHAwrp.jpg","id":27205,"title":"Inception","original_title":"Inception","overview":"Cobb, a skilled thief who commits corporate espionage by infiltrating the subconscious of his targets is offered a chance to regain his old life as payment for a task considered to be impossible: \"inception\", the implantation of another person's idea into a target's subconscious.","poster_path":"/oYuLEt3zVCKq57qu2F8dT7NIa6f.jpg","media_type":"movie","adult":false,"original_language":"en","genre_ids":[28,878,12],"popularity":92.871,"release_date":"2010-07-15","video":false,"vote_average":8.369,"vote_count":35987}],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1375666_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[{"adult":false,"backdrop_path":"/s3TBrRGB1iav7gFOCNx3H31MoES.jpg","id":27205,"title":"盗梦空间","original_language":"en","original_title":"Inception","overview":"道姆·柯布与同事阿瑟和纳什在一次针对日本能源大亨齐藤的盗梦行动中失败,反被齐藤利用。齐藤威逼利诱因遭通缉而流亡海外的柯布帮他拆分他竞争对手的公司,采取极端措施在其唯一继承人罗伯特·费希尔的深层潜意识中种下放弃家族公司、自立门户的想法。为了重返美国,柯布偷偷求助于岳父迈尔斯,吸收了年轻的梦境设计师艾里阿德妮、梦境演员艾姆斯和药剂师约瑟夫加入行动。在一层层递进的梦境中,柯布不仅要对付费希尔潜意识的本能反抗,还必须直面已逝妻子梅的处处破坏,实际情况远比预想危险得多…","poster_path":"/lQEjWasu07JbQHdfFI5VnEUfId2.jpg","media_type":"movie","genre_ids":[28,878,12],"popularity":74.425,"release_date":"2010-07-15","video":false,"vote_average":8.359,"vote_count":32695}],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt15389382_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":3854678,"name":"双胞胎贝丝","overview":"夏茉和莫蒂沉浸在全新的超逼真游戏机。","media_type":"tv_episode","vote_average":8.8,"vote_count":8,"air_date":"2022-09-18","episode_number":3,"production_code":"","runtime":22,"season_number":6,"show_id":60625,"still_path":"/8ZCNAxkDo67GtcBJBDIERdywnar.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1699275_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1305550,"name":"Part 1","overview":"In a distant part of the galaxy lie The Twelve Colonies of Man, a civilization that has been at peace for some forty years with an empire of machines, the Cylons, who were created generations before as worker drones for mankind, but became independent, rose in rebellion, and launched war on their masters. Now, the Cylons have evolved into more human form, into machine-created biological beings, who seek to exterminate true biological humans. To this end they use a human scientist, Gaius, to help one of their infiltrators, known as #6, penetrate the Colonies' master ...","media_type":"tv_episode","vote_average":8.1,"vote_count":20,"air_date":"2003-12-08","episode_number":1,"episode_type":"standard","production_code":"","runtime":95,"season_number":1,"show_id":71365,"still_path":"/mBkKJW9ppIEjkD4CXGaAntekQNm.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1699275_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1305550,"name":"迷你剧上","overview":"40年的和平结束,人类遭受到赛昂人灭绝种族式的攻击,幸存者被迫逃离他们的12个殖民地。","media_type":"tv_episode","vote_average":8.1,"vote_count":20,"air_date":"2003-12-08","episode_number":1,"production_code":"","runtime":95,"season_number":1,"show_id":71365,"still_path":"/mBkKJW9ppIEjkD4CXGaAntekQNm.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1699276_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1305551,"name":"Part 2","overview":"After forty years of armistice, the Cylons attacks the Twelve Colonies of Kobol. Their strategy: a virus implanted into the mankind defense system. The former Battlestar Galactica, which is being adapted into a museum, is not connected with the defense system and becomes the only warship capable of fighting against the Cylons in the hopes of leading the survivors to planet 'Earth'.","media_type":"tv_episode","vote_average":8.118,"vote_count":17,"air_date":"2003-12-09","episode_number":2,"episode_type":"finale","production_code":"","runtime":90,"season_number":1,"show_id":71365,"still_path":"/77kEx9Zw6yI69oCrffQc1hIGOjC.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt1699276_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1305551,"name":"迷你剧下","overview":"40年的和平结束,人类遭受到赛昂人灭绝种族式的攻击,幸存者被迫逃离他们的12个殖民地。","media_type":"tv_episode","vote_average":8.1,"vote_count":16,"air_date":"2003-12-09","episode_number":2,"production_code":"","runtime":90,"season_number":1,"show_id":71365,"still_path":"/77kEx9Zw6yI69oCrffQc1hIGOjC.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt21599650_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":3891529,"name":"Solaricks","overview":"The Smiths deal with last season's fallout, and Rick and Morty are stranded in space.","media_type":"tv_episode","vote_average":8.091,"vote_count":55,"air_date":"2022-09-04","episode_number":1,"episode_type":"standard","production_code":"","runtime":23,"season_number":6,"show_id":60625,"still_path":"/5tiOEjp03nvaGiKT73knretU8e8.jpg"}],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt7660970_api_key_8964_language_en_US_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[{"backdrop_path":"/8IC1q0lHFwi5m8VtChLzIfmpaZH.jpg","id":86941,"name":"The North Water","original_name":"The North Water","overview":"Henry Drax is a harpooner and brutish killer whose amorality has been shaped to fit the harshness of his world, who will set sail on a whaling expedition to the Arctic with Patrick Sumner, a disgraced ex-army surgeon who signs up as the ship’s doctor. Hoping to escape the horrors of his past, Sumner finds himself on an ill-fated journey with a murderous psychopath. In search of redemption, his story becomes a harsh struggle for survival in the Arctic wasteland.","poster_path":"/9CM0ca8pX1os3SJ24hsIc0nN8ph.jpg","media_type":"tv","adult":false,"original_language":"en","genre_ids":[18,9648],"popularity":40.783,"first_air_date":"2021-07-14","vote_average":7.392,"vote_count":120,"origin_country":["US"]}],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___api_themoviedb_org_3_find_tt7660970_api_key_8964_language_zh_CN_external_source_imdb_id:
--------------------------------------------------------------------------------
1 | {"movie_results":[],"person_results":[],"tv_results":[{"adult":false,"backdrop_path":"/8IC1q0lHFwi5m8VtChLzIfmpaZH.jpg","id":86941,"name":"北海鲸梦","original_language":"en","original_name":"The North Water","overview":"改编自伊恩·麦奎尔的同名获奖小说,聚焦19世纪一次灾难性的捕鲸活动。故事围绕帕特里克·萨姆纳展开,他是一名声名狼藉的前战地医生,后成为捕鲸船上的医生,在船上遇到了鱼叉手亨利·德拉克斯,一个残忍、不道德的杀手。萨姆纳没有逃离过去的恐惧,而是被迫在北极荒原上为生存而进行残酷的斗争...","poster_path":"/9CM0ca8pX1os3SJ24hsIc0nN8ph.jpg","media_type":"tv","genre_ids":[18,9648],"popularity":11.318,"first_air_date":"2021-07-14","vote_average":7.5,"vote_count":75,"origin_country":["US"]}],"tv_episode_results":[],"tv_season_results":[]}
--------------------------------------------------------------------------------
/test_data/https___feeds_simplecast_com_82FI35Px:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/test_data/https___feeds_simplecast_com_82FI35Px
--------------------------------------------------------------------------------
/test_data/https___open_spotify_com_oembed_url_https___open_spotify_com_album_65KwtzkJXw7oT819NFWmEP:
--------------------------------------------------------------------------------
1 | {"html":"","iframe_url":"https://open.spotify.com/embed/album/65KwtzkJXw7oT819NFWmEP?utm_source=oembed","width":456,"height":352,"version":"1.0","provider_name":"Spotify","provider_url":"https://spotify.com","type":"rich","title":"The Race For Space","thumbnail_url":"https://image-cdn-ak.spotifycdn.com/image/ab67616d00001e02123ebfc7ca99a9bb6342cd36","thumbnail_width":300,"thumbnail_height":300}
--------------------------------------------------------------------------------
/test_data/https___podcasts_files_bbci_co_uk_b006qykl_rss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/test_data/https___podcasts_files_bbci_co_uk_b006qykl_rss
--------------------------------------------------------------------------------
/test_data/https___rsshub_app_ximalaya_album_51101122_0_shownote:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/test_data/https___rsshub_app_ximalaya_album_51101122_0_shownote
--------------------------------------------------------------------------------
/test_data/https___tiaodao_typlog_io_feed_xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/test_data/https___tiaodao_typlog_io_feed_xml
--------------------------------------------------------------------------------
/test_data/https___www_digforfire_net_digforfire_radio_feed_xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/test_data/https___www_digforfire_net_digforfire_radio_feed_xml
--------------------------------------------------------------------------------
/test_data/https___www_ypshuo_com_api_novel_getInfo_novelId_1:
--------------------------------------------------------------------------------
1 | {"code":"00","data":{"id":1,"novel_name":"诡秘之主","category_id":1,"novel_img":"https://qidian.qpic.cn/qdbimg/349573/1010868264/300","author_name":"爱潜水的乌贼","synopsis":"蒸汽与机械的浪潮中,谁能触及非凡?历史和黑暗的迷雾里,又是谁在耳语?我从诡秘中醒来,睁眼看见这个世界:枪械,大炮,巨舰,飞空艇,差分机;魔药,占卜,诅咒,倒吊人,封印物……光明依旧照耀,神秘从未远离,这是一段“愚者”的传说。","word_number":4465200,"update_status":1,"update_explain":null,"status":2,"source":"[{\"bookId\":\"1010868264\",\"siteName\":\"起点中文网\",\"bookPage\":\"http://book.qidian.com/info/1010868264\"},{\"bookId\":\"20868264\",\"siteName\":\"创世中文网\",\"bookPage\":\"http://chuangshi.qq.com/bk/ly/20868264.html\"}]","power":"0","point":328,"score":8.7,"scorer":120,"score_1":8,"scorer_1":2,"score_2":10,"scorer_2":1,"score_3":8.7,"scorer_3":117,"create_time":1605427200,"update_time":1605427200,"novel_tags":[{"tag_name":"异世大陆"}],"novel_category":{"cate_name":"玄幻"}}}
--------------------------------------------------------------------------------
/test_data/igdb_websites_fields____where_game_url____https___www_igdb_com_games_portal_2__:
--------------------------------------------------------------------------------
1 | [{"id": 17869, "category": 1, "game": 72, "trusted": false, "url": "http://www.thinkwithportals.com/", "checksum": "c2d5f8d1-6178-75f3-18ec-7b23c114d396"}, {"id": 17870, "category": 13, "game": 72, "trusted": true, "url": "https://store.steampowered.com/app/620", "checksum": "d622151c-d8cf-8843-c3e7-65d50f29b460"}, {"id": 41194, "category": 3, "game": 72, "trusted": true, "url": "https://en.wikipedia.org/wiki/Portal_2", "checksum": "b40db784-417c-2f63-4c5e-b45a8cdba054"}, {"id": 41195, "category": 4, "game": 72, "trusted": true, "url": "https://www.facebook.com/Portal", "checksum": "936dbbda-3698-362b-7fbe-56e5234de2b3"}, {"id": 150881, "category": 9, "game": 72, "trusted": true, "url": "https://www.youtube.com/user/Valve", "checksum": "f64c60a1-46f6-8e18-bb0f-114891e70261"}, {"id": 150882, "category": 5, "game": 72, "trusted": true, "url": "https://twitter.com/valvesoftware", "checksum": "096a3b5a-008a-6042-8678-fbe6845ef85d"}, {"id": 150883, "category": 2, "game": 72, "trusted": false, "url": "https://theportalwiki.com/wiki/Portal_2", "checksum": "02d302e6-ace7-a642-2a87-7c15cbbbc39c"}, {"id": 332601, "category": 6, "game": 72, "trusted": true, "url": "https://www.twitch.tv/directory/game/Portal%202", "checksum": "5ad23ff6-fc6c-74ba-8d0d-0acb84f16815"}]
--------------------------------------------------------------------------------
/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/users/__init__.py
--------------------------------------------------------------------------------
/users/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import *
4 |
5 | admin.site.register(User)
6 | admin.site.register(Preference)
7 |
--------------------------------------------------------------------------------
/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UsersConfig(AppConfig):
5 | name = "users"
6 |
7 | def ready(self):
8 | from . import apis # noqa
9 |
10 | # register cron jobs
11 | from users.jobs import MastodonUserSync # noqa
12 |
--------------------------------------------------------------------------------
/users/jobs/__init__.py:
--------------------------------------------------------------------------------
1 | from .sync import MastodonUserSync
2 |
3 | __all__ = ["MastodonUserSync"]
4 |
--------------------------------------------------------------------------------
/users/management/commands/invite.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.management.base import BaseCommand
3 | from django.urls import reverse
4 |
5 | from takahe.utils import Invite
6 |
7 |
8 | class Command(BaseCommand):
9 | help = "Manage invite"
10 |
11 | def add_arguments(self, parser):
12 | parser.add_argument(
13 | "--create",
14 | action="store_true",
15 | )
16 | # parser.add_argument(
17 | # "--revoke",
18 | # action="store_true",
19 | # )
20 |
21 | def handle(self, *args, **options):
22 | if options["create"]:
23 | inv = Invite.create_random()
24 | self.stdout.write(self.style.SUCCESS(f"Invite created: {inv.token}"))
25 | self.stdout.write(
26 | self.style.SUCCESS(
27 | f"Link: {settings.SITE_INFO['site_url']}{reverse('users:login')}?invite={inv.token}"
28 | )
29 | )
30 |
--------------------------------------------------------------------------------
/users/middlewares.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | from django.conf import settings
4 | from django.middleware.locale import LocaleMiddleware
5 | from django.utils import translation
6 |
7 | if TYPE_CHECKING:
8 | from users.models import User
9 |
10 |
11 | def activate_language_for_user(user: "User | None", request=None):
12 | user_language = None
13 | if user and user.is_authenticated:
14 | user_language = getattr(user, "language", "")
15 | if not user_language:
16 | if request:
17 | try:
18 | user_language = translation.get_supported_language_variant(
19 | request.GET.get("lang")
20 | )
21 | except Exception:
22 | user_language = translation.get_language_from_request(request)
23 | else:
24 | user_language = settings.LANGUAGE_CODE
25 | # if user_language in dict(settings.LANGUAGES).keys():
26 | translation.activate(user_language)
27 | if request:
28 | request.LANGUAGE_CODE = translation.get_language()
29 |
30 |
31 | class LanguageMiddleware(LocaleMiddleware):
32 | def process_request(self, request):
33 | user = getattr(request, "user", None)
34 | activate_language_for_user(user, request)
35 |
--------------------------------------------------------------------------------
/users/migrations/0002_preference_auto_bookmark_cats.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-06-16 02:57
2 |
3 | from django.db import migrations, models
4 |
5 | import users.models.preference
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("users", "0001_initial_0_10"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="preference",
16 | name="auto_bookmark_cats",
17 | field=models.JSONField(default=users.models.preference._default_book_cats),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/users/migrations/0002_preference_default_no_share.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.16 on 2023-02-12 13:43
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="preference",
14 | name="default_no_share",
15 | field=models.BooleanField(default=False),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/users/migrations/0003_preference_discover_layout.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.18 on 2023-04-19 21:47
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0002_preference_default_no_share"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="preference",
14 | name="discover_layout",
15 | field=models.JSONField(blank=True, default=list),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/users/migrations/0003_remove_preference_no_anonymous_view.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-06-16 16:21
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0002_preference_auto_bookmark_cats"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="preference",
14 | name="no_anonymous_view",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/users/migrations/0004_alter_preference_classic_homepage.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.18 on 2023-04-20 13:31
2 |
3 | from django.db import migrations, models
4 |
5 | sql = 'ALTER TABLE users_preference ALTER COLUMN "classic_homepage" TYPE SMALLINT USING CASE WHEN classic_homepage THEN 1 ELSE 0 END;'
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("users", "0003_preference_discover_layout"),
11 | ]
12 |
13 | operations = [
14 | migrations.RunSQL(sql),
15 | migrations.AlterField(
16 | model_name="preference",
17 | name="classic_homepage",
18 | field=models.PositiveSmallIntegerField(default=0),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/users/migrations/0004_remove_user_at_least_one_login_method_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-07-02 18:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0003_remove_preference_no_anonymous_view"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveConstraint(
13 | model_name="user",
14 | name="at_least_one_login_method",
15 | ),
16 | migrations.RemoveConstraint(
17 | model_name="user",
18 | name="unique_email",
19 | ),
20 | migrations.RemoveConstraint(
21 | model_name="user",
22 | name="unique_mastodon_username",
23 | ),
24 | migrations.RemoveConstraint(
25 | model_name="user",
26 | name="unique_mastodon_id",
27 | ),
28 | migrations.RemoveIndex(
29 | model_name="user",
30 | name="users_user_mastodo_bd2db5_idx",
31 | ),
32 | migrations.AddIndex(
33 | model_name="user",
34 | index=models.Index(models.F("is_active"), name="index_user_is_active"),
35 | ),
36 | ]
37 |
--------------------------------------------------------------------------------
/users/migrations/0006_alter_task_type.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.17 on 2024-12-26 04:34
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0005_remove_follow_owner_remove_follow_target_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="task",
14 | name="type",
15 | field=models.CharField(
16 | choices=[
17 | ("journal.doubanimporter", "douban importer"),
18 | ("journal.doufenexporter", "doufen exporter"),
19 | ("journal.goodreadsimporter", "goodreads importer"),
20 | ("journal.letterboxdimporter", "letterboxd importer"),
21 | ],
22 | db_index=True,
23 | max_length=255,
24 | ),
25 | ),
26 | migrations.RunSQL(
27 | sql="UPDATE users_task SET type='journal.letterboxdimporter' WHERE type='import.letterboxd'"
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/users/migrations/0006_unique_email.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.19 on 2023-06-30 13:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0005_add_dedicated_username"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="user",
14 | name="email",
15 | field=models.EmailField(
16 | default=None,
17 | null=True,
18 | max_length=254,
19 | unique=False,
20 | verbose_name="email address",
21 | ),
22 | ),
23 | migrations.RunSQL("UPDATE users_user SET email = null;"),
24 | migrations.AlterField(
25 | model_name="user",
26 | name="email",
27 | field=models.EmailField(
28 | default=None,
29 | null=True,
30 | max_length=254,
31 | unique=True,
32 | verbose_name="email address",
33 | ),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/users/migrations/0007_alter_task_type.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.17 on 2025-01-27 07:42
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0006_alter_task_type"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="task",
14 | name="type",
15 | field=models.CharField(
16 | choices=[
17 | ("journal.csvexporter", "csv exporter"),
18 | ("journal.doubanimporter", "douban importer"),
19 | ("journal.doufenexporter", "doufen exporter"),
20 | ("journal.goodreadsimporter", "goodreads importer"),
21 | ("journal.letterboxdimporter", "letterboxd importer"),
22 | ("journal.ndjsonexporter", "ndjson exporter"),
23 | ],
24 | db_index=True,
25 | max_length=255,
26 | ),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/users/migrations/0007_user_pending_email.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.19 on 2023-07-03 18:09
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0006_unique_email"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="user",
14 | name="pending_email",
15 | field=models.EmailField(
16 | default=None,
17 | max_length=254,
18 | null=True,
19 | verbose_name="email address pending verification",
20 | ),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/users/migrations/0008_alter_task_type.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.18 on 2025-03-03 23:16
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0007_alter_task_type"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="task",
14 | name="type",
15 | field=models.CharField(
16 | choices=[
17 | ("journal.baseimporter", "base importer"),
18 | ("journal.csvexporter", "csv exporter"),
19 | ("journal.csvimporter", "csv importer"),
20 | ("journal.doubanimporter", "douban importer"),
21 | ("journal.doufenexporter", "doufen exporter"),
22 | ("journal.goodreadsimporter", "goodreads importer"),
23 | ("journal.letterboxdimporter", "letterboxd importer"),
24 | ("journal.ndjsonexporter", "ndjson exporter"),
25 | ("journal.ndjsonimporter", "ndjson importer"),
26 | ("journal.opmlimporter", "opml importer"),
27 | ],
28 | db_index=True,
29 | max_length=255,
30 | ),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/users/migrations/0008_user_at_least_one_login_method.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.19 on 2023-07-04 02:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0007_user_pending_email"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddConstraint(
13 | model_name="user",
14 | constraint=models.CheckConstraint(
15 | check=models.Q(
16 | ("is_active", False),
17 | ("mastodon_username__isnull", False),
18 | ("email__isnull", False),
19 | _connector="OR",
20 | ),
21 | name="at_least_one_login_method",
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/users/migrations/0011_preference_hidden_categories.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.3 on 2023-07-12 03:46
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0010_add_local_mute_block"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="preference",
14 | name="hidden_categories",
15 | field=models.JSONField(default=list),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/users/migrations/0015_user_mastodon_last_reachable.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.7 on 2023-11-11 05:34
2 |
3 | import django.utils.timezone
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("users", "0011_preference_hidden_categories"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="user",
15 | name="mastodon_last_reachable",
16 | field=models.DateTimeField(default=django.utils.timezone.now),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/users/migrations/0016_rename_preference_default_no_share.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.8 on 2023-12-10 19:26
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0015_remove_preference_mastodon_publish_public_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="preference",
14 | name="mastodon_default_repost",
15 | field=models.BooleanField(default=True),
16 | ),
17 | migrations.RunSQL(
18 | "UPDATE users_preference SET mastodon_default_repost = false where default_no_share = true;"
19 | ),
20 | migrations.RemoveField(
21 | model_name="preference",
22 | name="default_no_share",
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/users/migrations/0017_mastodon_site_username_bd2db5_idx.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.8 on 2023-12-25 21:46
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0016_rename_preference_default_no_share"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddIndex(
13 | model_name="user",
14 | index=models.Index(
15 | fields=["mastodon_site", "mastodon_username"],
16 | name="users_user_mastodo_bd2db5_idx",
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/users/migrations/0018_apidentity_anonymous_viewable.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.8 on 2023-12-29 07:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | def migrate_data(apps, schema_editor):
7 | APIdentity = apps.get_model("users", "APIdentity")
8 | APIdentity.objects.filter(local=False).update(anonymous_viewable=False)
9 | APIdentity.objects.filter(user__isnull=False).update(anonymous_viewable=False)
10 | APIdentity.objects.filter(user__is_active=False).update(anonymous_viewable=False)
11 | APIdentity.objects.filter(user__preference__no_anonymous_view=True).update(
12 | anonymous_viewable=False
13 | )
14 |
15 |
16 | class Migration(migrations.Migration):
17 | dependencies = [
18 | ("users", "0017_mastodon_site_username_bd2db5_idx"),
19 | ]
20 |
21 | operations = [
22 | migrations.AddField(
23 | model_name="apidentity",
24 | name="anonymous_viewable",
25 | field=models.BooleanField(default=True),
26 | ),
27 | migrations.RunPython(migrate_data),
28 | ]
29 |
--------------------------------------------------------------------------------
/users/migrations/0020_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-04-04 01:30
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0019_task"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="user",
14 | name="language",
15 | field=models.CharField(
16 | choices=[("en", "English"), ("zh-hans", "Simplified Chinese")],
17 | default="en",
18 | max_length=10,
19 | verbose_name="language",
20 | ),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/users/migrations/0021_alter_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.13 on 2024-06-02 19:10
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0020_user_language"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="user",
14 | name="language",
15 | field=models.CharField(
16 | choices=[
17 | ("en", "English"),
18 | ("zh-hans", "Simplified Chinese"),
19 | ("zh-hant", "Traditional Chinese"),
20 | ],
21 | default="en",
22 | max_length=10,
23 | verbose_name="language",
24 | ),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/users/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neodb-social/neodb/c94a5c5e8b89c80a3fa8e809647d5e2fc87deaaf/users/migrations/__init__.py
--------------------------------------------------------------------------------
/users/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .apidentity import APIdentity
2 | from .preference import Preference
3 | from .task import Task
4 | from .user import User
5 |
6 | __all__ = ["APIdentity", "Preference", "Task", "User"]
7 |
--------------------------------------------------------------------------------
/users/templates/users/_profile_social_icons.html:
--------------------------------------------------------------------------------
1 | {% if identity.user.mastodon %}
2 |
3 |
7 |
8 |
9 |
10 | {% endif %}
11 | {% if identity.user.threads %}
12 |
13 |
17 |
18 |
19 |
20 | {% endif %}
21 | {% if identity.user.bluesky %}
22 |
23 |
27 |
28 |
29 |
30 | {% endif %}
31 |
--------------------------------------------------------------------------------
/users/templates/users/fetch_identity_failed.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 | {% trans "Unable to find the user, please check your spelling; or the server may be busy, please try again later." %}
5 |
6 |
--------------------------------------------------------------------------------
/users/templates/users/fetch_identity_pending.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load l10n %}
4 | {% load humanize %}
5 | {% load mastodon %}
6 | {% load thumb %}
7 | {% get_current_language as LANGUAGE_CODE %}
8 |
9 |
10 |
11 |
12 |
13 | {{ site_name }} - {% trans 'Searching the fediverse' %}
14 | {% include "common_libs.html" %}
15 |
16 |
17 | {% include '_header.html' %}
18 |
19 |
20 |
21 | {% if handle %}
22 | {% trans "Searching the fediverse" %} - {{ handle }}
23 |
26 |
27 |
28 | {% else %}
29 | {% trans "System busy, please try again in a minute." %}
30 | {% endif %}
31 |
32 |
33 |
34 | {% include "_sidebar_search.html" %}
35 |
36 |
37 | {% include '_footer.html' %}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/users/templates/users/fetch_identity_refresh.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/users/templates/users/home_anonymous.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% get_current_language as LANGUAGE_CODE %}
4 |
5 |
6 |
7 |
8 |
9 | {{ site_name }} - {{ identity.handle }}
10 |
11 |
12 |
13 |
14 |
15 |
16 | {% if identity.local and identity.user.mastodon %}
17 | Mastodon verification
18 | {% endif %}
19 |
20 |
21 |
--------------------------------------------------------------------------------
/users/templates/users/preferences_anonymous.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 | {% load mastodon %}
4 | {% load duration %}
5 | {% load thumb %}
6 | {% get_current_language as LANGUAGE_CODE %}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ site_name }} - {% trans "Preferences" %}
14 | {% include "common_libs.html" %}
15 |
16 |
17 |
18 | {% include "_header.html" %}
19 |
20 | {% include "users/_pref_device.html" with open=1 %}
21 | {% include "_sidebar_anonymous.html" %}
22 |
23 | {% include "_footer.html" %}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/users/templates/users/relationship_list.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% for identity in list %}
3 |
4 | {% include 'users/profile_actions.html' with show_home=1 %}
5 | @{{ identity.handle }}
9 |
10 | {% empty %}
11 | {% trans "nothing so far." %}
12 | {% endfor %}
13 |
14 |
15 |
16 |
19 |
20 |
21 | {% blocktrans %}You may download the list here.{% endblocktrans %}
22 |
23 |
30 |
--------------------------------------------------------------------------------
/users/templates/users/verify_email.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load static %}
3 | {% get_current_language as LANGUAGE_CODE %}
4 |
5 |
6 |
7 |
8 |
9 | {{ site_name }} - {% trans 'Verify Your Email' %}
10 | {% include "common_libs.html" %}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {% trans 'Verify Your Email' %}
19 | {% if success %}
20 |
21 | {{ request.user.email }} {% trans "Verified successfully." %}
22 |
23 | {% trans 'Continue' %}
24 |
25 | {% else %}
26 |
27 | {{ error }}
28 | {% trans "Login again" %}
29 |
30 | {% endif %}
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/users/templates/users/welcome.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load static %}
3 | {% get_current_language as LANGUAGE_CODE %}
4 |
5 |
6 |
7 |
8 |
9 | {{ site_name }} - {% trans 'Register' %}
10 | {% include "common_libs.html" %}
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 | {% trans "Welcome" %}
22 |
23 | {% blocktrans %}
24 | {{ site_name }} is flourishing because of collaborations and contributions from users like you. Please read our rules , and feel free to contact us if you have any question or feedback.
25 | {% endblocktrans %}
26 |
27 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/users/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .account import *
2 | from .actions import *
3 | from .data import *
4 | from .profile import *
5 |
--------------------------------------------------------------------------------