Compare commits

..

16 Commits

Author SHA1 Message Date
0df3dd728d [release] Bump to version 0.15.4 2024-09-09 13:05:17 -04:00
1fe8d8aa51 [boardgames] Add better list template and fix URLs (also for moods) 2024-09-09 12:56:42 -04:00
8f3c7beffa [trails] Make trail admin raw ID field 2024-09-09 12:12:39 -04:00
aac0efbb14 [misc] Cleaning up imports and urls for webpages 2024-09-09 12:12:08 -04:00
b0d4dd0899 [trails] Add new trails app 2024-09-09 12:11:39 -04:00
16db67ea84 [bgg] Add rudimentary bgg push scrobble script 2024-09-09 11:56:42 -04:00
7a747268a1 [lifevents] Remove redundant completion percentage 2024-09-09 11:56:20 -04:00
2037ffc67a [deps] Add webdav client library and fix boto3 2024-09-09 11:55:58 -04:00
2136c1562a [books] Fix calculation of current page 2024-09-08 14:13:40 -04:00
eae169aff7 [version] Bump to version 0.15.3 2024-09-08 00:53:19 -04:00
6c0bd2e409 [scrobbles] Clean up references to book_page_data 2024-09-08 00:52:50 -04:00
d69d7311d5 [version] Bump to 0.15.2 2024-09-08 00:45:36 -04:00
c484dab210 [books] Fix importing order of page data 2024-09-08 00:44:55 -04:00
3d62a6a227 [videos] Fix error looking up TV shows 2024-09-07 23:07:39 -04:00
1a5d4a6717 [scrobbles] Clean up dataclass inheritence 2024-09-07 11:36:36 -04:00
8f0491a90b [pyproject] Upgrade version to 0.15.0 2024-09-07 11:04:01 -04:00
32 changed files with 1671 additions and 663 deletions

1094
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "0.11.12"
version = "0.15.4"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]
@ -38,7 +38,6 @@ honcho = "^1.1.0"
howlongtobeatpy = "^1.0.5"
beautifulsoup4 = "^4.11.2"
django-storages = "^1.13.2"
boto3 = "^1.26.98"
stream-sqlite = "^0.0.41"
ipython = "^8.14.0"
pendulum = "^2.1.2"
@ -46,6 +45,8 @@ trafilatura = "^1.6.3"
django-imagekit = "^5.0.0"
thefuzz = "^0.22.1"
dataclass-wizard = "0.22.0"
webdavclient3 = "^3.14.6"
boto3 = "^1.35.14"
[tool.poetry.group.dev]
optional = true

View File

@ -1,8 +1,15 @@
import csv
import json
import logging
from typing import Optional
from typing import TYPE_CHECKING, Optional
import requests
from bs4 import BeautifulSoup
from django.contrib.auth import get_user_model
User = get_user_model()
if TYPE_CHECKING:
from scrobbles.models import Scrobble
logger = logging.getLogger(__name__)
@ -90,3 +97,56 @@ def lookup_boardgame_from_bgg(lookup_id: str) -> dict:
}
return game_dict
def push_scrobble_to_bgg(scrobble: "Scrobble", user: User) -> Optional[bool]:
bgg_username = user.profile.bgg_username
bgg_password = user.profile.bgg_password
if not bgg_username or bgg_password:
return
login_payload = {
"credentials": {"username": bgg_username, "password": bgg_password}
}
headers = {"content-type": "application/json"}
# TODO Look up past plays for scrobble.media_obj.bggeek_id, and make sure we haven't scrobbled this before
with requests.Session() as s:
p = s.post(
"https://boardgamegeek.com/login/api/v1",
data=json.dumps(login_payload),
headers=headers,
)
players = []
if scrobble.metadata:
for player in scrobble.metadata.players:
if player["user_id"]:
player_user = User.objects.filter(
id=player["user_id"]
).first()
if player_user:
if player_user.bgg_username:
player["username"] = player_user.bgg_username
else:
player["name"] = player_user.username
player["win"] = player.get("win")
player["color"] = player.get("color")
player["new"] = player.get("new")
player["score"] = player.get("score")
players.append(player)
play_payload = {
"playdate": scrobble.timestamp.date.strftime("%Y-%m-%d"),
"length": scrobble.playback_position_seconds / 60,
"comments": "Uploaded from Vrobbler",
"location": scrobble.log.location or None,
"objectid": scrobble.media_obj.bggeek_id,
"quantity": "1",
"action": "save",
"players": players,
"objecttype": "thing",
"ajax": 1,
}

View File

@ -6,10 +6,12 @@ app_name = "boardgames"
urlpatterns = [
path(
"board-game/", views.BoardGameListView.as_view(), name="boardgame_list"
"board-games/",
views.BoardGameListView.as_view(),
name="boardgame_list",
),
path(
"board-game/<slug:slug>/",
"board-games/<slug:slug>/",
views.BoardGameDetailView.as_view(),
name="boardgame_detail",
),

View File

@ -93,7 +93,7 @@ def create_book_from_row(row: list):
title=row[KoReaderBookColumn.TITLE.value],
pages=total_pages,
koreader_data_by_hash={
row[KoReaderBookColumn.MD5.value]: {
str(row[KoReaderBookColumn.MD5.value]): {
"title": row[KoReaderBookColumn.TITLE.value],
"author_str": author_str,
"book_id": row[KoReaderBookColumn.ID.value],
@ -233,7 +233,7 @@ def build_scrobbles_from_book_map(
should_create_scrobble = True
if should_create_scrobble:
scrobble_page_data = OrderedDict(
scrobble_page_data = dict(
sorted(
scrobble_page_data.items(),
key=lambda x: x[1]["start_ts"],
@ -296,7 +296,7 @@ def build_scrobbles_from_book_map(
log_data = {
"koreader_hash": book_dict.get("hash"),
"page_data": scrobble_page_data,
"pages_read": cur_page_number,
"pages_read": len(scrobble_page_data.keys()),
}
scrobbles_to_create.append(
Scrobble(
@ -359,6 +359,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
is_os_file = "https://" not in file_path
if is_os_file:
# Loading sqlite file from local filesystem
con = sqlite3.connect(file_path)
cur = con.cursor()
try:
@ -376,6 +377,8 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
)
new_scrobbles = build_scrobbles_from_book_map(book_map, user)
else:
# Streaming the sqlite file off S3
book_map = {}
for table_name, pragma_table_info, rows in stream_sqlite(
_sqlite_bytes(file_path), max_buffer_size=1_048_576
):
@ -383,6 +386,9 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
if table_name == "book":
book_map = build_book_map(rows)
for table_name, pragma_table_info, rows in stream_sqlite(
_sqlite_bytes(file_path), max_buffer_size=1_048_576
):
if table_name == "page_stat_data":
book_map = build_page_data(rows, book_map, tz)
new_scrobbles = build_scrobbles_from_book_map(book_map, user)

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3
from enum import Enum
from typing import Optional
from bs4 import BeautifulSoup
import requests

View File

@ -34,6 +34,7 @@ from vrobbler.apps.books.locg import (
lookup_comic_from_locg,
lookup_comic_writer_by_locg_slug,
)
from vrobbler.apps.scrobbles.dataclasses import BookLogData
COMICVINE_API_KEY = getattr(settings, "COMICVINE_API_KEY", "")
@ -137,6 +138,10 @@ class Book(LongPlayScrobblableMixin):
def subtitle(self):
return f" by {self.author}"
@property
def logdata_cls(self):
return BookLogData
@property
def primary_image_url(self) -> str:
url = ""
@ -278,19 +283,24 @@ class Book(LongPlayScrobblableMixin):
author.headshot.save(fname, ContentFile(r.content), save=True)
self.authors.add(author)
def page_data_for_user(self, user_id: int, convert_timestamps: bool=True) -> dict:
def page_data_for_user(
self, user_id: int, convert_timestamps: bool = True
) -> dict:
scrobbles = self.scrobble_set.filter(user=user_id)
pages = {}
for scrobble in scrobbles:
if scrobble.book_page_data:
for page, data in scrobble.book_page_data.items():
if scrobble.logdata.page_data:
for page, data in scrobble.logdata.page_data.items():
if convert_timestamps:
data["start_ts"] = datetime.fromtimestamp(data["start_ts"])
data["start_ts"] = datetime.fromtimestamp(
data["start_ts"]
)
data["end_ts"] = datetime.fromtimestamp(data["end_ts"])
pages[page] = data
sorted_pages = OrderedDict(sorted(pages.items(), key=lambda x: x[1]["start_ts"]))
sorted_pages = OrderedDict(
sorted(pages.items(), key=lambda x: x[1]["start_ts"])
)
return sorted_pages

View File

@ -48,7 +48,7 @@ class KoReaderBookRows:
300 + wiggle,
self.DEFAULT_STR,
self.DEFAULT_STR,
hashlib.md5(title.encode()),
hashlib.md5(title.encode()).hexdigest(),
i * wiggle * 20,
120,
]

View File

@ -21,9 +21,10 @@ def test_build_book_map(get_mock, koreader_rows, valid_response):
@mock.patch("requests.get")
def test_load_page_data_to_map(get_mock, koreader_rows, valid_response):
get_mock.return_value = valid_response
book_map = build_book_map(koreader_rows.BOOK_ROWS)
book_map = build_page_data(koreader_rows.PAGE_STATS_ROWS, book_map)
book_map = build_page_data(
koreader_rows.PAGE_STATS_ROWS,
build_book_map(koreader_rows.BOOK_ROWS),
)
assert (
len(book_map[1]["pages"])
== koreader_rows.BOOK_ROWS[0][
@ -45,9 +46,9 @@ def test_build_scrobbles_from_pages(
# Corresponds to number of sessions per book ( 20 pages per session, 120 +/- 15 pages read )
expected_scrobbles = 6 * len(book_map.keys())
assert len(scrobbles) == expected_scrobbles
assert len(scrobbles[0].book_page_data.keys()) == 22
assert len(scrobbles[1].book_page_data.keys()) == 20
assert len(scrobbles[2].book_page_data.keys()) == 20
assert len(scrobbles[3].book_page_data.keys()) == 20
assert len(scrobbles[4].book_page_data.keys()) == 20
assert len(scrobbles[5].book_page_data.keys()) == 18
assert len(scrobbles[0].logdata.page_data.keys()) == 21
assert len(scrobbles[1].logdata.page_data.keys()) == 20
assert len(scrobbles[2].logdata.page_data.keys()) == 20
assert len(scrobbles[3].logdata.page_data.keys()) == 20
assert len(scrobbles[4].logdata.page_data.keys()) == 20
assert len(scrobbles[5].logdata.page_data.keys()) == 18

View File

@ -1,7 +1,6 @@
from django.apps import apps
from django.db import models
from django.urls import reverse
import pendulum
from scrobbles.dataclasses import LifeEventLogData
from scrobbles.mixins import ScrobblableMixin
@ -9,8 +8,6 @@ BNULL = {"blank": True, "null": True}
class LifeEvent(ScrobblableMixin):
COMPLETION_PERCENT = 100
description = models.TextField(**BNULL)
def __str__(self):

View File

@ -5,9 +5,9 @@ app_name = "moods"
urlpatterns = [
path("mood/", views.MoodListView.as_view(), name="mood-list"),
path("moods/", views.MoodListView.as_view(), name="mood-list"),
path(
"mood/<slug:slug>/",
"moods/<slug:slug>/",
views.MoodDetailView.as_view(),
name="mood-detail",
),

View File

@ -0,0 +1,601 @@
# Generated by Django 4.2.16 on 2024-09-09 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("profiles", "0015_userprofile_bgg_username"),
]
operations = [
migrations.AlterField(
model_name="userprofile",
name="timezone",
field=models.CharField(
choices=[
("Pacific/Midway", "(GMT-1100) Pacific/Midway"),
("Pacific/Niue", "(GMT-1100) Pacific/Niue"),
("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"),
("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"),
("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"),
("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"),
("US/Hawaii", "(GMT-1000) US/Hawaii"),
("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"),
("America/Adak", "(GMT-0900) America/Adak"),
("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"),
("America/Anchorage", "(GMT-0800) America/Anchorage"),
("America/Juneau", "(GMT-0800) America/Juneau"),
("America/Metlakatla", "(GMT-0800) America/Metlakatla"),
("America/Nome", "(GMT-0800) America/Nome"),
("America/Sitka", "(GMT-0800) America/Sitka"),
("America/Yakutat", "(GMT-0800) America/Yakutat"),
("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"),
("US/Alaska", "(GMT-0800) US/Alaska"),
("America/Creston", "(GMT-0700) America/Creston"),
("America/Dawson", "(GMT-0700) America/Dawson"),
(
"America/Dawson_Creek",
"(GMT-0700) America/Dawson_Creek",
),
("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"),
("America/Hermosillo", "(GMT-0700) America/Hermosillo"),
("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"),
("America/Mazatlan", "(GMT-0700) America/Mazatlan"),
("America/Phoenix", "(GMT-0700) America/Phoenix"),
("America/Tijuana", "(GMT-0700) America/Tijuana"),
("America/Vancouver", "(GMT-0700) America/Vancouver"),
("America/Whitehorse", "(GMT-0700) America/Whitehorse"),
("Canada/Pacific", "(GMT-0700) Canada/Pacific"),
("US/Arizona", "(GMT-0700) US/Arizona"),
("US/Pacific", "(GMT-0700) US/Pacific"),
(
"America/Bahia_Banderas",
"(GMT-0600) America/Bahia_Banderas",
),
("America/Belize", "(GMT-0600) America/Belize"),
("America/Boise", "(GMT-0600) America/Boise"),
(
"America/Cambridge_Bay",
"(GMT-0600) America/Cambridge_Bay",
),
("America/Chihuahua", "(GMT-0600) America/Chihuahua"),
(
"America/Ciudad_Juarez",
"(GMT-0600) America/Ciudad_Juarez",
),
("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"),
("America/Denver", "(GMT-0600) America/Denver"),
("America/Edmonton", "(GMT-0600) America/Edmonton"),
("America/El_Salvador", "(GMT-0600) America/El_Salvador"),
("America/Guatemala", "(GMT-0600) America/Guatemala"),
("America/Inuvik", "(GMT-0600) America/Inuvik"),
("America/Managua", "(GMT-0600) America/Managua"),
("America/Merida", "(GMT-0600) America/Merida"),
("America/Mexico_City", "(GMT-0600) America/Mexico_City"),
("America/Monterrey", "(GMT-0600) America/Monterrey"),
("America/Regina", "(GMT-0600) America/Regina"),
(
"America/Swift_Current",
"(GMT-0600) America/Swift_Current",
),
("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"),
("America/Yellowknife", "(GMT-0600) America/Yellowknife"),
("Canada/Mountain", "(GMT-0600) Canada/Mountain"),
("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"),
("US/Mountain", "(GMT-0600) US/Mountain"),
("America/Atikokan", "(GMT-0500) America/Atikokan"),
("America/Bogota", "(GMT-0500) America/Bogota"),
("America/Cancun", "(GMT-0500) America/Cancun"),
("America/Cayman", "(GMT-0500) America/Cayman"),
("America/Chicago", "(GMT-0500) America/Chicago"),
("America/Eirunepe", "(GMT-0500) America/Eirunepe"),
("America/Guayaquil", "(GMT-0500) America/Guayaquil"),
(
"America/Indiana/Knox",
"(GMT-0500) America/Indiana/Knox",
),
(
"America/Indiana/Tell_City",
"(GMT-0500) America/Indiana/Tell_City",
),
("America/Jamaica", "(GMT-0500) America/Jamaica"),
("America/Lima", "(GMT-0500) America/Lima"),
("America/Matamoros", "(GMT-0500) America/Matamoros"),
("America/Menominee", "(GMT-0500) America/Menominee"),
(
"America/North_Dakota/Beulah",
"(GMT-0500) America/North_Dakota/Beulah",
),
(
"America/North_Dakota/Center",
"(GMT-0500) America/North_Dakota/Center",
),
(
"America/North_Dakota/New_Salem",
"(GMT-0500) America/North_Dakota/New_Salem",
),
("America/Ojinaga", "(GMT-0500) America/Ojinaga"),
("America/Panama", "(GMT-0500) America/Panama"),
(
"America/Rankin_Inlet",
"(GMT-0500) America/Rankin_Inlet",
),
("America/Resolute", "(GMT-0500) America/Resolute"),
("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"),
("America/Winnipeg", "(GMT-0500) America/Winnipeg"),
("Canada/Central", "(GMT-0500) Canada/Central"),
("Pacific/Easter", "(GMT-0500) Pacific/Easter"),
("US/Central", "(GMT-0500) US/Central"),
("America/Anguilla", "(GMT-0400) America/Anguilla"),
("America/Antigua", "(GMT-0400) America/Antigua"),
("America/Aruba", "(GMT-0400) America/Aruba"),
("America/Asuncion", "(GMT-0400) America/Asuncion"),
("America/Barbados", "(GMT-0400) America/Barbados"),
(
"America/Blanc-Sablon",
"(GMT-0400) America/Blanc-Sablon",
),
("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"),
(
"America/Campo_Grande",
"(GMT-0400) America/Campo_Grande",
),
("America/Caracas", "(GMT-0400) America/Caracas"),
("America/Cuiaba", "(GMT-0400) America/Cuiaba"),
("America/Curacao", "(GMT-0400) America/Curacao"),
("America/Detroit", "(GMT-0400) America/Detroit"),
("America/Dominica", "(GMT-0400) America/Dominica"),
("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"),
("America/Grenada", "(GMT-0400) America/Grenada"),
("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"),
("America/Guyana", "(GMT-0400) America/Guyana"),
("America/Havana", "(GMT-0400) America/Havana"),
(
"America/Indiana/Indianapolis",
"(GMT-0400) America/Indiana/Indianapolis",
),
(
"America/Indiana/Marengo",
"(GMT-0400) America/Indiana/Marengo",
),
(
"America/Indiana/Petersburg",
"(GMT-0400) America/Indiana/Petersburg",
),
(
"America/Indiana/Vevay",
"(GMT-0400) America/Indiana/Vevay",
),
(
"America/Indiana/Vincennes",
"(GMT-0400) America/Indiana/Vincennes",
),
(
"America/Indiana/Winamac",
"(GMT-0400) America/Indiana/Winamac",
),
("America/Iqaluit", "(GMT-0400) America/Iqaluit"),
(
"America/Kentucky/Louisville",
"(GMT-0400) America/Kentucky/Louisville",
),
(
"America/Kentucky/Monticello",
"(GMT-0400) America/Kentucky/Monticello",
),
("America/Kralendijk", "(GMT-0400) America/Kralendijk"),
("America/La_Paz", "(GMT-0400) America/La_Paz"),
(
"America/Lower_Princes",
"(GMT-0400) America/Lower_Princes",
),
("America/Manaus", "(GMT-0400) America/Manaus"),
("America/Marigot", "(GMT-0400) America/Marigot"),
("America/Martinique", "(GMT-0400) America/Martinique"),
("America/Montserrat", "(GMT-0400) America/Montserrat"),
("America/Nassau", "(GMT-0400) America/Nassau"),
("America/New_York", "(GMT-0400) America/New_York"),
(
"America/Port-au-Prince",
"(GMT-0400) America/Port-au-Prince",
),
(
"America/Port_of_Spain",
"(GMT-0400) America/Port_of_Spain",
),
("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"),
("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"),
(
"America/Santo_Domingo",
"(GMT-0400) America/Santo_Domingo",
),
(
"America/St_Barthelemy",
"(GMT-0400) America/St_Barthelemy",
),
("America/St_Kitts", "(GMT-0400) America/St_Kitts"),
("America/St_Lucia", "(GMT-0400) America/St_Lucia"),
("America/St_Thomas", "(GMT-0400) America/St_Thomas"),
("America/St_Vincent", "(GMT-0400) America/St_Vincent"),
("America/Toronto", "(GMT-0400) America/Toronto"),
("America/Tortola", "(GMT-0400) America/Tortola"),
("Canada/Eastern", "(GMT-0400) Canada/Eastern"),
("US/Eastern", "(GMT-0400) US/Eastern"),
("America/Araguaina", "(GMT-0300) America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"(GMT-0300) America/Argentina/Buenos_Aires",
),
(
"America/Argentina/Catamarca",
"(GMT-0300) America/Argentina/Catamarca",
),
(
"America/Argentina/Cordoba",
"(GMT-0300) America/Argentina/Cordoba",
),
(
"America/Argentina/Jujuy",
"(GMT-0300) America/Argentina/Jujuy",
),
(
"America/Argentina/La_Rioja",
"(GMT-0300) America/Argentina/La_Rioja",
),
(
"America/Argentina/Mendoza",
"(GMT-0300) America/Argentina/Mendoza",
),
(
"America/Argentina/Rio_Gallegos",
"(GMT-0300) America/Argentina/Rio_Gallegos",
),
(
"America/Argentina/Salta",
"(GMT-0300) America/Argentina/Salta",
),
(
"America/Argentina/San_Juan",
"(GMT-0300) America/Argentina/San_Juan",
),
(
"America/Argentina/San_Luis",
"(GMT-0300) America/Argentina/San_Luis",
),
(
"America/Argentina/Tucuman",
"(GMT-0300) America/Argentina/Tucuman",
),
(
"America/Argentina/Ushuaia",
"(GMT-0300) America/Argentina/Ushuaia",
),
("America/Bahia", "(GMT-0300) America/Bahia"),
("America/Belem", "(GMT-0300) America/Belem"),
("America/Cayenne", "(GMT-0300) America/Cayenne"),
("America/Fortaleza", "(GMT-0300) America/Fortaleza"),
("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"),
("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"),
("America/Halifax", "(GMT-0300) America/Halifax"),
("America/Maceio", "(GMT-0300) America/Maceio"),
("America/Moncton", "(GMT-0300) America/Moncton"),
("America/Montevideo", "(GMT-0300) America/Montevideo"),
("America/Paramaribo", "(GMT-0300) America/Paramaribo"),
(
"America/Punta_Arenas",
"(GMT-0300) America/Punta_Arenas",
),
("America/Recife", "(GMT-0300) America/Recife"),
("America/Santarem", "(GMT-0300) America/Santarem"),
("America/Santiago", "(GMT-0300) America/Santiago"),
("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"),
("America/Thule", "(GMT-0300) America/Thule"),
("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"),
("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"),
("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"),
("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"),
("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"),
("America/St_Johns", "(GMT-0230) America/St_Johns"),
("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"),
("America/Miquelon", "(GMT-0200) America/Miquelon"),
("America/Noronha", "(GMT-0200) America/Noronha"),
("America/Nuuk", "(GMT-0200) America/Nuuk"),
(
"Atlantic/South_Georgia",
"(GMT-0200) Atlantic/South_Georgia",
),
("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"),
("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"),
("Africa/Accra", "(GMT+0000) Africa/Accra"),
("Africa/Bamako", "(GMT+0000) Africa/Bamako"),
("Africa/Banjul", "(GMT+0000) Africa/Banjul"),
("Africa/Bissau", "(GMT+0000) Africa/Bissau"),
("Africa/Conakry", "(GMT+0000) Africa/Conakry"),
("Africa/Dakar", "(GMT+0000) Africa/Dakar"),
("Africa/Freetown", "(GMT+0000) Africa/Freetown"),
("Africa/Lome", "(GMT+0000) Africa/Lome"),
("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"),
("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"),
("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"),
("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"),
(
"America/Danmarkshavn",
"(GMT+0000) America/Danmarkshavn",
),
(
"America/Scoresbysund",
"(GMT+0000) America/Scoresbysund",
),
("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"),
("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"),
("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"),
("GMT", "(GMT+0000) GMT"),
("UTC", "(GMT+0000) UTC"),
("Africa/Algiers", "(GMT+0100) Africa/Algiers"),
("Africa/Bangui", "(GMT+0100) Africa/Bangui"),
("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"),
("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"),
("Africa/Douala", "(GMT+0100) Africa/Douala"),
("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"),
("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"),
("Africa/Lagos", "(GMT+0100) Africa/Lagos"),
("Africa/Libreville", "(GMT+0100) Africa/Libreville"),
("Africa/Luanda", "(GMT+0100) Africa/Luanda"),
("Africa/Malabo", "(GMT+0100) Africa/Malabo"),
("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"),
("Africa/Niamey", "(GMT+0100) Africa/Niamey"),
("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"),
("Africa/Tunis", "(GMT+0100) Africa/Tunis"),
("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"),
("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"),
("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"),
("Europe/Dublin", "(GMT+0100) Europe/Dublin"),
("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"),
("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"),
("Europe/Jersey", "(GMT+0100) Europe/Jersey"),
("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"),
("Europe/London", "(GMT+0100) Europe/London"),
("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"),
("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"),
("Africa/Cairo", "(GMT+0200) Africa/Cairo"),
("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"),
("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"),
("Africa/Harare", "(GMT+0200) Africa/Harare"),
("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"),
("Africa/Juba", "(GMT+0200) Africa/Juba"),
("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"),
("Africa/Kigali", "(GMT+0200) Africa/Kigali"),
("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"),
("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"),
("Africa/Maputo", "(GMT+0200) Africa/Maputo"),
("Africa/Maseru", "(GMT+0200) Africa/Maseru"),
("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"),
("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"),
("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"),
("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"),
("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"),
("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"),
("Europe/Andorra", "(GMT+0200) Europe/Andorra"),
("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"),
("Europe/Berlin", "(GMT+0200) Europe/Berlin"),
("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"),
("Europe/Brussels", "(GMT+0200) Europe/Brussels"),
("Europe/Budapest", "(GMT+0200) Europe/Budapest"),
("Europe/Busingen", "(GMT+0200) Europe/Busingen"),
("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"),
("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"),
("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"),
("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"),
("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"),
("Europe/Madrid", "(GMT+0200) Europe/Madrid"),
("Europe/Malta", "(GMT+0200) Europe/Malta"),
("Europe/Monaco", "(GMT+0200) Europe/Monaco"),
("Europe/Oslo", "(GMT+0200) Europe/Oslo"),
("Europe/Paris", "(GMT+0200) Europe/Paris"),
("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"),
("Europe/Prague", "(GMT+0200) Europe/Prague"),
("Europe/Rome", "(GMT+0200) Europe/Rome"),
("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"),
("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"),
("Europe/Skopje", "(GMT+0200) Europe/Skopje"),
("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"),
("Europe/Tirane", "(GMT+0200) Europe/Tirane"),
("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"),
("Europe/Vatican", "(GMT+0200) Europe/Vatican"),
("Europe/Vienna", "(GMT+0200) Europe/Vienna"),
("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"),
("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"),
("Europe/Zurich", "(GMT+0200) Europe/Zurich"),
("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"),
("Africa/Asmara", "(GMT+0300) Africa/Asmara"),
(
"Africa/Dar_es_Salaam",
"(GMT+0300) Africa/Dar_es_Salaam",
),
("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"),
("Africa/Kampala", "(GMT+0300) Africa/Kampala"),
("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"),
("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"),
("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"),
("Asia/Aden", "(GMT+0300) Asia/Aden"),
("Asia/Amman", "(GMT+0300) Asia/Amman"),
("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"),
("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"),
("Asia/Beirut", "(GMT+0300) Asia/Beirut"),
("Asia/Damascus", "(GMT+0300) Asia/Damascus"),
("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"),
("Asia/Gaza", "(GMT+0300) Asia/Gaza"),
("Asia/Hebron", "(GMT+0300) Asia/Hebron"),
("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"),
("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"),
("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"),
("Asia/Qatar", "(GMT+0300) Asia/Qatar"),
("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"),
("Europe/Athens", "(GMT+0300) Europe/Athens"),
("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"),
("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"),
("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"),
("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"),
("Europe/Kirov", "(GMT+0300) Europe/Kirov"),
("Europe/Kyiv", "(GMT+0300) Europe/Kyiv"),
("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"),
("Europe/Minsk", "(GMT+0300) Europe/Minsk"),
("Europe/Moscow", "(GMT+0300) Europe/Moscow"),
("Europe/Riga", "(GMT+0300) Europe/Riga"),
("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"),
("Europe/Sofia", "(GMT+0300) Europe/Sofia"),
("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"),
("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"),
("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"),
("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"),
("Indian/Comoro", "(GMT+0300) Indian/Comoro"),
("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"),
("Asia/Tehran", "(GMT+0330) Asia/Tehran"),
("Asia/Baku", "(GMT+0400) Asia/Baku"),
("Asia/Dubai", "(GMT+0400) Asia/Dubai"),
("Asia/Muscat", "(GMT+0400) Asia/Muscat"),
("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"),
("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"),
("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"),
("Europe/Samara", "(GMT+0400) Europe/Samara"),
("Europe/Saratov", "(GMT+0400) Europe/Saratov"),
("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"),
("Indian/Mahe", "(GMT+0400) Indian/Mahe"),
("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"),
("Indian/Reunion", "(GMT+0400) Indian/Reunion"),
("Asia/Kabul", "(GMT+0430) Asia/Kabul"),
("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"),
("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"),
("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"),
("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"),
("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"),
("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"),
("Asia/Karachi", "(GMT+0500) Asia/Karachi"),
("Asia/Oral", "(GMT+0500) Asia/Oral"),
("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"),
("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"),
("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"),
("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"),
("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"),
("Indian/Maldives", "(GMT+0500) Indian/Maldives"),
("Asia/Colombo", "(GMT+0530) Asia/Colombo"),
("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"),
("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"),
("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"),
("Asia/Almaty", "(GMT+0600) Asia/Almaty"),
("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"),
("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"),
("Asia/Omsk", "(GMT+0600) Asia/Omsk"),
("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"),
("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"),
("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"),
("Indian/Chagos", "(GMT+0600) Indian/Chagos"),
("Asia/Yangon", "(GMT+0630) Asia/Yangon"),
("Indian/Cocos", "(GMT+0630) Indian/Cocos"),
("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"),
("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"),
("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"),
("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"),
("Asia/Hovd", "(GMT+0700) Asia/Hovd"),
("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"),
("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"),
("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"),
("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"),
("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"),
("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"),
("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"),
("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"),
("Indian/Christmas", "(GMT+0700) Indian/Christmas"),
("Asia/Brunei", "(GMT+0800) Asia/Brunei"),
("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"),
("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"),
("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"),
("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"),
("Asia/Kuching", "(GMT+0800) Asia/Kuching"),
("Asia/Macau", "(GMT+0800) Asia/Macau"),
("Asia/Makassar", "(GMT+0800) Asia/Makassar"),
("Asia/Manila", "(GMT+0800) Asia/Manila"),
("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"),
("Asia/Singapore", "(GMT+0800) Asia/Singapore"),
("Asia/Taipei", "(GMT+0800) Asia/Taipei"),
("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"),
("Australia/Perth", "(GMT+0800) Australia/Perth"),
("Australia/Eucla", "(GMT+0845) Australia/Eucla"),
("Asia/Chita", "(GMT+0900) Asia/Chita"),
("Asia/Dili", "(GMT+0900) Asia/Dili"),
("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"),
("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"),
("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"),
("Asia/Seoul", "(GMT+0900) Asia/Seoul"),
("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"),
("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"),
("Pacific/Palau", "(GMT+0900) Pacific/Palau"),
("Australia/Adelaide", "(GMT+0930) Australia/Adelaide"),
(
"Australia/Broken_Hill",
"(GMT+0930) Australia/Broken_Hill",
),
("Australia/Darwin", "(GMT+0930) Australia/Darwin"),
(
"Antarctica/DumontDUrville",
"(GMT+1000) Antarctica/DumontDUrville",
),
(
"Antarctica/Macquarie",
"(GMT+1000) Antarctica/Macquarie",
),
("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"),
("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"),
("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"),
("Australia/Hobart", "(GMT+1000) Australia/Hobart"),
("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"),
("Australia/Melbourne", "(GMT+1000) Australia/Melbourne"),
("Australia/Sydney", "(GMT+1000) Australia/Sydney"),
("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"),
("Pacific/Guam", "(GMT+1000) Pacific/Guam"),
(
"Pacific/Port_Moresby",
"(GMT+1000) Pacific/Port_Moresby",
),
("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"),
("Australia/Lord_Howe", "(GMT+1030) Australia/Lord_Howe"),
("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"),
("Asia/Magadan", "(GMT+1100) Asia/Magadan"),
("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"),
("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"),
(
"Pacific/Bougainville",
"(GMT+1100) Pacific/Bougainville",
),
("Pacific/Efate", "(GMT+1100) Pacific/Efate"),
("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"),
("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"),
("Pacific/Norfolk", "(GMT+1100) Pacific/Norfolk"),
("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"),
("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"),
("Antarctica/McMurdo", "(GMT+1200) Antarctica/McMurdo"),
("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"),
("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"),
("Pacific/Auckland", "(GMT+1200) Pacific/Auckland"),
("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"),
("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"),
("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"),
("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"),
("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"),
("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"),
("Pacific/Wake", "(GMT+1200) Pacific/Wake"),
("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"),
("Pacific/Chatham", "(GMT+1245) Pacific/Chatham"),
("Pacific/Apia", "(GMT+1300) Pacific/Apia"),
("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"),
("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"),
("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"),
("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"),
],
default="UTC",
max_length=255,
),
),
]

View File

@ -23,6 +23,7 @@ class ScrobbleInline(admin.TabularInline):
"sport_event",
"board_game",
"geo_location",
"trail",
"web_page",
"life_event",
"user",
@ -95,7 +96,7 @@ class ChartRecordAdmin(admin.ModelAdmin):
@admin.register(Scrobble)
class ScrobbleAdmin(admin.ModelAdmin):
date_hierarchy = "timestamp"
# date_hierarchy = "timestamp"
list_display = (
"timestamp",
"media_name",

View File

@ -30,9 +30,34 @@ class JSONDataclass(JSONWizard):
def json(self):
return json.dumps(self.asdict)
class LongPlayLogData(JSONDataclass):
serial_scrobble_id: Optional[int]
long_play_complete: Optional[bool]
long_play_complete: bool = False
class WithOthersLogData(JSONDataclass):
with_user_ids: Optional[list[int]] = None
with_names_str: Optional[list[str]] = None
@property
def with_names(self) -> list[str]:
with_names = []
if self.with_user_ids:
with_names += [u.full_name for u in self.with_users if u]
if self.with_names_str:
with_names += [u for u in self.with_names_str]
return with_names
@property
def with_users(self) -> list[User]:
with_users = []
if self.with_user_ids:
with_users = [
User.objects.filter(id=i).first() for i in self.with_user_ids
]
return with_users
@dataclass
class BoardGameScoreLogData(JSONDataclass):
@ -73,6 +98,8 @@ class BoardGameScoreLogData(JSONDataclass):
@dataclass
class BoardGameLogData(LongPlayLogData):
serial_scrobble_id: Optional[int]
long_play_complete: bool = False
players: Optional[list[BoardGameScoreLogData]] = None
location: Optional[str] = None
geo_location_id: Optional[int] = None
@ -96,45 +123,30 @@ class BookPageLogData(JSONDataclass):
@dataclass
class BookLogData(LongPlayLogData):
koreader_hash: Optional[str]
pages_read: Optional[int]
page_data: Optional[list[BookPageLogData]]
page_end: Optional[int]
page_start: Optional[int]
serial_scrobble_id: Optional[int]
long_play_complete: Optional[bool] = None
koreader_hash: Optional[str] = None
page_data: Optional[dict[int, BookPageLogData]] = None
pages_read: Optional[int] = None
page_start: Optional[int] = None
page_end: Optional[int] = None
serial_scrobble_id: Optional[int] = None
@dataclass
class LifeEventLogData(JSONDataclass):
participant_user_ids: Optional[list[int]] = None
participant_names: Optional[list[str]] = None
class LifeEventLogData(WithOthersLogData):
with_user_ids: Optional[list[int]] = None
with_names_str: Optional[list[str]] = None
location: Optional[str] = None
geo_location_id: Optional[int] = None
details: Optional[str] = None
def participants(self) -> list[str]:
participants = []
if self.participant_user_ids:
participants += [u.full_name for u in self.participant_users]
if self.participant_names:
participants += self.participant_names
return participants
@property
def participant_users(self) -> list[User]:
participants = []
if self.participant_user_ids:
for id in self.participant_user_ids:
participants.append(User.objects.filter(id=id).first())
return participants
def geo_location(self):
return GeoLocation.objects.filter(id=self.geo_location_id).first()
@dataclass
class MoodLogData(JSONDataclass):
reasons: Optional[str]
reasons: Optional[str] = None
@dataclass
@ -144,23 +156,36 @@ class VideoLogData(JSONDataclass):
run_time_seconds: int
kind: str
year: Optional[int]
episode_number: Optional[int]
source_url: Optional[str]
imdbID: Optional[str]
season_number: Optional[int]
cover_url = Optional[str]
next_imdb_id: Optional[int]
tv_series_id: Optional[str]
episode_number: Optional[int] = None
source_url: Optional[str] = None
imdbID: Optional[str] = None
season_number: Optional[int] = None
cover_url: Optional[str] = None
next_imdb_id: Optional[int] = None
tv_series_id: Optional[str] = None
@dataclass
class VideoGameLogData(LongPlayLogData):
serial_scrobble_id: Optional[int]
long_play_complete: bool = False
console: str = ""
emulated: bool = False
console: Optional[str] = None
emulator: Optional[str] = None
@dataclass
class BrickSetLogData(LongPlayLogData):
built_with_names: Optional[list[str]]
built_with_user_ids: Optional[list[str]]
class BrickSetLogData(LongPlayLogData, WithOthersLogData):
serial_scrobble_id: Optional[int]
long_play_complete: bool = False
with_user_ids: Optional[list[int]] = None
with_names_str: Optional[list[str]] = None
@dataclass
class TrailLogData(WithOthersLogData):
with_user_ids: Optional[list[int]] = None
with_names_str: Optional[list[str]] = None
details: Optional[str] = None
effort: Optional[str] = None
difficulty: Optional[str] = None

View File

@ -0,0 +1,48 @@
# Generated by Django 4.2.16 on 2024-09-09 16:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("trails", "0001_initial"),
("scrobbles", "0061_alter_scrobble_media_type"),
]
operations = [
migrations.AddField(
model_name="scrobble",
name="trail",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to="trails.trail",
),
),
migrations.AlterField(
model_name="scrobble",
name="media_type",
field=models.CharField(
choices=[
("Video", "Video"),
("Track", "Track"),
("PodcastEpisode", "Podcast episode"),
("SportEvent", "Sport event"),
("Book", "Book"),
("VideoGame", "Video game"),
("BoardGame", "Board game"),
("GeoLocation", "GeoLocation"),
("Trail", "Trail"),
("WebPage", "Web Page"),
("LifeEvent", "Life event"),
("Mood", "Mood"),
("BrickSet", "Brick set"),
],
default="Video",
max_length=14,
),
),
]

View File

@ -41,6 +41,7 @@ from sports.models import SportEvent
from videogames import retroarch
from videogames.models import VideoGame
from videos.models import Series, Video
from trails.models import Trail
from webpages.models import WebPage
from vrobbler.apps.scrobbles.constants import MEDIA_END_PADDING_SECONDS
@ -483,6 +484,7 @@ class Scrobble(TimeStampedModel):
VIDEO_GAME = "VideoGame", "Video game"
BOARD_GAME = "BoardGame", "Board game"
GEO_LOCATION = "GeoLocation", "GeoLocation"
TRAIL = "Trail", "Trail"
WEBPAGE = "WebPage", "Web Page"
LIFE_EVENT = "LifeEvent", "Life event"
MOOD = "Mood", "Mood"
@ -507,12 +509,15 @@ class Scrobble(TimeStampedModel):
geo_location = models.ForeignKey(
GeoLocation, on_delete=models.DO_NOTHING, **BNULL
)
trail = models.ForeignKey(Trail, on_delete=models.DO_NOTHING, **BNULL)
web_page = models.ForeignKey(WebPage, on_delete=models.DO_NOTHING, **BNULL)
life_event = models.ForeignKey(
LifeEvent, on_delete=models.DO_NOTHING, **BNULL
)
mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
brickset = models.ForeignKey(BrickSet, on_delete=models.DO_NOTHING, **BNULL)
brickset = models.ForeignKey(
BrickSet, on_delete=models.DO_NOTHING, **BNULL
)
media_type = models.CharField(
max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
)
@ -894,15 +899,15 @@ class Scrobble(TimeStampedModel):
def calc_reading_duration(self) -> int:
duration = 0
if self.book_page_data:
for k, v in self.book_page_data.items():
if self.logdata.page_data:
for k, v in self.logdata.page_data.items():
duration += v.get("duration")
return duration
def calc_pages_read(self) -> int:
pages_read = 0
if self.book_page_data:
pages = [int(k) for k in self.book_page_data.keys()]
if self.logdata.page_data:
pages = [int(k) for k in self.logdata.page_data.keys()]
pages.sort()
if len(pages) == 1:
pages_read = 1
@ -915,8 +920,8 @@ class Scrobble(TimeStampedModel):
@property
def last_page_read(self) -> int:
last_page = 0
if self.book_page_data:
pages = [int(k) for k in self.book_page_data.keys()]
if self.logdata.page_data:
pages = [int(k) for k in self.logdata.page_data.keys()]
pages.sort()
last_page = pages[-1]
return last_page

View File

View File

@ -0,0 +1,29 @@
from django.contrib import admin
from trails.models import Trail
from scrobbles.admin import ScrobbleInline
class TrailInline(admin.TabularInline):
model = Trail
extra = 0
@admin.register(Trail)
class TrailAdmin(admin.ModelAdmin):
date_hierarchy = "created"
list_display = (
"uuid",
"title",
"trailhead_location",
)
raw_id_fields = (
"trailhead_location",
"trail_terminus_location",
)
ordering = ("-created",)
search_fields = ("title",)
inlines = [
ScrobbleInline,
]

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class TrailsConfig(AppConfig):
name = "trails"

View File

@ -0,0 +1,109 @@
# Generated by Django 4.2.16 on 2024-09-09 16:11
from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
import taggit.managers
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
("scrobbles", "0061_alter_scrobble_media_type"),
("locations", "0006_delete_rawgeolocation"),
]
operations = [
migrations.CreateModel(
name="Trail",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created",
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name="created"
),
),
(
"modified",
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
blank=True,
default=uuid.uuid4,
editable=False,
null=True,
),
),
(
"title",
models.CharField(blank=True, max_length=255, null=True),
),
(
"run_time_seconds",
models.IntegerField(blank=True, null=True),
),
(
"run_time_ticks",
models.PositiveBigIntegerField(blank=True, null=True),
),
("description", models.TextField(blank=True, null=True)),
(
"strava_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"trailforks_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"genre",
taggit.managers.TaggableManager(
blank=True,
help_text="A comma-separated list of tags.",
through="scrobbles.ObjectWithGenres",
to="scrobbles.Genre",
verbose_name="Tags",
),
),
(
"trail_terminus_location",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="trail_termini",
to="locations.geolocation",
),
),
(
"trailhead_location",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="trailheads",
to="locations.geolocation",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -0,0 +1,49 @@
from enum import Enum
from django.apps import apps
from django.db import models
from django.urls import reverse
from scrobbles.dataclasses import TrailLogData
from scrobbles.mixins import ScrobblableMixin
from locations.models import GeoLocation
BNULL = {"blank": True, "null": True}
class TrailType(Enum):
WOODS = "Woods"
ROAD = "Road"
class Trail(ScrobblableMixin):
description = models.TextField(**BNULL)
trailhead_location = models.ForeignKey(
GeoLocation,
related_name="trailheads",
on_delete=models.DO_NOTHING,
**BNULL,
)
trail_terminus_location = models.ForeignKey(
GeoLocation,
related_name="trail_termini",
on_delete=models.DO_NOTHING,
**BNULL,
)
strava_id = models.CharField(max_length=255, **BNULL)
trailforks_id = models.CharField(max_length=255, **BNULL)
def get_absolute_url(self):
return reverse("trails:trail_detail", kwargs={"slug": self.uuid})
@property
def logdata_cls(self):
return TrailLogData
@classmethod
def find_or_create(cls, title: str) -> "Trail":
return cls.objects.filter(title=title).first()
def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(
user_id=user_id, life_event=self
).order_by("-timestamp")

View File

@ -0,0 +1,14 @@
from django.urls import path
from trails import views
app_name = "trials"
urlpatterns = [
path("trails/", views.TrailListView.as_view(), name="trail-list"),
path(
"trails/<slug:slug>/",
views.TrailDetailView.as_view(),
name="trail-detail",
),
]

View File

@ -0,0 +1,11 @@
from trails.models import Trail
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
class TrailListView(ScrobbleableListView):
model = Trail
class TrailDetailView(ScrobbleableDetailView):
model = Trail

View File

@ -0,0 +1,27 @@
# Generated by Django 4.2.15 on 2024-09-07 05:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("videos", "0016_video_tmdb_id"),
]
operations = [
migrations.AlterField(
model_name="video",
name="video_type",
field=models.CharField(
choices=[
("U", "Unknown"),
("E", "TV Episode"),
("M", "Movie"),
("S", "Skate Video"),
],
default="U",
max_length=1,
),
),
]

View File

@ -45,8 +45,6 @@ def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False):
series_name = video_dict.pop("series_name", None)
if series_name:
series_name = video_dict.pop("series_name")
series, series_created = Series.objects.get_or_create(
name=series_name
)

View File

@ -5,9 +5,9 @@ app_name = "webpages"
urlpatterns = [
path("webpage/", views.WebPageListView.as_view(), name="webpage-list"),
path("webpages/", views.WebPageListView.as_view(), name="webpage-list"),
path(
"webpage/<slug:slug>/",
"webpages/<slug:slug>/",
views.WebPageDetailView.as_view(),
name="webpage-detail",
),

View File

@ -1,5 +1,4 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse

View File

@ -111,6 +111,7 @@ INSTALLED_APPS = [
"videogames",
"locations",
"webpages",
"trails",
"lifeevents",
"moods",
"mathfilters",

View File

@ -125,6 +125,7 @@ INSTALLED_APPS = [
"videogames",
"locations",
"webpages",
"trails",
"lifeevents",
"moods",
"mathfilters",

View File

@ -1,82 +1,43 @@
{% extends "base_list.html" %}
{% load urlreplace %}
{% block title %}Albums{% endblock %}
{% block title %}Board Games{% endblock %}
{% block head_extra %}
<style>
dl { width: 210px; float:left; margin-right: 10px; }
dt a { color:white; text-decoration: none; font-size:smaller; }
img { height:200px; width: 200px; object-fit: cover; }
dd .right { float:right; }
</style>
{% endblock %}
{% block lists %}
<div class="row">
<p class="view">
<span class="view-links">
{% if view == 'grid' %}
View as <a href="?{% urlreplace view='list' %}">List</a>
{% else %}
View as <a href="?{% urlreplace view='grid' %}">Grid</a>
{% endif %}
</span>
</p>
<p class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</p>
<hr />
{% if view == 'grid' %}
<div>
{% for game in object_list %}
{% if game.cover %}
<dl style="width: 130px; float: left; margin-right:10px;">
<dd><img src="{{game.cover.url}}" width=120 height=120 /></dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="col-md">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Scrobbles</th>
<th scope="col">Name</th>
<th scope="col">Publisher</th>
<th scope="col">Start</th>
</tr>
</thead>
<tbody>
{% for game in object_list %}
{% for obj in object_list %}
<tr>
<td>{{game.scrobbles.count}}</td>
<td><a href="{{game.get_absolute_url}}">{{game}}</a></td>
<td><a href="{{game.publisher.get_absolute_url}}">{{game.publisher}}</a></td>
<td><a href="{{obj.get_absolute_url}}">{{obj}}</a></td>
{% if request.user.is_authenticated %}
<td>{{obj.scrobble_count}}</td>
<td><a type="button" class="btn btn-sm btn-primary" href="{{obj.get_start_url}}">Scrobble</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="pagination" style="margin-bottom:50px;">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="?{% urlreplace page=page_obj.previous_page_number %}">prev</a>
{% endif %}
<span class="page-current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?{% urlreplace page=page_obj.next_page_number %}">next</a>
{% endif %}
</span>
</div>
</div>
{% endblock %}

View File

@ -13,6 +13,7 @@ from vrobbler.apps.videogames import urls as videogame_urls
from vrobbler.apps.boardgames import urls as boardgame_urls
from vrobbler.apps.locations import urls as locations_urls
from vrobbler.apps.lifeevents import urls as lifeevents_urls
from vrobbler.apps.trails import urls as trails_urls
from vrobbler.apps.webpages import urls as webpages_urls
from vrobbler.apps.moods import urls as moods_urls
from vrobbler.apps.music.api.views import (
@ -73,6 +74,7 @@ urlpatterns = [
path("", include(bricksets_urls, namespace="bricksets")),
path("", include(sports_urls, namespace="sports")),
path("", include(locations_urls, namespace="locations")),
path("", include(trails_urls, namespace="trails")),
path("", include(webpages_urls, namespace="webpages")),
path("", include(podcast_urls, namespace="podcasts")),
path("", include(lifeevents_urls, namespace="life-events")),