Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df3dd728d | |||
| 1fe8d8aa51 | |||
| 8f3c7beffa | |||
| aac0efbb14 | |||
| b0d4dd0899 | |||
| 16db67ea84 | |||
| 7a747268a1 | |||
| 2037ffc67a | |||
| 2136c1562a | |||
| eae169aff7 | |||
| 6c0bd2e409 | |||
| d69d7311d5 | |||
| c484dab210 | |||
| 3d62a6a227 | |||
| 1a5d4a6717 | |||
| 8f0491a90b | |||
| 7e961076b4 | |||
| 14062f3b60 | |||
| 0a8acdf33f | |||
| b5d194e74f | |||
| 3a50a8b015 | |||
| 921cf9d8b3 | |||
| ba0be65ed0 | |||
| caad6329c9 | |||
| cfd6ac861e | |||
| ca36e25948 | |||
| c84acf6ae7 | |||
| 610464e732 | |||
| 047dd22069 | |||
| 2c73121367 | |||
| 9affd6e03a | |||
| b414bbf59c | |||
| 5e22cb3106 | |||
| cc9a2a64df | |||
| c2b334b926 | |||
| ad6f0e1b62 | |||
| 1df8157d8d | |||
| 7d33e6afdb | |||
| 40d112c58c | |||
| cdfd8af078 | |||
| 0ce19527a2 | |||
| f3fc58e2c0 | |||
| b02b75fa90 | |||
| b626ac583a | |||
| adc10ab43b | |||
| 5555b1b89e | |||
| 7fd90f8cf0 | |||
| f80daba67b | |||
| 8bd4fd1d4b | |||
| f30e416e8d | |||
| f29a039562 | |||
| 882367cee6 | |||
| 74c32bc4d5 | |||
| dcd7196010 | |||
| 2e57b2ce07 | |||
| 9d664ba476 | |||
| 0fa89af1d9 | |||
| 2c5516ae4e | |||
| 0cba46b103 | |||
| 5014c4428b | |||
| 1d554429f1 | |||
| daea268465 | |||
| 1cceb2bd63 | |||
| e15d253d58 | |||
| 8ed0bd3d21 | |||
| 113f200eb7 | |||
| 1b5ffd2a3c | |||
| f168608cee | |||
| c28f93a1bb | |||
| 99ad2f797f | |||
| 1f28179ed1 | |||
| 1648f988ff | |||
| dc3a77f14f | |||
| 5994dccf23 | |||
| bfadc0d87b | |||
| 78782cc538 | |||
| 06277f21de | |||
| 8fbe37a163 | |||
| 20dd4d217a | |||
| 53657a9454 | |||
| 42066cebb2 | |||
| 6d60a729b7 | |||
| 89541a13f2 | |||
| f87bc5fd55 | |||
| 0a8078dee0 | |||
| 37da74708c | |||
| b470e7acea | |||
| 5cdd12783f | |||
| 8bf43f298c | |||
| a269949a23 | |||
| 8a5f200b44 | |||
| 1c41ca3e18 | |||
| b69963b75f | |||
| 1327d5da40 | |||
| 5d5b49f19b | |||
| d93c827b80 | |||
| 2349c39487 | |||
| ae96831fe4 | |||
| 706be782dc | |||
| a72e0b0fb9 | |||
| f5df6c97a9 | |||
| e00f2de4b1 | |||
| b4a7cafa3d | |||
| aebe4f899d | |||
| 2bf0ca1a8c | |||
| a0c414135c | |||
| 1dcd151c65 | |||
| 2e9d92d9c7 | |||
| 8776f75d12 | |||
| c2a53875a0 | |||
| ffb0b7372d | |||
| d03744c240 | |||
| d9a7929cb0 | |||
| 9bc087d824 | |||
| e65ab4d8c2 | |||
| 2d26a7c3bd | |||
| 1fd3e47656 | |||
| dd0ce967c8 | |||
| 1f6a4956d2 | |||
| 22f5d288d4 | |||
| 77cf67ea09 | |||
| d1dfc5502d | |||
| 0d83050956 | |||
| 134bd3f650 | |||
| 261f2eb9ce | |||
| 8f56bfba50 | |||
| 750fa05b15 | |||
| 302ddf6650 | |||
| 49dd148f9f | |||
| 79d4e79f3e | |||
| 874d9dc7d2 | |||
| 0cd95036da | |||
| 829c6b9978 | |||
| df1bfd4177 | |||
| 0fff2ec388 | |||
| 8a83c9fdb3 | |||
| 651fc7a745 | |||
| c22a306840 | |||
| 11e6161502 | |||
| 3af2ad203c | |||
| 7697dd006d | |||
| 73a6277e29 | |||
| 79fc407c33 | |||
| 84ec84c018 | |||
| beb9569663 | |||
| efcf01b49f | |||
| 01270a9e32 | |||
| c9280c2bad | |||
| 416fd807f4 | |||
| 3069e93697 | |||
| e47f4905e8 | |||
| bf59fb213e | |||
| 589bf533ea | |||
| 1fb29304a3 | |||
| 42604b9e23 | |||
| 4642063510 | |||
| 65944eb911 | |||
| 4cf2ceb2dd | |||
| 78151e5070 | |||
| f90fa160d7 | |||
| 3b7498c419 | |||
| a59997dafb | |||
| 4cdb6150dc | |||
| 7fa21c27ea | |||
| b981c090c6 | |||
| 605435b9ea | |||
| f4d00b4a22 | |||
| 504620fc89 | |||
| df3424c68f | |||
| 71b5af7615 | |||
| 7035f01441 | |||
| 525c7c4e2b | |||
| 4e330a6f03 | |||
| ff18ad8efc | |||
| 15c1317395 | |||
| 6b80d107b0 | |||
| 13124aca6b | |||
| a685c6ff9c | |||
| 02200bb1f7 | |||
| 0bbd488f0d | |||
| 2c08336e33 | |||
| a637a59a40 | |||
| 123f7c53f9 | |||
| 2c26cc01ba | |||
| 36bc1c2e95 | |||
| 5651ccb990 | |||
| cd7b06eaa2 | |||
| 949416059d | |||
| ca434cb08a | |||
| 2661aee915 | |||
| 74f672a2fd | |||
| 449b74ae3f | |||
| 3d1ad8da3a | |||
| 75e374bb69 | |||
| 2ba694655b | |||
| 879942d070 | |||
| bfd210d280 | |||
| e5c6b5e8d9 | |||
| 3cd1603b91 | |||
| 27523bc7ff | |||
| 67ef1a15ec | |||
| 288027bfce | |||
| bfdf73d4c0 | |||
| 033e3c3b35 | |||
| 40504f83e1 | |||
| ccca81bbab | |||
| 84f49af163 | |||
| e044c70072 | |||
| d3e09c25bd | |||
| 30f78a9290 | |||
| a5553966de | |||
| 9e2d7a6bc0 | |||
| 70c7eda415 | |||
| 0ad7dac6cb | |||
| 209875f0e6 | |||
| 919fa1b0b4 | |||
| 0b3bc53704 | |||
| dfc1365fa3 | |||
| f22ef1a163 | |||
| 7cb818b585 | |||
| 1a5cd5106f | |||
| 3b65144b68 | |||
| 4ae13b3a1a | |||
| 3de2be50cf | |||
| 80d197bc54 | |||
| a274796405 | |||
| 37fd1d8458 | |||
| 45ddce36aa | |||
| c3ddd01a6f | |||
| 6920c99931 | |||
| d2d71b3c85 | |||
| 6300df8e9e | |||
| b427731bc3 | |||
| 6058024434 |
10
.drone.yml
10
.drone.yml
@ -14,7 +14,7 @@ steps:
|
||||
# Install dependencies
|
||||
- cp vrobbler.conf.test vrobbler.conf
|
||||
- pip install poetry
|
||||
- poetry install
|
||||
- poetry install --with dev
|
||||
# Start with a fresh database (which is already running as a service from Drone)
|
||||
- poetry run pytest --cov-report term:skip-covered --cov=vrobbler tests
|
||||
environment:
|
||||
@ -30,7 +30,7 @@ steps:
|
||||
- vrobbler.service
|
||||
username: root
|
||||
ssh_key:
|
||||
from_secret: ssh_key
|
||||
from_secret: jail_key
|
||||
command_timeout: 2m
|
||||
script:
|
||||
- pip uninstall -y vrobbler
|
||||
@ -42,7 +42,7 @@ steps:
|
||||
branch:
|
||||
- main
|
||||
- name: build success notification
|
||||
image: parrazam/drone-ntfy
|
||||
image: parrazam/drone-ntfy:0.3-linux-amd64
|
||||
when:
|
||||
status: [success]
|
||||
settings:
|
||||
@ -50,11 +50,10 @@ steps:
|
||||
topic: drone
|
||||
priority: low
|
||||
tags:
|
||||
- cd
|
||||
- failure
|
||||
- vrobbler
|
||||
- name: build failure notification
|
||||
image: parrazam/drone-ntfy
|
||||
image: parrazam/drone-ntfy:0.3-linux-amd64
|
||||
when:
|
||||
status: [failure]
|
||||
settings:
|
||||
@ -62,7 +61,6 @@ steps:
|
||||
topic: drone
|
||||
priority: high
|
||||
tags:
|
||||
- cd
|
||||
- success
|
||||
- vrobbler
|
||||
volumes:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
db.sqlite3
|
||||
db.sqlite3*
|
||||
vrobbler.conf
|
||||
media/
|
||||
dist/
|
||||
|
||||
3
data/moods.json
Normal file
3
data/moods.json
Normal file
File diff suppressed because one or more lines are too long
3865
poetry.lock
generated
3865
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "0.11.12"
|
||||
version = "0.15.4"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
@ -38,14 +38,20 @@ 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"
|
||||
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.dev-dependencies]
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
Werkzeug = "2.0.3"
|
||||
black = "^22.3"
|
||||
coverage = "^7.0.5"
|
||||
@ -57,7 +63,6 @@ pytest-django = "^4.5.2"
|
||||
pytest-flake8 = "^1.1"
|
||||
pytest-isort = "^3.0"
|
||||
pytest-runner = "^6.0"
|
||||
pytest-selenium = "^2.0.1"
|
||||
time-machine = "^2.9.0"
|
||||
types-pytz = "^2022.1"
|
||||
types-requests = "^2.27"
|
||||
@ -65,7 +70,7 @@ bandit = "^1.7.4"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = "-ra -q"
|
||||
addopts = "-ra -q --reuse-db"
|
||||
testpaths = ["tests"]
|
||||
DJANGO_SETTINGS_MODULE='vrobbler.settings-testing'
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from vrobbler.apps.podcasts.scrapers import scrape_data_from_google_podcasts
|
||||
|
||||
expected_desc_snippet = (
|
||||
@ -8,6 +9,7 @@ expected_img_url = "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcR1F0Cf
|
||||
expected_google_url = "https://podcasts.google.com/feed/aHR0cHM6Ly9mZWVkcy5ucHIub3JnLzUxMDMxOC9wb2RjYXN0LnhtbA"
|
||||
|
||||
|
||||
@pytest.mark.skip("Google Podcasts is gone")
|
||||
def test_get_not_allowed_from_mopidy():
|
||||
query = "Up First"
|
||||
result_dict = scrape_data_from_google_podcasts(query)
|
||||
|
||||
@ -1,19 +1,39 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
from boardgames.models import BoardGame
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def boardgame_scrobble():
|
||||
user = User.objects.create(
|
||||
email="test@exmaple.com", first_name="Test", last_name="User"
|
||||
)
|
||||
return Scrobble.objects.create(
|
||||
board_game=BoardGame.objects.create(title="Test Board Game"),
|
||||
media_type="BoardGame",
|
||||
played_to_completion=True,
|
||||
log={
|
||||
"players": [
|
||||
{"user_id": user.id, "win": True, "score": 30, "color": "Blue"}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class MopidyRequest:
|
||||
name = "Same in the End"
|
||||
artist = "Sublime"
|
||||
album = "Sublime"
|
||||
track_number = 4
|
||||
run_time_ticks = 156604
|
||||
run_time = "156"
|
||||
run_time = 60
|
||||
playback_time_ticks = 15045
|
||||
musicbrainz_track_id = "54214d63-5adf-4909-87cd-c65c37a6d558"
|
||||
musicbrainz_album_id = "03b864cd-7761-314c-a892-05a89ddff00d"
|
||||
@ -65,8 +85,8 @@ def valid_auth_token():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mopidy_track_request_data():
|
||||
return MopidyRequest().request_json
|
||||
def mopidy_track():
|
||||
return MopidyRequest()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -78,6 +98,61 @@ def mopidy_track_diff_album_request_data(**kwargs):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mopidy_podcast_request_data():
|
||||
def mopidy_podcast():
|
||||
mopidy_uri = "local:podcast:Up%20First/2022-01-01%20Up%20First.mp3"
|
||||
return MopidyRequest(mopidy_uri=mopidy_uri).request_json
|
||||
return MopidyRequest(mopidy_uri=mopidy_uri)
|
||||
|
||||
|
||||
class JellyfinTrackRequest:
|
||||
name = "Emotion"
|
||||
artist = "Carly Rae Jepsen"
|
||||
album = "Emotion"
|
||||
track_number = 1
|
||||
item_type = "Audio"
|
||||
timestamp = "2024-01-14 12:00:19"
|
||||
run_time_ticks = 156604
|
||||
run_time = "00:00:60"
|
||||
playback_time_ticks = 15045
|
||||
musicbrainz_track_id = "54214d63-5adf-4909-87cd-c65c37a6d558"
|
||||
musicbrainz_album_id = "03b864cd-7761-314c-a892-05a89ddff00d"
|
||||
musicbrainz_artist_id = "95f5b748-d370-47fe-85bd-0af2dc450bc0"
|
||||
status = "resumed"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.request_data = {
|
||||
"Name": kwargs.get("name", self.name),
|
||||
"Artist": kwargs.get("artist", self.artist),
|
||||
"Album": kwargs.get("album", self.album),
|
||||
"TrackNumber": int(kwargs.get("track_number", self.track_number)),
|
||||
"RunTime": kwargs.get("run_time", self.run_time),
|
||||
"ItemType": kwargs.get("item_type", self.item_type),
|
||||
"UtcTimestamp": kwargs.get("timestamp", self.timestamp),
|
||||
"PlaybackPositionTicks": int(
|
||||
kwargs.get("playback_time_ticks", self.playback_time_ticks)
|
||||
),
|
||||
"Provider_musicbrainztrack": kwargs.get(
|
||||
"musicbrainz_track_id", self.musicbrainz_track_id
|
||||
),
|
||||
"Provider_musicbrainzalbum": kwargs.get(
|
||||
"musicbrainz_album_id", self.musicbrainz_album_id
|
||||
),
|
||||
"Provider_musicbrainzartist": kwargs.get(
|
||||
"musicbrainz_artist_id", self.musicbrainz_artist_id
|
||||
),
|
||||
"Status": kwargs.get("status", self.status),
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
for key in self.request_data.keys():
|
||||
if self.request_data[key] != getattr(self, key):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def request_json(self):
|
||||
return json.dumps(self.request_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jellyfin_track():
|
||||
return JellyfinTrackRequest()
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import time_machine
|
||||
@ -13,12 +11,13 @@ from profiles.models import UserProfile
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
def build_scrobbles(client, request_data, num=7, spacing=2):
|
||||
def build_scrobbles(client, request_json, num=7, spacing=2):
|
||||
url = reverse("scrobbles:mopidy-webhook")
|
||||
user = get_user_model().objects.create(username="Test User")
|
||||
UserProfile.objects.create(user=user, timezone="US/Eastern")
|
||||
user.profile.timezone = "US/Eastern"
|
||||
user.profile.save()
|
||||
for i in range(num):
|
||||
client.post(url, request_data, content_type="application/json")
|
||||
client.post(url, request_json, content_type="application/json")
|
||||
s = Scrobble.objects.last()
|
||||
s.user = user
|
||||
s.timestamp = timezone.now() - timedelta(days=i * spacing)
|
||||
@ -28,8 +27,8 @@ def build_scrobbles(client, request_data, num=7, spacing=2):
|
||||
|
||||
@pytest.mark.django_db
|
||||
@time_machine.travel(datetime(2022, 3, 4, 1, 24))
|
||||
def test_scrobble_counts_data(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data)
|
||||
def test_scrobble_counts_data(client, mopidy_track):
|
||||
build_scrobbles(client, mopidy_track.request_json)
|
||||
user = get_user_model().objects.first()
|
||||
count_dict = scrobble_counts(user)
|
||||
assert count_dict == {
|
||||
@ -43,8 +42,8 @@ def test_scrobble_counts_data(client, mopidy_track_request_data):
|
||||
|
||||
@pytest.mark.django_db
|
||||
@time_machine.travel(datetime(2022, 3, 4, 1, 24))
|
||||
def test_live_charts(client, mopidy_track_request_data):
|
||||
build_scrobbles(client, mopidy_track_request_data, 7, 1)
|
||||
def test_live_charts(client, mopidy_track):
|
||||
build_scrobbles(client, mopidy_track.request_json, 7, 1)
|
||||
user = get_user_model().objects.first()
|
||||
|
||||
week = week_of_scrobbles(user)
|
||||
|
||||
31
tests/scrobbles_tests/test_metadata.py
Normal file
31
tests/scrobbles_tests/test_metadata.py
Normal file
@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from scrobbles.dataclasses import BoardGameLogData, BoardGameScoreLogData
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_boardgame_log_data(boardgame_scrobble):
|
||||
assert not boardgame_scrobble.geo_location
|
||||
assert boardgame_scrobble.logdata == BoardGameLogData(
|
||||
players=[
|
||||
BoardGameScoreLogData(
|
||||
user_id=1,
|
||||
name_str="",
|
||||
bgg_username="",
|
||||
color="Blue",
|
||||
character=None,
|
||||
team=None,
|
||||
score=30,
|
||||
win=True,
|
||||
new=None,
|
||||
)
|
||||
],
|
||||
location=None,
|
||||
geo_location_id=None,
|
||||
difficulty=None,
|
||||
solo=None,
|
||||
two_handed=None,
|
||||
)
|
||||
assert len(boardgame_scrobble.logdata.players) == 1
|
||||
assert boardgame_scrobble.logdata.players[0].user.id == 1
|
||||
assert boardgame_scrobble.logdata.players[0].name == "Test"
|
||||
@ -1,7 +1,12 @@
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
import time_machine
|
||||
from django.urls import reverse
|
||||
from music.models import Track
|
||||
from podcasts.models import Episode
|
||||
from podcasts.models import PodcastEpisode
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
@ -25,31 +30,57 @@ def test_bad_mopidy_request_data(client, valid_auth_token):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"seconds, expected_percent_played, expected_scrobble_id",
|
||||
[
|
||||
(1, 1, 1),
|
||||
(58, 96, 1),
|
||||
(59, 98, 1),
|
||||
(60, 100, 1),
|
||||
(1, 1, 2),
|
||||
(1, 1, 3),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_scrobble_mopidy_track(
|
||||
client, mopidy_track_request_data, valid_auth_token
|
||||
client,
|
||||
mopidy_track,
|
||||
valid_auth_token,
|
||||
seconds,
|
||||
expected_percent_played,
|
||||
expected_scrobble_id,
|
||||
):
|
||||
url = reverse("scrobbles:mopidy-webhook")
|
||||
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||
response = client.post(
|
||||
url,
|
||||
mopidy_track_request_data,
|
||||
content_type="application/json",
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data == {"scrobble_id": 1}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.title == "Same in the End"
|
||||
# Start new scrobble
|
||||
minutes = 0
|
||||
calc_seconds = seconds
|
||||
if seconds >= 60:
|
||||
minutes = 1
|
||||
calc_seconds = calc_seconds % 10
|
||||
with time_machine.travel(datetime(2024, 1, 14, 12, minutes, calc_seconds)):
|
||||
mopidy_track.request_data["playback_time_ticks"] = seconds * 1000
|
||||
response = client.post(
|
||||
url,
|
||||
mopidy_track.request_json,
|
||||
content_type="application/json",
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data == {"scrobble_id": expected_scrobble_id}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.percent_played == expected_percent_played
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.title == "Same in the End"
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="API is unstable")
|
||||
@pytest.mark.skip(reason="Allmusic API is unstable")
|
||||
@pytest.mark.django_db
|
||||
def test_scrobble_mopidy_same_track_different_album(
|
||||
client,
|
||||
mopidy_track_request_data,
|
||||
mopidy_track,
|
||||
mopidy_track_diff_album_request_data,
|
||||
valid_auth_token,
|
||||
):
|
||||
@ -57,7 +88,7 @@ def test_scrobble_mopidy_same_track_different_album(
|
||||
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||
response = client.post(
|
||||
url,
|
||||
mopidy_track_request_data,
|
||||
mopidy_track.request_data,
|
||||
content_type="application/json",
|
||||
headers=headers,
|
||||
)
|
||||
@ -96,5 +127,85 @@ def test_scrobble_mopidy_podcast(
|
||||
assert response.data == {"scrobble_id": 1}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Episode
|
||||
assert scrobble.media_obj.__class__ == PodcastEpisode
|
||||
assert scrobble.media_obj.title == "Up First"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("music.utils.lookup_artist_from_mb", return_value={})
|
||||
@patch(
|
||||
"music.utils.lookup_album_dict_from_mb",
|
||||
return_value={"year": "1999", "mb_group_id": 1},
|
||||
)
|
||||
@patch("music.utils.lookup_track_from_mb", return_value={})
|
||||
@patch("music.models.lookup_artist_from_tadb", return_value={})
|
||||
@patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"})
|
||||
@patch("music.models.Album.fetch_artwork", return_value=None)
|
||||
@patch("music.models.Album.scrape_allmusic", return_value=None)
|
||||
def test_scrobble_jellyfin_track(
|
||||
mock_lookup_artist,
|
||||
mock_lookup_album,
|
||||
mock_lookup_track,
|
||||
mock_lookup_artist_tadb,
|
||||
mock_lookup_album_tadb,
|
||||
mock_fetch_artwork,
|
||||
mock_scrape_allmusic,
|
||||
client,
|
||||
jellyfin_track,
|
||||
valid_auth_token,
|
||||
):
|
||||
url = reverse("scrobbles:jellyfin-webhook")
|
||||
headers = {"Authorization": f"Token {valid_auth_token}"}
|
||||
|
||||
with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)):
|
||||
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
response = client.post(
|
||||
url,
|
||||
jellyfin_track.request_json,
|
||||
content_type="application/json",
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.data == {"scrobble_id": 1}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.title == "Emotion"
|
||||
|
||||
with time_machine.travel(datetime(2024, 1, 14, 12, 0, 58)):
|
||||
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
response = client.post(
|
||||
url,
|
||||
jellyfin_track.request_json,
|
||||
content_type="application/json",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == {"scrobble_id": 1}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.title == "Emotion"
|
||||
|
||||
with time_machine.travel(datetime(2024, 1, 14, 12, 1, 1)):
|
||||
jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
response = client.post(
|
||||
url,
|
||||
jellyfin_track.request_json,
|
||||
content_type="application/json",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.data == {"scrobble_id": 2}
|
||||
|
||||
scrobble = Scrobble.objects.get(id=1)
|
||||
assert scrobble.media_obj.__class__ == Track
|
||||
assert scrobble.media_obj.title == "Emotion"
|
||||
|
||||
@ -20,6 +20,7 @@ VROBBLER_THESPORTSDB_API_KEY="<key>"
|
||||
VROBBLER_THEAUDIODB_API_KEY="<key>"
|
||||
VROBBLER_IGDB_CLIENT_ID="<id>"
|
||||
VROBBLER_IGDB_CLIENT_SECRET="<key>"
|
||||
VROBBLER_COMICVINE_API_KEY="<key>"
|
||||
|
||||
# Storages
|
||||
# VROBBLER_DATABASE_URL="postgres://USER:PASSWORD@HOST:PORT/NAME"
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ from django.urls import reverse
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import ResizeToFit
|
||||
from scrobbles.dataclasses import BoardGameLogData
|
||||
from scrobbles.mixins import ScrobblableMixin
|
||||
from vrobbler.apps.boardgames.bgg import lookup_boardgame_id_from_bgg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
BNULL = {"blank": True, "null": True}
|
||||
@ -96,6 +96,10 @@ class BoardGame(ScrobblableMixin):
|
||||
"boardgames:boardgame_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
return BoardGameLogData
|
||||
|
||||
def primary_image_url(self) -> str:
|
||||
url = ""
|
||||
if self.cover:
|
||||
@ -151,12 +155,12 @@ class BoardGame(ScrobblableMixin):
|
||||
cls, lookup_id: str, data: Optional[dict] = {}
|
||||
) -> Optional["BoardGame"]:
|
||||
"""Given a Lookup ID (either BGG or BGA ID), return a board game object"""
|
||||
boardgame = None
|
||||
boardgame = cls.objects.filter(bggeek_id=lookup_id).first()
|
||||
|
||||
if not data:
|
||||
if not data or not boardgame:
|
||||
data = lookup_boardgame_from_bgg(lookup_id)
|
||||
|
||||
if data:
|
||||
if data and not boardgame:
|
||||
boardgame, created = cls.objects.get_or_create(
|
||||
title=data["title"], bggeek_id=lookup_id
|
||||
)
|
||||
|
||||
@ -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",
|
||||
),
|
||||
|
||||
@ -55,9 +55,7 @@ def scrape_data_from_amazon(url) -> dict:
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
soup = BeautifulSoup(r.text, "html.parser")
|
||||
import pdb
|
||||
|
||||
pdb.set_trace()
|
||||
# TODO Fix this scraper
|
||||
data_dict["rating"] = get_rating_from_soup(soup)
|
||||
data_dict["review"] = get_review_from_soup(soup)
|
||||
return data_dict
|
||||
|
||||
233
vrobbler/apps/books/comicvine.py
Normal file
233
vrobbler/apps/books/comicvine.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""
|
||||
ComicVine API Information & Documentation:
|
||||
https://comicvine.gamespot.com/api/
|
||||
https://comicvine.gamespot.com/api/documentation
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComicVineClient(object):
|
||||
"""
|
||||
Interacts with the ``search`` resource of the ComicVine API. Requires an
|
||||
account on https://comicvine.gamespot.com/ in order to obtain an API key.
|
||||
"""
|
||||
|
||||
# All API requests made by this client will be made to this URL.
|
||||
API_URL = "https://www.comicvine.com/api/search/"
|
||||
|
||||
# A valid User-Agent header must be set in order for our API requests to
|
||||
# be accepted, otherwise our request will be rejected with a
|
||||
# **403 - Forbidden** error.
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:7.0) "
|
||||
"Gecko/20130825 Firefox/36.0"
|
||||
}
|
||||
|
||||
# A set of valid resource types to return in results.
|
||||
RESOURCE_TYPES = {
|
||||
"character",
|
||||
"issue",
|
||||
"location",
|
||||
"object",
|
||||
"person",
|
||||
"publisher",
|
||||
"story_arc",
|
||||
"team",
|
||||
"volume",
|
||||
}
|
||||
|
||||
def __init__(self, api_key, expire_after=300):
|
||||
"""
|
||||
Store the API key in a class variable, and install the requests cache,
|
||||
configuring it using the ``expire_after`` parameter.
|
||||
|
||||
:param api_key: Your personal ComicVine API key.
|
||||
:type api_key: str
|
||||
:param expire_after: The number of seconds to retain an entry in cache.
|
||||
:type expire_after: int or None
|
||||
"""
|
||||
|
||||
self.api_key = api_key
|
||||
|
||||
def search(self, query, offset=0, limit=10, resources=None):
|
||||
"""
|
||||
Perform a search against the API, using the provided query term. If
|
||||
required, a list of resource types to filter search results to can
|
||||
be included.
|
||||
|
||||
Take the JSON contained in the response and provide it to the custom
|
||||
``Response`` object's constructor. Return the ``Response`` object.
|
||||
|
||||
:param query: The search query with which to make the request.
|
||||
:type query: str
|
||||
:param offset: The index of the first record returned.
|
||||
:type offset: int or None
|
||||
:param limit: How many records to return **(max 10)**
|
||||
:type limit: int or None
|
||||
:param resources: A list of resources to include in the search results.
|
||||
:type resources: list or None
|
||||
:type use_cache: bool
|
||||
|
||||
:return: The response object containing the results of the search
|
||||
query.
|
||||
:rtype: comicvine_search.response.Response
|
||||
"""
|
||||
|
||||
params = self._request_params(query, offset, limit, resources)
|
||||
json_data = self._query_api(params)
|
||||
|
||||
return json_data
|
||||
|
||||
def _request_params(self, query, offset, limit, resources):
|
||||
"""
|
||||
Construct a dict containing the required key-value pairs of parameters
|
||||
required in order to make the API request.
|
||||
|
||||
The documentation for the ``search`` resource can be found at
|
||||
https://comicvine.gamespot.com/api/documentation#toc-0-30.
|
||||
|
||||
Regarding 'limit', as per the documentation:
|
||||
|
||||
The number of results to display per page. This value defaults to
|
||||
10 and can not exceed this number.
|
||||
|
||||
:param query: The search query with which to make the request.
|
||||
:type query: str
|
||||
:param offset: The index of the first record returned.
|
||||
:type offset: int
|
||||
:param limit: How many records to return **(max 10)**
|
||||
:type limit: int
|
||||
:param resources: A list of resources to include in the search results.
|
||||
:type resources: list or None
|
||||
|
||||
:return: A dictionary of request parameters.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return {
|
||||
"api_key": self.api_key,
|
||||
"format": "json",
|
||||
"limit": min(10, limit), # hard limit of 10
|
||||
"offset": max(0, offset), # cannot provide negative offset
|
||||
"query": query,
|
||||
"resources": self._validate_resources(resources),
|
||||
}
|
||||
|
||||
def _validate_resources(self, resources):
|
||||
"""
|
||||
Provided a list of resources, first convert it to a set and perform an
|
||||
intersection with the set of valid resource types, ``RESOURCE_TYPES``.
|
||||
Return a comma-separted string of the remaining valid resources, or
|
||||
None if the set is empty.
|
||||
|
||||
:param resources: A list of resources to include in the search results.
|
||||
:type resources: list or None
|
||||
|
||||
:return: A comma-separated string of valid resources.
|
||||
:rtype: str or None
|
||||
"""
|
||||
|
||||
if not resources:
|
||||
return None
|
||||
|
||||
valid_resources = self.RESOURCE_TYPES & set(resources)
|
||||
return ",".join(valid_resources) if valid_resources else None
|
||||
|
||||
def _query_api(self, params):
|
||||
"""
|
||||
Query the ComicVine API's ``search`` resource, providing the required
|
||||
headers and parameters with the request. Optionally allow the caller
|
||||
of the function to disable the request cache.
|
||||
|
||||
If an error occurs during the request, handle it accordingly. Upon
|
||||
success, return the JSON from the response.
|
||||
|
||||
:param params: Parameters to include with the request.
|
||||
:type params: dict
|
||||
:param use_cache: Toggle the use of requests_cache.
|
||||
:type use_cache: bool
|
||||
|
||||
:return: The JSON contained in the response.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
# Since we're performing the identical action regardless of whether
|
||||
# or not the request cache is to be used, store the procedure in a
|
||||
# local function to avoid repetition.
|
||||
def __httpget():
|
||||
response = requests.get(
|
||||
self.API_URL, headers=self.HEADERS, params=params
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
self._handle_http_error(response)
|
||||
|
||||
return response.json()
|
||||
|
||||
return __httpget()
|
||||
|
||||
def _handle_http_error(self, response):
|
||||
"""
|
||||
Provided a ``requests.Response`` object, if the status code is
|
||||
anything other than **200**, we will treat it as an error.
|
||||
|
||||
Using the response's status code, determine which type of exception to
|
||||
raise. Construct an exception message from the response's status code
|
||||
and reason properties before raising the exception.
|
||||
|
||||
:param response: The requests.Response object returned by the HTTP
|
||||
request.
|
||||
:type response: requests.Response
|
||||
|
||||
:raises ComicVineUnauthorizedException: if no API key provided.
|
||||
:raises ComicVineForbiddenException: if no User-Agent header provided.
|
||||
:raises ComicVineApiException: if an unidentified error occurs.
|
||||
"""
|
||||
|
||||
exception = {
|
||||
401: Exception,
|
||||
403: Exception,
|
||||
}.get(response.status_code, Exception)
|
||||
message = f"{response.status_code} {response.reason}"
|
||||
|
||||
raise exception(message)
|
||||
|
||||
|
||||
def lookup_comic_from_comicvine(title: str) -> dict:
|
||||
api_key = getattr(settings, "COMICVINE_API_KEY", "")
|
||||
if not api_key:
|
||||
logger.warn("No ComicVine API key configured, not looking anything up")
|
||||
return {}
|
||||
|
||||
client = ComicVineClient(
|
||||
api_key=getattr(settings, "COMICVINE_API_KEY", None)
|
||||
)
|
||||
result = [
|
||||
r
|
||||
for r in client.search(title).get("results")
|
||||
if r.get("resource_type") == "volume"
|
||||
][0]
|
||||
|
||||
if "volume" not in result.keys():
|
||||
logger.warn("No result found on ComicVine", extra={"title": title})
|
||||
return {}
|
||||
|
||||
title = " ".join([result.get("volume").get("name"), result.get("name)")])
|
||||
data_dict = {
|
||||
"title": title,
|
||||
"cover_url": result.get("image").get("original_url"),
|
||||
"comicvine_data": {
|
||||
"id": result.get("id"),
|
||||
"site_detail_url": result.get("site_detail_url"),
|
||||
"description": result.get("description"),
|
||||
"image": result.get("image").get("original_url"),
|
||||
},
|
||||
}
|
||||
|
||||
return data_dict
|
||||
@ -1,22 +1,20 @@
|
||||
import codecs
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Iterable, List
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from books.models import Author, Book, Page
|
||||
from books.models import Author, Book
|
||||
from books.openlibrary import get_author_openlibrary_id
|
||||
from django.db.models import Sum
|
||||
from pylast import httpx, tempfile
|
||||
from scrobbles.models import Scrobble
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import get_user_model
|
||||
from stream_sqlite import stream_sqlite
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class KoReaderBookColumn(Enum):
|
||||
@ -43,11 +41,79 @@ class KoReaderPageStatColumn(Enum):
|
||||
|
||||
|
||||
def _sqlite_bytes(sqlite_url):
|
||||
with httpx.stream("GET", sqlite_url) as r:
|
||||
yield from r.iter_bytes(chunk_size=65_536)
|
||||
with requests.get(sqlite_url, stream=True) as r:
|
||||
yield from r.iter_content(chunk_size=65_536)
|
||||
|
||||
|
||||
def get_book_map_from_sqlite(rows: Iterable) -> dict:
|
||||
# Grace period between page reads for it to be a new scrobble
|
||||
SESSION_GAP_SECONDS = 1800 # a half hour
|
||||
|
||||
|
||||
def get_author_str_from_row(row):
|
||||
"""Given a the raw author string from KoReader, convert it to a single line and
|
||||
strip the middle initials, as OpenLibrary lookup usually fails with those.
|
||||
"""
|
||||
ko_authors = row[KoReaderBookColumn.AUTHORS.value].replace("\n", ", ")
|
||||
# Strip middle initials, OpenLibrary often fails with these
|
||||
return re.sub(" [A-Z]. ", " ", ko_authors)
|
||||
|
||||
|
||||
def lookup_or_create_authors_from_author_str(ko_author_str: str) -> list:
|
||||
"""Takes a string of authors from KoReader and returns a list
|
||||
of Authors from our database
|
||||
"""
|
||||
author_str_list = ko_author_str.split(", ")
|
||||
author_list = []
|
||||
for author_str in author_str_list:
|
||||
logger.debug(f"Looking up author {author_str}")
|
||||
# KoReader gave us nothing, bail
|
||||
if author_str == "N/A":
|
||||
logger.warn(f"KoReader author string is N/A, no authors to find")
|
||||
continue
|
||||
|
||||
author = Author.objects.filter(name=author_str).first()
|
||||
if not author:
|
||||
author = Author.objects.create(
|
||||
name=author_str,
|
||||
openlibrary_id=get_author_openlibrary_id(author_str),
|
||||
)
|
||||
author.fix_metadata()
|
||||
logger.debug(f"Created author {author}")
|
||||
author_list.append(author)
|
||||
return author_list
|
||||
|
||||
|
||||
def create_book_from_row(row: list):
|
||||
# No KoReader book yet, create it
|
||||
author_str = get_author_str_from_row(row)
|
||||
total_pages = row[KoReaderBookColumn.PAGES.value]
|
||||
run_time = total_pages * Book.AVG_PAGE_READING_SECONDS
|
||||
|
||||
book = Book.objects.create(
|
||||
title=row[KoReaderBookColumn.TITLE.value],
|
||||
pages=total_pages,
|
||||
koreader_data_by_hash={
|
||||
str(row[KoReaderBookColumn.MD5.value]): {
|
||||
"title": row[KoReaderBookColumn.TITLE.value],
|
||||
"author_str": author_str,
|
||||
"book_id": row[KoReaderBookColumn.ID.value],
|
||||
"pages": total_pages,
|
||||
}
|
||||
},
|
||||
run_time_seconds=run_time,
|
||||
)
|
||||
book.fix_metadata()
|
||||
|
||||
# Add authors
|
||||
author_list = lookup_or_create_authors_from_author_str(author_str)
|
||||
if author_list:
|
||||
book.authors.add(*author_list)
|
||||
|
||||
# self._lookup_authors
|
||||
return book
|
||||
|
||||
|
||||
def build_book_map(rows) -> dict:
|
||||
"""Given an interable of sqlite rows from the books table, lookup existing
|
||||
books, create ones that don't exist, and return a mapping of koreader IDs to
|
||||
primary key IDs for page creation.
|
||||
@ -56,207 +122,284 @@ def get_book_map_from_sqlite(rows: Iterable) -> dict:
|
||||
book_id_map = {}
|
||||
|
||||
for book_row in rows:
|
||||
if (
|
||||
book_row[KoReaderBookColumn.TITLE.value]
|
||||
== "KOReader Quickstart Guide"
|
||||
):
|
||||
logger.info(
|
||||
"Ignoring the KOReader quickstart guide. No on wants that."
|
||||
)
|
||||
continue
|
||||
book = Book.objects.filter(
|
||||
koreader_md5=book_row[KoReaderBookColumn.MD5.value]
|
||||
koreader_data_by_hash__icontains=book_row[
|
||||
KoReaderBookColumn.MD5.value
|
||||
]
|
||||
).first()
|
||||
|
||||
if not book:
|
||||
book, created = Book.objects.get_or_create(
|
||||
title=book_row[KoReaderBookColumn.TITLE.value]
|
||||
)
|
||||
|
||||
if created:
|
||||
total_pages = book_row[KoReaderBookColumn.PAGES.value]
|
||||
run_time = total_pages * book.AVG_PAGE_READING_SECONDS
|
||||
ko_authors = book_row[
|
||||
KoReaderBookColumn.AUTHORS.value
|
||||
].replace("\n", ", ")
|
||||
# Strip middle initials, OpenLibrary often fails with these
|
||||
ko_authors = re.sub(" [A-Z]. ", " ", ko_authors)
|
||||
book_dict = {
|
||||
"title": book_row[KoReaderBookColumn.TITLE.value],
|
||||
"pages": total_pages,
|
||||
"koreader_md5": book_row[KoReaderBookColumn.MD5.value],
|
||||
"koreader_id": int(book_row[KoReaderBookColumn.ID.value]),
|
||||
"koreader_authors": ko_authors,
|
||||
"run_time_seconds": run_time,
|
||||
}
|
||||
Book.objects.filter(pk=book.id).update(**book_dict)
|
||||
|
||||
# Add authors
|
||||
authors = ko_authors.split(", ")
|
||||
author_list = []
|
||||
for author_str in authors:
|
||||
logger.debug(f"Looking up author {author_str}")
|
||||
if author_str == "N/A":
|
||||
continue
|
||||
|
||||
author = Author.objects.filter(name=author_str).first()
|
||||
if not author:
|
||||
author = Author.objects.create(
|
||||
name=author_str,
|
||||
openlibrary_id=get_author_openlibrary_id(
|
||||
author_str
|
||||
),
|
||||
)
|
||||
author.fix_metadata()
|
||||
logger.debug(f"Created author {author}")
|
||||
book.authors.add(author)
|
||||
|
||||
# This will try to fix metadata by looking it up on OL
|
||||
book.fix_metadata()
|
||||
book = create_book_from_row(book_row)
|
||||
|
||||
book.refresh_from_db()
|
||||
total_seconds = 0
|
||||
if book_row[KoReaderBookColumn.TOTAL_READ_TIME.value]:
|
||||
total_seconds = book_row[KoReaderBookColumn.TOTAL_READ_TIME.value]
|
||||
|
||||
book_id_map[book_row[KoReaderBookColumn.ID.value]] = (
|
||||
book.id,
|
||||
total_seconds,
|
||||
)
|
||||
|
||||
book_id_map[book_row[KoReaderBookColumn.ID.value]] = {
|
||||
"book_id": book.id,
|
||||
"hash": book_row[KoReaderBookColumn.MD5.value],
|
||||
"total_seconds": total_seconds,
|
||||
}
|
||||
return book_id_map
|
||||
|
||||
|
||||
def build_scrobbles_from_pages(
|
||||
rows: Iterable, book_id_map: dict, user_id: int
|
||||
) -> List[Scrobble]:
|
||||
new_scrobbles = []
|
||||
def build_page_data(page_rows: list, book_map: dict, user_tz=None) -> dict:
|
||||
"""Given rows of page data from KoReader, parse each row and build
|
||||
scrobbles for our user, loading the page data into the page_data
|
||||
field on the scrobble instance.
|
||||
"""
|
||||
book_ids_not_found = []
|
||||
for page_row in page_rows:
|
||||
koreader_book_id = page_row[KoReaderPageStatColumn.ID_BOOK.value]
|
||||
|
||||
new_scrobbles = []
|
||||
pages_found = []
|
||||
book_read_time_map = {}
|
||||
for page_row in rows:
|
||||
koreader_id = page_row[KoReaderPageStatColumn.ID_BOOK.value]
|
||||
if koreader_id not in book_id_map.keys():
|
||||
if koreader_book_id not in book_map.keys():
|
||||
book_ids_not_found.append(koreader_book_id)
|
||||
continue
|
||||
|
||||
if "pages" not in book_map[koreader_book_id].keys():
|
||||
book_map[koreader_book_id]["pages"] = {}
|
||||
|
||||
page_number = page_row[KoReaderPageStatColumn.PAGE.value]
|
||||
ts = page_row[KoReaderPageStatColumn.START_TIME.value]
|
||||
book_id = book_id_map[koreader_id][0]
|
||||
book_read_time_map[book_id] = book_id_map[koreader_id][1]
|
||||
duration = page_row[KoReaderPageStatColumn.DURATION.value]
|
||||
start_ts = page_row[KoReaderPageStatColumn.START_TIME.value]
|
||||
|
||||
page, page_created = Page.objects.get_or_create(
|
||||
book_id=book_id, number=page_number, user_id=user_id
|
||||
book_map[koreader_book_id]["pages"][page_number] = {
|
||||
"duration": duration,
|
||||
"start_ts": start_ts,
|
||||
"end_ts": start_ts + duration,
|
||||
}
|
||||
if book_ids_not_found:
|
||||
logger.info(
|
||||
f"Found pages for books not in file: {set(book_ids_not_found)}"
|
||||
)
|
||||
if page_created:
|
||||
page.start_time = datetime.utcfromtimestamp(ts).replace(
|
||||
tzinfo=pytz.utc
|
||||
return book_map
|
||||
|
||||
|
||||
def build_scrobbles_from_book_map(
|
||||
book_map: dict, user: "User"
|
||||
) -> list["Scrobble"]:
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
|
||||
scrobbles_to_create = []
|
||||
|
||||
pages_not_found = []
|
||||
for koreader_book_id, book_dict in book_map.items():
|
||||
book_id = book_dict["book_id"]
|
||||
if "pages" not in book_dict.keys():
|
||||
pages_not_found.append(book_id)
|
||||
continue
|
||||
|
||||
should_create_scrobble = False
|
||||
scrobble_page_data = {}
|
||||
playback_position_seconds = 0
|
||||
prev_page_stats = {}
|
||||
last_page_number = 0
|
||||
|
||||
pages_processed = 0
|
||||
total_pages_read = len(book_map[koreader_book_id]["pages"])
|
||||
ordered_pages = sorted(
|
||||
book_map[koreader_book_id]["pages"].items(),
|
||||
key=lambda x: x[1]["start_ts"],
|
||||
)
|
||||
|
||||
for cur_page_number, stats in ordered_pages:
|
||||
pages_processed += 1
|
||||
|
||||
seconds_from_last_page = 0
|
||||
if prev_page_stats:
|
||||
seconds_from_last_page = stats.get(
|
||||
"end_ts"
|
||||
) - prev_page_stats.get("start_ts")
|
||||
|
||||
playback_position_seconds = playback_position_seconds + stats.get(
|
||||
"duration"
|
||||
)
|
||||
page.duration_seconds = page_row[
|
||||
KoReaderPageStatColumn.DURATION.value
|
||||
]
|
||||
page.save(update_fields=["start_time", "duration_seconds"])
|
||||
pages_found.append(page)
|
||||
|
||||
playback_position_seconds = 0
|
||||
for page in set(pages_found):
|
||||
# Add up page seconds to set the aggregate time of all pages to reading time
|
||||
playback_position_seconds = (
|
||||
playback_position_seconds + page.duration_seconds
|
||||
)
|
||||
if page.is_scrobblable:
|
||||
# Check to see if a scrobble with this timestamp, book and user already exists
|
||||
scrobble = Scrobble.objects.filter(
|
||||
timestamp=page.start_time,
|
||||
book_id=page.book_id,
|
||||
user_id=user_id,
|
||||
).first()
|
||||
if not scrobble:
|
||||
logger.debug(
|
||||
f"Queueing scrobble for {page.book}, page {page.number}"
|
||||
end_of_reading = pages_processed == total_pages_read
|
||||
big_jump_to_this_page = (cur_page_number - last_page_number) > 10
|
||||
is_session_gap = seconds_from_last_page > SESSION_GAP_SECONDS
|
||||
if (
|
||||
is_session_gap and not big_jump_to_this_page
|
||||
) or end_of_reading:
|
||||
should_create_scrobble = True
|
||||
|
||||
if should_create_scrobble:
|
||||
scrobble_page_data = dict(
|
||||
sorted(
|
||||
scrobble_page_data.items(),
|
||||
key=lambda x: x[1]["start_ts"],
|
||||
)
|
||||
)
|
||||
new_scrobble = Scrobble(
|
||||
book_id=page.book_id,
|
||||
user_id=user_id,
|
||||
source="KOReader",
|
||||
media_type=Scrobble.MediaType.BOOK,
|
||||
timestamp=page.start_time,
|
||||
played_to_completion=True,
|
||||
playback_position_seconds=playback_position_seconds,
|
||||
in_progress=False,
|
||||
book_pages_read=page.number,
|
||||
long_play_complete=False,
|
||||
)
|
||||
new_scrobbles.append(new_scrobble)
|
||||
# After setting a scrobblable page, reset our accumulator
|
||||
playback_position_seconds = 0
|
||||
return new_scrobbles
|
||||
try:
|
||||
first_page = scrobble_page_data.get(
|
||||
list(scrobble_page_data.keys())[0]
|
||||
)
|
||||
last_page = scrobble_page_data.get(
|
||||
list(scrobble_page_data.keys())[-1]
|
||||
)
|
||||
except IndexError:
|
||||
logger.error(
|
||||
"Could not process book, no page data found",
|
||||
extra={"scrobble_page_data": scrobble_page_data},
|
||||
)
|
||||
continue
|
||||
|
||||
timezone = user.profile.timezone
|
||||
|
||||
timestamp = datetime.fromtimestamp(
|
||||
int(first_page.get("start_ts"))
|
||||
).replace(tzinfo=pytz.timezone(timezone))
|
||||
|
||||
# Add a shim here temporarily to fix imports while we were in France
|
||||
# if date is between 10/15 and 12/15, cast it to Europe/Central
|
||||
if (
|
||||
datetime(2023, 10, 15).replace(
|
||||
tzinfo=pytz.timezone("Europe/Paris")
|
||||
)
|
||||
<= timestamp
|
||||
<= datetime(2023, 12, 15).replace(
|
||||
tzinfo=pytz.timezone("Europe/Paris")
|
||||
)
|
||||
):
|
||||
timezone = "Europe/Paris"
|
||||
|
||||
stop_timestamp = datetime.fromtimestamp(
|
||||
int(last_page.get("end_ts"))
|
||||
).replace(tzinfo=pytz.timezone(timezone))
|
||||
|
||||
if (
|
||||
timestamp.tzinfo._dst.seconds == 0
|
||||
or stop_timestamp.tzinfo._dst.seconds == 0
|
||||
):
|
||||
timestamp = timestamp - timedelta(hours=1)
|
||||
stop_timestamp = stop_timestamp - timedelta(hours=1)
|
||||
|
||||
scrobble = Scrobble.objects.filter(
|
||||
timestamp=timestamp,
|
||||
book_id=book_id,
|
||||
user_id=user.id,
|
||||
).first()
|
||||
|
||||
if not scrobble:
|
||||
logger.info(
|
||||
f"Queueing scrobble for {book_id}, page {cur_page_number}"
|
||||
)
|
||||
log_data = {
|
||||
"koreader_hash": book_dict.get("hash"),
|
||||
"page_data": scrobble_page_data,
|
||||
"pages_read": len(scrobble_page_data.keys()),
|
||||
}
|
||||
scrobbles_to_create.append(
|
||||
Scrobble(
|
||||
book_id=book_id,
|
||||
user_id=user.id,
|
||||
source="KOReader",
|
||||
media_type=Scrobble.MediaType.BOOK,
|
||||
timestamp=timestamp,
|
||||
log=log_data,
|
||||
stop_timestamp=stop_timestamp,
|
||||
playback_position_seconds=playback_position_seconds,
|
||||
in_progress=False,
|
||||
played_to_completion=True,
|
||||
long_play_complete=False,
|
||||
timezone=timezone,
|
||||
)
|
||||
)
|
||||
# Then start over
|
||||
should_create_scrobble = False
|
||||
playback_position_seconds = 0
|
||||
scrobble_page_data = {}
|
||||
|
||||
# We accumulate pages for the scrobble until we should create a new one
|
||||
scrobble_page_data[cur_page_number] = stats
|
||||
|
||||
last_page_number = cur_page_number
|
||||
prev_page_stats = stats
|
||||
if pages_not_found:
|
||||
logger.info(f"Pages not found for books: {set(pages_not_found)}")
|
||||
return scrobbles_to_create
|
||||
|
||||
|
||||
def enrich_koreader_scrobbles(scrobbles: list) -> None:
|
||||
def fix_long_play_stats_for_scrobbles(scrobbles: list) -> None:
|
||||
"""Given a list of scrobbles, update pages read, long play seconds and check
|
||||
for media completion"""
|
||||
|
||||
for scrobble in scrobbles:
|
||||
scrobble.book_pages_read = scrobble.book.page_set.last().number
|
||||
# But if there's a next scrobble, set pages read to their starting page
|
||||
#
|
||||
if scrobble.next:
|
||||
scrobble.book_pages_read = scrobble.next.book_pages_read - 1
|
||||
scrobble.long_play_seconds = scrobble.book.page_set.filter(
|
||||
number__lte=scrobble.book_pages_read
|
||||
).aggregate(Sum("duration_seconds"))["duration_seconds__sum"]
|
||||
|
||||
scrobble.save(update_fields=["book_pages_read", "long_play_seconds"])
|
||||
|
||||
|
||||
def process_koreader_sqlite_url(file_url, user_id) -> list:
|
||||
book_id_map = {}
|
||||
new_scrobbles = []
|
||||
|
||||
for table_name, pragma_table_info, rows in stream_sqlite(
|
||||
_sqlite_bytes(file_url), max_buffer_size=1_048_576
|
||||
):
|
||||
logger.debug(f"Found table {table_name} - processing")
|
||||
if table_name == "book":
|
||||
book_id_map = get_book_map_from_sqlite(rows)
|
||||
|
||||
if table_name == "page_stat_data":
|
||||
new_scrobbles = build_scrobbles_from_pages(
|
||||
rows, book_id_map, user_id
|
||||
if scrobble.previous and not scrobble.previous.long_play_complete:
|
||||
scrobble.long_play_seconds = scrobble.playback_position_seconds + (
|
||||
scrobble.previous.long_play_seconds or 0
|
||||
)
|
||||
logger.debug(f"Creating {len(new_scrobbles)} new scrobbles")
|
||||
else:
|
||||
scrobble.long_play_seconds = scrobble.playback_position_seconds
|
||||
scrobble.log["book_pages_read"] = scrobble.calc_pages_read()
|
||||
|
||||
created = []
|
||||
if new_scrobbles:
|
||||
created = Scrobble.objects.bulk_create(new_scrobbles)
|
||||
enrich_koreader_scrobbles(created)
|
||||
logger.info(
|
||||
f"Created {len(created)} scrobbles",
|
||||
extra={"created_scrobbles": created},
|
||||
)
|
||||
return created
|
||||
scrobble.save(update_fields=["log", "long_play_seconds"])
|
||||
|
||||
|
||||
def process_koreader_sqlite_file(file_path, user_id) -> list:
|
||||
"""Given a sqlite file from KoReader, open the book table, iterate
|
||||
over rows creating scrobbles from each book found"""
|
||||
# Create a SQL connection to our SQLite database
|
||||
con = sqlite3.connect(file_path)
|
||||
cur = con.cursor()
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
|
||||
book_id_map = get_book_map_from_sqlite(cur.execute("SELECT * FROM book"))
|
||||
new_scrobbles = build_scrobbles_from_pages(
|
||||
cur.execute("SELECT * from page_stat_data"), book_id_map, user_id
|
||||
)
|
||||
new_scrobbles = []
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
tz = pytz.utc
|
||||
if user:
|
||||
tz = user.profile.timezone
|
||||
|
||||
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:
|
||||
book_map = build_book_map(cur.execute("SELECT * FROM book"))
|
||||
except sqlite3.OperationalError:
|
||||
logger.warning("KOReader sqlite file had not table: book")
|
||||
return new_scrobbles
|
||||
|
||||
book_map = build_page_data(
|
||||
cur.execute(
|
||||
"SELECT * from page_stat_data ORDER BY id_book, start_time"
|
||||
),
|
||||
book_map,
|
||||
tz,
|
||||
)
|
||||
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
|
||||
):
|
||||
logger.debug(f"Found table {table_name} - processing")
|
||||
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)
|
||||
|
||||
logger.info(f"Creating {len(new_scrobbles)} new scrobbles")
|
||||
created = []
|
||||
if new_scrobbles:
|
||||
created = Scrobble.objects.bulk_create(new_scrobbles)
|
||||
enrich_koreader_scrobbles(created)
|
||||
fix_long_play_stats_for_scrobbles(created)
|
||||
logger.info(
|
||||
f"Created {len(created)} scrobbles",
|
||||
extra={"created_scrobbles": created},
|
||||
)
|
||||
return created
|
||||
|
||||
|
||||
def process_koreader_sqlite(file_path: str, user_id: int) -> list:
|
||||
is_os_file = "https://" not in file_path
|
||||
|
||||
if is_os_file:
|
||||
created = process_koreader_sqlite_file(file_path, user_id)
|
||||
else:
|
||||
created = process_koreader_sqlite_url(file_path, user_id)
|
||||
return created
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
import logging
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from books.models import Book
|
||||
from django.core.management.base import BaseCommand
|
||||
from scrobbles.models import Scrobble
|
||||
from vrobbler.apps.books.koreader import fix_long_play_stats_for_scrobbles
|
||||
from vrobbler.apps.scrobbles.utils import timestamp_user_tz_to_utc
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Grace period between page reads for it to be a new scrobble
|
||||
SESSION_GAP_SECONDS = 1800 # a half hour
|
||||
|
||||
def update_scrobble_from_page_data(scrobble, commit=True):
|
||||
page_list = list(scrobble.book_page_data.items())
|
||||
first_page_start_ts = datetime.fromtimestamp(page_list[0][1]["start_ts"])
|
||||
last_page_end_ts = datetime.fromtimestamp(page_list[-1][1]["end_ts"])
|
||||
|
||||
if (
|
||||
datetime(2023, 10, 15) <= first_page_start_ts <= datetime(2023, 12, 15)
|
||||
):
|
||||
first_page_start_ts.replace(tzinfo=pytz.timezone("Europe/Paris"))
|
||||
last_page_end_ts.replace(tzinfo=pytz.timezone("Europe/Paris"))
|
||||
else:
|
||||
first_page_start_ts.replace(tzinfo=pytz.timezone("US/Eastern"))
|
||||
last_page_end_ts.replace(tzinfo=pytz.timezone("US/Eastern"))
|
||||
|
||||
scrobble.timestamp = first_page_start_ts
|
||||
scrobble.stop_timestamp = last_page_end_ts
|
||||
if commit:
|
||||
scrobble.save(update_fields=["timestamp", "stop_timestamp"])
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
scrobbles_to_create = []
|
||||
|
||||
for scrobble in Scrobble.objects.filter(media_type="Book", source="KOReader"):
|
||||
update_scrobble_from_page_data(scrobble)
|
||||
@ -0,0 +1,160 @@
|
||||
import logging
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from books.models import Book
|
||||
from django.core.management.base import BaseCommand
|
||||
from scrobbles.models import Scrobble
|
||||
from vrobbler.apps.books.koreader import fix_long_play_stats_for_scrobbles
|
||||
from vrobbler.apps.scrobbles.utils import timestamp_user_tz_to_utc
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Grace period between page reads for it to be a new scrobble
|
||||
SESSION_GAP_SECONDS = 1800 # a half hour
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
scrobbles_to_create = []
|
||||
|
||||
for book in Book.objects.filter(koreader_id__isnull=False):
|
||||
book.scrobble_set.all().delete()
|
||||
|
||||
koreader_data = book.koreader_data_by_hash or {}
|
||||
if book.koreader_md5:
|
||||
koreader_data[book.koreader_md5] = {
|
||||
"title": book.title,
|
||||
"book_id": book.koreader_id,
|
||||
"author_str": book.koreader_authors,
|
||||
"pages": book.pages,
|
||||
}
|
||||
book.koreader_data_by_hash = koreader_data
|
||||
book.save(update_fields=["koreader_data_by_hash"])
|
||||
|
||||
# Next parse all this book's pages into new scrobbles
|
||||
should_create_scrobble = False
|
||||
scrobble_page_data = {}
|
||||
playback_position_seconds = 0
|
||||
prev_page = None
|
||||
|
||||
pages_processed = 0
|
||||
total_pages = book.pages
|
||||
for page in book.page_set.order_by("number"):
|
||||
user = page.user
|
||||
book_id = page.book.id
|
||||
pages_processed += 1
|
||||
scrobble_page_data[page.number] = {
|
||||
"duration": page.duration_seconds,
|
||||
"start_ts": page.start_time.timestamp(),
|
||||
"end_ts": page.end_time.timestamp(),
|
||||
}
|
||||
seconds_from_last_page = 0
|
||||
if prev_page:
|
||||
seconds_from_last_page = (
|
||||
page.end_time.timestamp()
|
||||
- prev_page.start_time.timestamp()
|
||||
)
|
||||
playback_position_seconds = (
|
||||
playback_position_seconds + page.duration_seconds
|
||||
)
|
||||
|
||||
end_of_reading = pages_processed == total_pages
|
||||
big_jump_to_this_page = False
|
||||
if prev_page:
|
||||
big_jump_to_this_page = (
|
||||
page.number - prev_page.number
|
||||
) > 10
|
||||
if (
|
||||
seconds_from_last_page > SESSION_GAP_SECONDS
|
||||
and not big_jump_to_this_page
|
||||
):
|
||||
should_create_scrobble = True
|
||||
|
||||
if should_create_scrobble:
|
||||
first_page = scrobble_page_data.get(
|
||||
list(scrobble_page_data.keys())[0]
|
||||
)
|
||||
last_page = scrobble_page_data.get(
|
||||
list(scrobble_page_data.keys())[-1]
|
||||
)
|
||||
start_ts = int(first_page.get("start_ts"))
|
||||
end_ts = start_ts + playback_position_seconds
|
||||
|
||||
timestamp = datetime.fromtimestamp(start_ts).replace(
|
||||
tzinfo=user.profile.tzinfo
|
||||
)
|
||||
stop_timestamp = datetime.fromtimestamp(end_ts).replace(
|
||||
tzinfo=user.profile.tzinfo
|
||||
)
|
||||
# Add a shim here temporarily to fix imports while we were in France
|
||||
# if date is between 10/15 and 12/15, cast it to Europe/Central
|
||||
if (
|
||||
datetime(2023, 10, 15).replace(
|
||||
tzinfo=pytz.timezone("Europe/Paris")
|
||||
)
|
||||
<= timestamp
|
||||
<= datetime(2023, 12, 15).replace(
|
||||
tzinfo=pytz.timezone("Europe/Paris")
|
||||
)
|
||||
):
|
||||
timestamp.replace(tzinfo=pytz.timezone("Europe/Paris"))
|
||||
|
||||
elif (
|
||||
timestamp.tzinfo._dst.seconds == 0
|
||||
or stop_timestamp.tzinfo._dst.seconds == 0
|
||||
):
|
||||
timestamp = timestamp - timedelta(hours=1)
|
||||
stop_timestamp = stop_timestamp - timedelta(hours=1)
|
||||
|
||||
scrobble = Scrobble.objects.filter(
|
||||
timestamp=timestamp,
|
||||
book_id=book_id,
|
||||
user_id=user.id,
|
||||
).first()
|
||||
if scrobble:
|
||||
logger.info(
|
||||
f"Found existing scrobble {scrobble}, updating"
|
||||
)
|
||||
scrobble.book_page_data = scrobble_page_data
|
||||
scrobble.playback_position_seconds = (
|
||||
scrobble.calc_reading_duration()
|
||||
)
|
||||
scrobble.save(
|
||||
update_fields=[
|
||||
"book_page_data",
|
||||
"playback_position_seconds",
|
||||
]
|
||||
)
|
||||
if not scrobble:
|
||||
logger.info(
|
||||
f"Queueing scrobble for {book_id}, page {page.number}"
|
||||
)
|
||||
scrobbles_to_create.append(
|
||||
Scrobble(
|
||||
book_id=book_id,
|
||||
user_id=user.id,
|
||||
source="KOReader",
|
||||
media_type=Scrobble.MediaType.BOOK,
|
||||
timestamp=timestamp,
|
||||
stop_timestamp=stop_timestamp,
|
||||
playback_position_seconds=playback_position_seconds,
|
||||
book_koreader_hash=list(
|
||||
book.koreader_data_by_hash.keys()
|
||||
)[0],
|
||||
book_page_data=scrobble_page_data,
|
||||
book_pages_read=page.number,
|
||||
in_progress=False,
|
||||
played_to_completion=True,
|
||||
long_play_complete=False,
|
||||
)
|
||||
)
|
||||
# Then start over
|
||||
should_create_scrobble = False
|
||||
playback_position_seconds = 0
|
||||
scrobble_page_data = {}
|
||||
|
||||
prev_page = page
|
||||
|
||||
created = Scrobble.objects.bulk_create(scrobbles_to_create)
|
||||
fix_long_play_stats_for_scrobbles(created)
|
||||
@ -0,0 +1,29 @@
|
||||
import logging
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from books.models import Book
|
||||
from django.core.management.base import BaseCommand
|
||||
from scrobbles.models import Scrobble
|
||||
from vrobbler.apps.books.koreader import fix_long_play_stats_for_scrobbles
|
||||
from vrobbler.apps.scrobbles.utils import timestamp_user_tz_to_utc
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
total_books = Book.objects.all().count()
|
||||
processed_books = 0
|
||||
|
||||
for book in Book.objects.all():
|
||||
for scrobble in book.scrobble_set.all():
|
||||
log_data = {
|
||||
"koreader_hash": scrobble.book_koreader_hash,
|
||||
"page_data": scrobble.book_page_data,
|
||||
"pages_read": scrobble.book_pages_read,
|
||||
}
|
||||
scrobble.log = log_data
|
||||
scrobble.save(update_fields=["log"])
|
||||
processed_books += 1
|
||||
logger.info(f"Processed book {processed_books} of {total_books}")
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-29 05:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("books", "0019_alter_book_authors"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="author",
|
||||
name="comicvine_data",
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="comicvine_data",
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="koreader_data_by_hash",
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -1,5 +1,6 @@
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import requests
|
||||
@ -22,12 +23,20 @@ from scrobbles.mixins import (
|
||||
)
|
||||
from scrobbles.utils import get_scrobbles_for_media
|
||||
from taggit.managers import TaggableManager
|
||||
from thefuzz import fuzz
|
||||
from vrobbler.apps.books.comicvine import (
|
||||
ComicVineClient,
|
||||
lookup_comic_from_comicvine,
|
||||
)
|
||||
|
||||
from vrobbler.apps.books.locg import (
|
||||
lookup_comic_by_locg_slug,
|
||||
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", "")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
@ -58,6 +67,7 @@ class Author(TimeStampedModel):
|
||||
wikidata_id = models.CharField(max_length=255, **BNULL)
|
||||
goodreads_id = models.CharField(max_length=255, **BNULL)
|
||||
librarything_id = models.CharField(max_length=255, **BNULL)
|
||||
comicvine_data = models.JSONField(**BNULL)
|
||||
amazon_id = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
def __str__(self):
|
||||
@ -91,9 +101,11 @@ class Book(LongPlayScrobblableMixin):
|
||||
title = models.CharField(max_length=255)
|
||||
authors = models.ManyToManyField(Author, blank=True)
|
||||
goodreads_id = models.CharField(max_length=255, **BNULL)
|
||||
# All individual koreader fields are deprecated
|
||||
koreader_id = models.IntegerField(**BNULL)
|
||||
koreader_authors = models.CharField(max_length=255, **BNULL)
|
||||
koreader_md5 = models.CharField(max_length=255, **BNULL)
|
||||
koreader_data_by_hash = models.JSONField(**BNULL)
|
||||
isbn = models.CharField(max_length=255, **BNULL)
|
||||
pages = models.IntegerField(**BNULL)
|
||||
language = models.CharField(max_length=4, **BNULL)
|
||||
@ -101,6 +113,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
first_sentence = models.TextField(**BNULL)
|
||||
openlibrary_id = models.CharField(max_length=255, **BNULL)
|
||||
locg_slug = models.CharField(max_length=255, **BNULL)
|
||||
comicvine_data = models.JSONField(**BNULL)
|
||||
cover = models.ImageField(upload_to="books/covers/", **BNULL)
|
||||
cover_small = ImageSpecField(
|
||||
source="cover",
|
||||
@ -125,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 = ""
|
||||
@ -144,27 +161,35 @@ class Book(LongPlayScrobblableMixin):
|
||||
if self.author:
|
||||
author_name = self.author.name
|
||||
|
||||
if not data:
|
||||
logger.warn(f"Checking openlibrary for {self.title}")
|
||||
if self.openlibrary_id and force_update:
|
||||
data = lookup_book_from_openlibrary(
|
||||
str(self.openlibrary_id)
|
||||
)
|
||||
else:
|
||||
data = lookup_book_from_openlibrary(
|
||||
str(self.title), author_name
|
||||
)
|
||||
|
||||
if not data:
|
||||
if self.locg_slug:
|
||||
logger.warn(
|
||||
f"Checking LOCG for {self.title} with slug {self.locg_slug}"
|
||||
)
|
||||
data = lookup_comic_by_locg_slug(str(self.locg_slug))
|
||||
else:
|
||||
logger.warn(f"Checking LOCG for {self.title}")
|
||||
data = lookup_comic_from_locg(str(self.title))
|
||||
|
||||
if not data:
|
||||
logger.warn(
|
||||
f"Book not found on LOCG, checking OL {self.title}"
|
||||
)
|
||||
if self.openlibrary_id and force_update:
|
||||
data = lookup_book_from_openlibrary(
|
||||
str(self.openlibrary_id)
|
||||
)
|
||||
else:
|
||||
data = lookup_book_from_openlibrary(
|
||||
str(self.title), author_name
|
||||
)
|
||||
if not data:
|
||||
logger.warn(f"Book not found in OL {self.title}")
|
||||
return
|
||||
if not data and COMICVINE_API_KEY:
|
||||
logger.warn(f"Checking ComicVine for {self.title}")
|
||||
cv_client = ComicVineClient(api_key=COMICVINE_API_KEY)
|
||||
data = lookup_comic_from_comicvine(str(self.title))
|
||||
|
||||
if not data:
|
||||
logger.warn(f"Book not found in any sources: {self.title}")
|
||||
return
|
||||
|
||||
# We can discard the author name from OL for now, we'll lookup details below
|
||||
data.pop("ol_author_name", "")
|
||||
@ -174,15 +199,31 @@ class Book(LongPlayScrobblableMixin):
|
||||
self.get_author_from_locg(data.pop("locg_writer_slug", ""))
|
||||
|
||||
ol_title = data.get("title", "")
|
||||
data.pop("ol_author_id", "")
|
||||
|
||||
# Kick out a little warning if we're about to change KoReader's title
|
||||
if ol_title.lower() != str(self.title).lower():
|
||||
if (
|
||||
fuzz.ratio(ol_title.lower(), str(self.title).lower()) < 80
|
||||
and not force_update
|
||||
):
|
||||
logger.warn(
|
||||
f"OL and KoReader disagree on this book title {self.title} != {ol_title}"
|
||||
f"OL and KoReader disagree on this book title {self.title} != {ol_title}, check manually"
|
||||
)
|
||||
self.openlibrary_id = data.get("openlibrary_id")
|
||||
self.save(update_fields=["openlibrary_id"])
|
||||
return
|
||||
|
||||
# If we don't know pages, don't overwrite existing with None
|
||||
if data.get("pages") == None:
|
||||
if "pages" in data.keys() and data.get("pages") == None:
|
||||
data.pop("pages")
|
||||
|
||||
if (
|
||||
not isinstance(data.get("pages"), int)
|
||||
and "pages" in data.keys()
|
||||
):
|
||||
logger.info(
|
||||
f"Pages for {self} from OL expected to be int, but got {data.get('pages')}"
|
||||
)
|
||||
data.pop("pages")
|
||||
|
||||
# Pop this, so we can look it up later
|
||||
@ -242,6 +283,27 @@ 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:
|
||||
scrobbles = self.scrobble_set.filter(user=user_id)
|
||||
|
||||
pages = {}
|
||||
for scrobble in scrobbles:
|
||||
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["end_ts"] = datetime.fromtimestamp(data["end_ts"])
|
||||
pages[page] = data
|
||||
sorted_pages = OrderedDict(
|
||||
sorted(pages.items(), key=lambda x: x[1]["start_ts"])
|
||||
)
|
||||
|
||||
return sorted_pages
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
return self.authors.first()
|
||||
@ -264,34 +326,35 @@ class Book(LongPlayScrobblableMixin):
|
||||
last_scrobble = get_scrobbles_for_media(self, user).last()
|
||||
progress = 0
|
||||
if last_scrobble:
|
||||
progress = int((last_scrobble.book_pages_read / self.pages) * 100)
|
||||
progress = int((last_scrobble.last_page_read / self.pages) * 100)
|
||||
return progress
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls, lookup_id: str, author: str = "") -> "Book":
|
||||
data = lookup_book_from_openlibrary(lookup_id, author)
|
||||
book = cls.objects.filter(openlibrary_id=lookup_id).first()
|
||||
|
||||
book, book_created = cls.objects.get_or_create(isbn=data["isbn"])
|
||||
if book_created:
|
||||
book.fix_metadata(data=data)
|
||||
if not book:
|
||||
data = lookup_book_from_openlibrary(lookup_id, author)
|
||||
|
||||
if not data:
|
||||
logger.error(
|
||||
f"No book found on openlibrary, or in our database for {lookup_id}"
|
||||
)
|
||||
return book
|
||||
|
||||
book, book_created = cls.objects.get_or_create(isbn=data["isbn"])
|
||||
if book_created:
|
||||
book.fix_metadata(data=data)
|
||||
|
||||
return book
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if (
|
||||
(not self.isbn and not self.cover)
|
||||
and (self.locg_slug or self.openlibrary_id)
|
||||
and self.id
|
||||
):
|
||||
self.fix_metadata(force_update=True)
|
||||
|
||||
return super(Book, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Page(TimeStampedModel):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
"""DEPRECATED, we need to migrate pages into page_data on scrobbles and move on"""
|
||||
|
||||
book = models.ForeignKey(Book, on_delete=models.CASCADE)
|
||||
number = models.IntegerField()
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
start_time = models.DateTimeField(**BNULL)
|
||||
end_time = models.DateTimeField(**BNULL)
|
||||
duration_seconds = models.IntegerField(**BNULL)
|
||||
|
||||
@ -5,6 +5,8 @@ import urllib
|
||||
|
||||
import requests
|
||||
|
||||
from thefuzz import fuzz
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ISBN_URL = "https://openlibrary.org/isbn/{isbn}.json"
|
||||
@ -36,8 +38,11 @@ def get_author_openlibrary_id(name: str) -> str:
|
||||
logger.warn(f"No author results found from search for {name}")
|
||||
return ""
|
||||
|
||||
result = results.get("docs", [])
|
||||
return result[0].get("key")
|
||||
try:
|
||||
result = results.get("docs", [])[0]
|
||||
except IndexError:
|
||||
result = {"key": ""}
|
||||
return result.get("key")
|
||||
|
||||
|
||||
def lookup_author_from_openlibrary(olid: str) -> dict:
|
||||
@ -99,11 +104,22 @@ def lookup_book_from_openlibrary(
|
||||
|
||||
top = None
|
||||
for result in results.get("docs"):
|
||||
# These Summary things suck and ruin our one-shot search
|
||||
if "Summary of" not in result.get("title"):
|
||||
if fuzz.ratio(title.lower(), result.get("title", "").lower()) > 90:
|
||||
top = result
|
||||
break
|
||||
|
||||
if not top:
|
||||
for result in results.get("docs"):
|
||||
# These Summary things suck and ruin our one-shot search
|
||||
if "Summary of" in result.get("title"):
|
||||
continue
|
||||
|
||||
if title.lower() in result.get("title", "").lower():
|
||||
top = result
|
||||
|
||||
if not top and len(results.get("docs")) > 0:
|
||||
top = results.get("docs")[0]
|
||||
|
||||
if not top:
|
||||
logger.warn(f"No book found for query {query}")
|
||||
return {}
|
||||
|
||||
123
vrobbler/apps/books/tests/conftest.py
Normal file
123
vrobbler/apps/books/tests/conftest.py
Normal file
@ -0,0 +1,123 @@
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from vrobbler.apps.books.koreader import KoReaderBookColumn
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
ordinal = lambda n: "%d%s" % (
|
||||
n,
|
||||
"tsnrhtdd"[(n // 10 % 10 != 1) * (n % 10 < 4) * n % 10 :: 4],
|
||||
)
|
||||
AVERAGE_PAGE_READING_SECONDS = 60
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
status_code = 200
|
||||
|
||||
def status_code(self):
|
||||
return self.status_code
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def valid_response():
|
||||
return DummyResponse()
|
||||
|
||||
|
||||
class KoReaderBookRows:
|
||||
id = 1
|
||||
DEFAULT_STR = "N/A"
|
||||
DEFAULT_INT = 0
|
||||
DEFAULT_TIME = 1703800469
|
||||
BOOK_ROWS = []
|
||||
PAGE_STATS_ROWS = []
|
||||
|
||||
def _gen_random_row(self, i):
|
||||
wiggle = random.randrange(15)
|
||||
title = f"Memoirs, Volume {i}"
|
||||
return [
|
||||
i,
|
||||
title,
|
||||
f"Lord Beaverbrook the {ordinal(i)}",
|
||||
self.DEFAULT_INT + wiggle / 10,
|
||||
self.DEFAULT_TIME + i * wiggle,
|
||||
0,
|
||||
300 + wiggle,
|
||||
self.DEFAULT_STR,
|
||||
self.DEFAULT_STR,
|
||||
hashlib.md5(title.encode()).hexdigest(),
|
||||
i * wiggle * 20,
|
||||
120,
|
||||
]
|
||||
|
||||
def _generate_random_book_rows(self, book_count):
|
||||
if book_count > 0:
|
||||
for i in range(1, book_count + 1):
|
||||
self.BOOK_ROWS.append(self._gen_random_row(i))
|
||||
|
||||
def _generate_custom_book_row(self, **kwargs):
|
||||
title = kwargs.get("title", self.DEFAULT_STR)
|
||||
if title and title != "N/A":
|
||||
self.BOOK_ROWS.append(
|
||||
[
|
||||
kwargs.get("id", self.id),
|
||||
kwargs.get("title", self.DEFAULT_STR),
|
||||
kwargs.get("authors", self.DEFAULT_STR),
|
||||
kwargs.get("notes", self.DEFAULT_INT),
|
||||
kwargs.get("last_open", self.DEFAULT_TIME),
|
||||
kwargs.get("highlights", self.DEFAULT_INT),
|
||||
kwargs.get("pages", self.DEFAULT_INT),
|
||||
kwargs.get("series", self.DEFAULT_STR),
|
||||
kwargs.get("language", self.DEFAULT_STR),
|
||||
hashlib.md5(title.encode()),
|
||||
kwargs.get("total_read_time", self.DEFAULT_INT),
|
||||
kwargs.get("total_read_pages", self.DEFAULT_INT),
|
||||
]
|
||||
)
|
||||
|
||||
def _generate_random_page_stats_rows(self):
|
||||
for book in self.BOOK_ROWS:
|
||||
pages = book[KoReaderBookColumn.PAGES.value]
|
||||
pages_per_session = 20
|
||||
|
||||
start_time = book[KoReaderBookColumn.LAST_OPEN.value]
|
||||
end_session = False
|
||||
for page_num in range(
|
||||
1, book[KoReaderBookColumn.TOTAL_READ_PAGES.value] + 1
|
||||
):
|
||||
wiggle = random.randrange(5)
|
||||
self.PAGE_STATS_ROWS.append(
|
||||
[
|
||||
book[KoReaderBookColumn.ID.value],
|
||||
page_num,
|
||||
start_time,
|
||||
AVERAGE_PAGE_READING_SECONDS + wiggle,
|
||||
pages,
|
||||
]
|
||||
)
|
||||
if end_session:
|
||||
start_time += 3600 # one second over an hour, marking a new reading session
|
||||
end_session = False
|
||||
else:
|
||||
start_time += AVERAGE_PAGE_READING_SECONDS
|
||||
|
||||
if page_num % pages_per_session == 0:
|
||||
end_session = True
|
||||
|
||||
def __init__(self, book_count=0, **kwargs):
|
||||
self._generate_random_book_rows(book_count)
|
||||
self._generate_custom_book_row(**kwargs)
|
||||
self._generate_random_page_stats_rows()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def koreader_rows():
|
||||
return KoReaderBookRows(book_count=1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def demo_user():
|
||||
return User.objects.create(email="demo@example.com")
|
||||
54
vrobbler/apps/books/tests/test_koreader.py
Normal file
54
vrobbler/apps/books/tests/test_koreader.py
Normal file
@ -0,0 +1,54 @@
|
||||
import pytest
|
||||
from unittest import mock
|
||||
|
||||
from books.koreader import (
|
||||
KoReaderBookColumn,
|
||||
build_book_map,
|
||||
build_page_data,
|
||||
build_scrobbles_from_book_map,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch("requests.get")
|
||||
def test_build_book_map(get_mock, koreader_rows, valid_response):
|
||||
get_mock.return_value = valid_response
|
||||
book_map = build_book_map(koreader_rows.BOOK_ROWS)
|
||||
assert len(book_map) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@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_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][
|
||||
KoReaderBookColumn.TOTAL_READ_PAGES.value
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch("requests.get")
|
||||
def test_build_scrobbles_from_pages(
|
||||
get_mock, koreader_rows, demo_user, 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)
|
||||
|
||||
scrobbles = build_scrobbles_from_book_map(book_map, demo_user)
|
||||
# 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].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
|
||||
36
vrobbler/apps/books/tests/test_openlibrary.py
Normal file
36
vrobbler/apps/books/tests/test_openlibrary.py
Normal file
@ -0,0 +1,36 @@
|
||||
from unittest import skip
|
||||
|
||||
import pytest
|
||||
|
||||
from books.openlibrary import lookup_book_from_openlibrary
|
||||
|
||||
|
||||
def test_lookup_modern_book():
|
||||
book = lookup_book_from_openlibrary("Matrix", "Lauren Groff")
|
||||
assert book.get("title") == "Matrix"
|
||||
assert book.get("openlibrary_id") == "OL32170218M"
|
||||
assert book.get("ol_author_id") == "OL3675729A"
|
||||
|
||||
|
||||
def test_lookup_classic_book():
|
||||
book = lookup_book_from_openlibrary(
|
||||
"The Life of Castruccio Castracani", "Machiavelli"
|
||||
)
|
||||
assert book.get("title") == "The Life of Castruccio Castracani of Lucca"
|
||||
assert book.get("openlibrary_id") == "OL8950869M"
|
||||
assert book.get("ol_author_id") == "OL23135A"
|
||||
|
||||
|
||||
def test_lookup_foreign_book():
|
||||
book = lookup_book_from_openlibrary("Ravagé", "René Barjavel")
|
||||
assert book.get("title") == "Ravage"
|
||||
assert book.get("openlibrary_id") == "OL8837839M"
|
||||
assert book.get("ol_author_id") == "OL152472A"
|
||||
|
||||
|
||||
@skip("This is rotten in OL, updated but waiting for it to update")
|
||||
def test_lookup_book():
|
||||
book = lookup_book_from_openlibrary("Hark! A Vagrant")
|
||||
assert book.get("title") == "Hark! A Vagrant"
|
||||
assert book.get("openlibrary_id") == "OL8837839M"
|
||||
assert book.get("ol_author_id") == "OL152472A"
|
||||
@ -1,15 +1,15 @@
|
||||
from django.views import generic
|
||||
from books.models import Book, Author
|
||||
|
||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
||||
|
||||
class BookListView(generic.ListView):
|
||||
|
||||
class BookListView(ScrobbleableListView):
|
||||
model = Book
|
||||
paginate_by = 20
|
||||
|
||||
|
||||
class BookDetailView(generic.DetailView):
|
||||
class BookDetailView(ScrobbleableDetailView):
|
||||
model = Book
|
||||
slug_field = "uuid"
|
||||
|
||||
|
||||
class AuthorDetailView(generic.DetailView):
|
||||
|
||||
0
vrobbler/apps/bricksets/__init__.py
Normal file
0
vrobbler/apps/bricksets/__init__.py
Normal file
23
vrobbler/apps/bricksets/admin.py
Normal file
23
vrobbler/apps/bricksets/admin.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from bricksets.models import BrickSet
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
class BrickSetInline(admin.TabularInline):
|
||||
model = BrickSet
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(BrickSet)
|
||||
class BrickSetAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"uuid",
|
||||
"title",
|
||||
)
|
||||
ordering = ("-created",)
|
||||
search_fields = ("title",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
5
vrobbler/apps/bricksets/apps.py
Normal file
5
vrobbler/apps/bricksets/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BrickSetsConfig(AppConfig):
|
||||
name = "bricksets"
|
||||
106
vrobbler/apps/bricksets/migrations/0001_initial.py
Normal file
106
vrobbler/apps/bricksets/migrations/0001_initial.py
Normal file
@ -0,0 +1,106 @@
|
||||
# Generated by Django 4.2.15 on 2024-09-07 05:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_extensions.db.fields
|
||||
import taggit.managers
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0059_remove_scrobble_book_koreader_hash_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="BrickSet",
|
||||
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),
|
||||
),
|
||||
(
|
||||
"number",
|
||||
models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
("release_year", models.IntegerField(blank=True, null=True)),
|
||||
("piece_count", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
"brickset_rating",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=1, max_digits=3, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"lego_item_number",
|
||||
models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
(
|
||||
"box_image",
|
||||
models.ImageField(
|
||||
blank=True, null=True, upload_to="brickset/boxes/"
|
||||
),
|
||||
),
|
||||
(
|
||||
"set_image",
|
||||
models.ImageField(
|
||||
blank=True, null=True, upload_to="brickset/sets/"
|
||||
),
|
||||
),
|
||||
(
|
||||
"genre",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="scrobbles.ObjectWithGenres",
|
||||
to="scrobbles.Genre",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/bricksets/migrations/__init__.py
Normal file
0
vrobbler/apps/bricksets/migrations/__init__.py
Normal file
63
vrobbler/apps/bricksets/models.py
Normal file
63
vrobbler/apps/bricksets/models.py
Normal file
@ -0,0 +1,63 @@
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import ResizeToFit
|
||||
from scrobbles.dataclasses import BrickSetLogData
|
||||
from scrobbles.mixins import LongPlayScrobblableMixin
|
||||
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
|
||||
class BrickSet(LongPlayScrobblableMixin):
|
||||
""""""
|
||||
|
||||
number = models.CharField(max_length=10, **BNULL)
|
||||
release_year = models.IntegerField(**BNULL)
|
||||
piece_count = models.IntegerField(**BNULL)
|
||||
brickset_rating = models.DecimalField(max_digits=3, decimal_places=1, **BNULL)
|
||||
lego_item_number = models.CharField(max_length=10, **BNULL)
|
||||
box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
|
||||
box_image_small = ImageSpecField(
|
||||
source="box_image",
|
||||
processors=[ResizeToFit(100, 100)],
|
||||
format="JPEG",
|
||||
options={"quality": 60},
|
||||
)
|
||||
box_image_medium = ImageSpecField(
|
||||
source="box_image",
|
||||
processors=[ResizeToFit(300, 300)],
|
||||
format="JPEG",
|
||||
options={"quality": 75},
|
||||
)
|
||||
set_image = models.ImageField(upload_to="brickset/sets/", **BNULL)
|
||||
set_image_small = ImageSpecField(
|
||||
source="set_image",
|
||||
processors=[ResizeToFit(100, 100)],
|
||||
format="JPEG",
|
||||
options={"quality": 60},
|
||||
)
|
||||
set_image_medium = ImageSpecField(
|
||||
source="set_image",
|
||||
processors=[ResizeToFit(300, 300)],
|
||||
format="JPEG",
|
||||
options={"quality": 75},
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("bricksets:brickset_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
return BrickSetLogData
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls, title: str) -> "BrickSet":
|
||||
return cls.objects.filter(title=title).first()
|
||||
|
||||
@property
|
||||
def primary_image_url(self) -> str:
|
||||
if self.box_image:
|
||||
return self.box_image.url
|
||||
return ""
|
||||
14
vrobbler/apps/bricksets/urls.py
Normal file
14
vrobbler/apps/bricksets/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django.urls import path
|
||||
from bricksets import views
|
||||
|
||||
app_name = "bricksets"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("bricksets/", views.BrickSetListView.as_view(), name="brickset_list"),
|
||||
path(
|
||||
"bricksets/<slug:slug>/",
|
||||
views.BrickSetDetailView.as_view(),
|
||||
name="brickset_detail",
|
||||
),
|
||||
]
|
||||
10
vrobbler/apps/bricksets/views.py
Normal file
10
vrobbler/apps/bricksets/views.py
Normal file
@ -0,0 +1,10 @@
|
||||
from bricksets.models import BrickSet
|
||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
||||
|
||||
|
||||
class BrickSetListView(ScrobbleableListView):
|
||||
model = BrickSet
|
||||
|
||||
|
||||
class BrickSetDetailView(ScrobbleableDetailView):
|
||||
model = BrickSet
|
||||
16
vrobbler/apps/lifeevents/admin.py
Normal file
16
vrobbler/apps/lifeevents/admin.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from lifeevents.models import LifeEvent
|
||||
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
@admin.register(LifeEvent)
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = ("title",)
|
||||
search_fields = ("title",)
|
||||
ordering = ("-created",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
79
vrobbler/apps/lifeevents/migrations/0001_initial.py
Normal file
79
vrobbler/apps/lifeevents/migrations/0001_initial.py
Normal file
@ -0,0 +1,79 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-07 13:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_extensions.db.fields
|
||||
import taggit.managers
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0055_rename_scrobble_log_scrobble_log"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LifeEvent",
|
||||
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)),
|
||||
(
|
||||
"genre",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="scrobbles.ObjectWithGenres",
|
||||
to="scrobbles.Genre",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/lifeevents/migrations/__init__.py
Normal file
0
vrobbler/apps/lifeevents/migrations/__init__.py
Normal file
33
vrobbler/apps/lifeevents/models.py
Normal file
33
vrobbler/apps/lifeevents/models.py
Normal file
@ -0,0 +1,33 @@
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from scrobbles.dataclasses import LifeEventLogData
|
||||
from scrobbles.mixins import ScrobblableMixin
|
||||
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
|
||||
class LifeEvent(ScrobblableMixin):
|
||||
description = models.TextField(**BNULL)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"life-events:life-event_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
return LifeEventLogData
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls, title: str) -> "LifeEvent":
|
||||
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")
|
||||
16
vrobbler/apps/lifeevents/urls.py
Normal file
16
vrobbler/apps/lifeevents/urls.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.urls import path
|
||||
from lifeevents import views
|
||||
|
||||
app_name = "lifeevents"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"lifeevents/", views.LifeEventListView.as_view(), name="lifeevent_list"
|
||||
),
|
||||
path(
|
||||
"lifeevent/<slug:slug>/",
|
||||
views.LifeEventDetailView.as_view(),
|
||||
name="life-event_detail",
|
||||
),
|
||||
]
|
||||
12
vrobbler/apps/lifeevents/views.py
Normal file
12
vrobbler/apps/lifeevents/views.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.views import generic
|
||||
from lifeevents.models import LifeEvent
|
||||
|
||||
|
||||
class LifeEventListView(generic.ListView):
|
||||
model = LifeEvent
|
||||
paginate_by = 20
|
||||
|
||||
|
||||
class LifeEventDetailView(generic.DetailView):
|
||||
model = LifeEvent
|
||||
slug_field = "uuid"
|
||||
@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from locations.models import GeoLocation, RawGeoLocation
|
||||
from locations.models import GeoLocation
|
||||
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
@ -19,18 +19,3 @@ class GeoLocationAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(RawGeoLocation)
|
||||
class RawGeoLocationAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"lat",
|
||||
"lon",
|
||||
"altitude",
|
||||
"speed",
|
||||
)
|
||||
ordering = (
|
||||
"lat",
|
||||
"lon",
|
||||
)
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-20 00:20
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("locations", "0005_alter_rawgeolocation_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="RawGeoLocation",
|
||||
),
|
||||
]
|
||||
@ -1,3 +1,4 @@
|
||||
from decimal import Decimal, getcontext
|
||||
import logging
|
||||
from typing import Dict
|
||||
from uuid import uuid4
|
||||
@ -13,11 +14,13 @@ logger = logging.getLogger(__name__)
|
||||
BNULL = {"blank": True, "null": True}
|
||||
User = get_user_model()
|
||||
|
||||
GEOLOC_ACCURACY = int(getattr(settings, "GEOLOC_ACCURACY", 4))
|
||||
GEOLOC_PROXIMITY = Decimal(getattr(settings, "GEOLOC_PROXIMITY", "0.0001"))
|
||||
|
||||
|
||||
class GeoLocation(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, "LOCATION_COMPLETION_PERCENT", 100)
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
lat = models.FloatField()
|
||||
lon = models.FloatField()
|
||||
truncated_lat = models.FloatField(**BNULL)
|
||||
@ -53,11 +56,11 @@ class GeoLocation(ScrobblableMixin):
|
||||
int_lon, r_lon = str(data_dict.get("lon", "")).split(".")
|
||||
|
||||
try:
|
||||
trunc_lat = r_lat[0:4]
|
||||
trunc_lat = r_lat[0:GEOLOC_ACCURACY]
|
||||
except IndexError:
|
||||
trunc_lat = r_lat
|
||||
try:
|
||||
trunc_lon = r_lon[0:4]
|
||||
trunc_lon = r_lon[0:GEOLOC_ACCURACY]
|
||||
except IndexError:
|
||||
trunc_lon = r_lon
|
||||
|
||||
@ -65,17 +68,12 @@ class GeoLocation(ScrobblableMixin):
|
||||
data_dict["lon"] = float(f"{int_lon}.{trunc_lon}")
|
||||
|
||||
int_alt, r_alt = str(data_dict.get("alt", "")).split(".")
|
||||
try:
|
||||
trunc_alt = r_lon[0:4]
|
||||
except IndexError:
|
||||
trunc_alt = r_alt
|
||||
|
||||
data_dict["altitude"] = float(f"{int_alt}.{trunc_alt}")
|
||||
data_dict["altitude"] = float(int_alt)
|
||||
|
||||
location = cls.objects.filter(
|
||||
lat=data_dict.get("lat"),
|
||||
lon=data_dict.get("lon"),
|
||||
altitude=data_dict.get("altitude"),
|
||||
).first()
|
||||
|
||||
if not location:
|
||||
@ -86,11 +84,49 @@ class GeoLocation(ScrobblableMixin):
|
||||
)
|
||||
return location
|
||||
|
||||
@property
|
||||
def subtitle(self) -> str:
|
||||
if self.title:
|
||||
return f"{self.lat} x {self.lon}"
|
||||
return ""
|
||||
|
||||
class RawGeoLocation(TimeStampedModel):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
lat = models.FloatField()
|
||||
lon = models.FloatField()
|
||||
altitude = models.FloatField(**BNULL)
|
||||
speed = models.FloatField(**BNULL)
|
||||
timestamp = models.DateTimeField(**BNULL)
|
||||
def loc_diff(self, old_lat_lon: tuple) -> tuple:
|
||||
return (
|
||||
abs(Decimal(old_lat_lon[0]) - Decimal(self.lat)),
|
||||
abs(Decimal(old_lat_lon[1]) - Decimal(self.lon)),
|
||||
)
|
||||
|
||||
def has_moved(self, previous_location: "GeoLocation") -> bool:
|
||||
has_moved = False
|
||||
|
||||
loc_diff = self.loc_diff(
|
||||
(previous_location.lat, previous_location.lon)
|
||||
)
|
||||
if loc_diff[0] > GEOLOC_PROXIMITY or loc_diff[1] > GEOLOC_PROXIMITY:
|
||||
has_moved = True
|
||||
logger.debug(
|
||||
f"[locations] checked whether location has moved against proximity setting",
|
||||
extra={
|
||||
"location_id": self.id,
|
||||
"loc_diff": loc_diff,
|
||||
"has_moved": has_moved,
|
||||
"previous_location_id": previous_location.id,
|
||||
"geoloc_proximity": GEOLOC_PROXIMITY,
|
||||
},
|
||||
)
|
||||
return has_moved
|
||||
|
||||
def in_proximity(self, named=False) -> models.QuerySet:
|
||||
lat_min = Decimal(self.lat) - GEOLOC_PROXIMITY
|
||||
lat_max = Decimal(self.lat) + GEOLOC_PROXIMITY
|
||||
lon_min = Decimal(self.lon) - GEOLOC_PROXIMITY
|
||||
lon_max = Decimal(self.lon) + GEOLOC_PROXIMITY
|
||||
is_title_null = not named
|
||||
close_locations = GeoLocation.objects.filter(
|
||||
title__isnull=is_title_null,
|
||||
lat__lte=lat_max,
|
||||
lat__gte=lat_min,
|
||||
lon__lte=lon_max,
|
||||
lon__gte=lon_min,
|
||||
).exclude(id=self.id)
|
||||
return close_locations
|
||||
|
||||
90
vrobbler/apps/locations/tests/test_models.py
Normal file
90
vrobbler/apps/locations/tests/test_models.py
Normal file
@ -0,0 +1,90 @@
|
||||
import pytest
|
||||
import logging
|
||||
|
||||
from locations.models import GeoLocation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_find_or_create(caplog):
|
||||
assert not GeoLocation.find_or_create({})
|
||||
assert "No lat or lon keys in data dict" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_find_or_create_truncation():
|
||||
loc = GeoLocation.find_or_create(
|
||||
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
|
||||
)
|
||||
assert loc.lat == 44.234
|
||||
assert loc.lon == -68.234
|
||||
assert loc.altitude == 60
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_find_or_create_finds_existing():
|
||||
extant = GeoLocation.objects.create(lat=44.234, lon=-68.234, altitude=50)
|
||||
|
||||
loc = GeoLocation.find_or_create(
|
||||
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
|
||||
)
|
||||
assert loc.id == extant.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_find_or_create_creates_new():
|
||||
extant = GeoLocation.objects.create(lat=44.234, lon=-69.234, altitude=60)
|
||||
|
||||
loc = GeoLocation.find_or_create(
|
||||
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
|
||||
)
|
||||
assert not loc.id == extant.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_found_in_proximity_location():
|
||||
lat = 44.234
|
||||
lon = -69.234
|
||||
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
|
||||
|
||||
close = GeoLocation.objects.create(
|
||||
lat=lat + 0.0001, lon=lon - 0.0001, altitude=60
|
||||
)
|
||||
assert close not in loc.in_proximity(named=True)
|
||||
assert close in loc.in_proximity()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_found_in_proximity_location():
|
||||
lat = 44.234
|
||||
lon = -69.234
|
||||
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
|
||||
|
||||
far = GeoLocation.objects.create(
|
||||
lat=lat + 0.0002, lon=lon - 0.0001, altitude=60
|
||||
)
|
||||
assert far not in loc.in_proximity()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_has_moved(caplog):
|
||||
lat = 44.234
|
||||
lon = -69.234
|
||||
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
|
||||
|
||||
past = GeoLocation.objects.get_or_create(
|
||||
lat=lat + 0.0009, lon=lon - 0.002, altitude=60
|
||||
)[0]
|
||||
assert loc.has_moved(past)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_has_not_moved():
|
||||
lat = 44.234
|
||||
lon = -69.234
|
||||
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
|
||||
|
||||
past = GeoLocation.objects.get_or_create(
|
||||
lat=lat + 0.00009, lon=lon - 0.00009, altitude=60
|
||||
)[0]
|
||||
assert not loc.has_moved(past)
|
||||
0
vrobbler/apps/moods/__init__.py
Normal file
0
vrobbler/apps/moods/__init__.py
Normal file
23
vrobbler/apps/moods/admin.py
Normal file
23
vrobbler/apps/moods/admin.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from moods.models import Mood
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
class MoodInline(admin.TabularInline):
|
||||
model = Mood
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Mood)
|
||||
class MoodAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"uuid",
|
||||
"title",
|
||||
)
|
||||
ordering = ("-created",)
|
||||
search_fields = ("title",)
|
||||
inlines = [
|
||||
ScrobbleInline,
|
||||
]
|
||||
5
vrobbler/apps/moods/apps.py
Normal file
5
vrobbler/apps/moods/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MoodsConfig(AppConfig):
|
||||
name = "moods"
|
||||
1
vrobbler/apps/moods/fixtures/moods.json
Normal file
1
vrobbler/apps/moods/fixtures/moods.json
Normal file
File diff suppressed because one or more lines are too long
79
vrobbler/apps/moods/migrations/0001_initial.py
Normal file
79
vrobbler/apps/moods/migrations/0001_initial.py
Normal file
@ -0,0 +1,79 @@
|
||||
# Generated by Django 4.2.13 on 2024-08-10 20:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import django_extensions.db.fields
|
||||
import taggit.managers
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Mood",
|
||||
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)),
|
||||
(
|
||||
"genre",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="scrobbles.ObjectWithGenres",
|
||||
to="scrobbles.Genre",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
18
vrobbler/apps/moods/migrations/0002_mood_image.py
Normal file
18
vrobbler/apps/moods/migrations/0002_mood_image.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.13 on 2024-08-10 20:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("moods", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="mood",
|
||||
name="image",
|
||||
field=models.ImageField(blank=True, null=True, upload_to="moods/"),
|
||||
),
|
||||
]
|
||||
0
vrobbler/apps/moods/migrations/__init__.py
Normal file
0
vrobbler/apps/moods/migrations/__init__.py
Normal file
56
vrobbler/apps/moods/models.py
Normal file
56
vrobbler/apps/moods/models.py
Normal file
@ -0,0 +1,56 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import ResizeToFit
|
||||
from scrobbles.mixins import ScrobblableMixin
|
||||
|
||||
from vrobbler.apps.scrobbles.dataclasses import MoodLogData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
BNULL = {"blank": True, "null": True}
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Mood(ScrobblableMixin):
|
||||
description = models.TextField(**BNULL)
|
||||
image = models.ImageField(upload_to="moods/", **BNULL)
|
||||
image_small = ImageSpecField(
|
||||
source="thumbnail",
|
||||
processors=[ResizeToFit(100, 100)],
|
||||
format="JPEG",
|
||||
options={"quality": 60},
|
||||
)
|
||||
image_medium = ImageSpecField(
|
||||
source="thumbnail",
|
||||
processors=[ResizeToFit(300, 300)],
|
||||
format="JPEG",
|
||||
options={"quality": 75},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.title:
|
||||
return self.title
|
||||
return str(self.uuid)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("moods:mood-detail", kwargs={"slug": self.uuid})
|
||||
|
||||
def get_start_url(self):
|
||||
return reverse("scrobbles:start", kwargs={"uuid": self.uuid})
|
||||
|
||||
@property
|
||||
def subtitle(self) -> str:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
return MoodLogData
|
||||
|
||||
@property
|
||||
def primary_image_url(self) -> str:
|
||||
if self.image:
|
||||
return self.image.url
|
||||
return ""
|
||||
14
vrobbler/apps/moods/urls.py
Normal file
14
vrobbler/apps/moods/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
from django.urls import path
|
||||
from moods import views
|
||||
|
||||
app_name = "moods"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("moods/", views.MoodListView.as_view(), name="mood-list"),
|
||||
path(
|
||||
"moods/<slug:slug>/",
|
||||
views.MoodDetailView.as_view(),
|
||||
name="mood-detail",
|
||||
),
|
||||
]
|
||||
10
vrobbler/apps/moods/views.py
Normal file
10
vrobbler/apps/moods/views.py
Normal file
@ -0,0 +1,10 @@
|
||||
from moods.models import Mood
|
||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
||||
|
||||
|
||||
class MoodListView(ScrobbleableListView):
|
||||
model = Mood
|
||||
|
||||
|
||||
class MoodDetailView(ScrobbleableDetailView):
|
||||
model = Mood
|
||||
@ -1,3 +1,78 @@
|
||||
jellyfin_keys = [
|
||||
"ServerId",
|
||||
"ServerName",
|
||||
"ServerVersion",
|
||||
"ServerUrl",
|
||||
"NotificationType",
|
||||
"Timestamp",
|
||||
"UtcTimestamp",
|
||||
"Name",
|
||||
"Overview",
|
||||
"Tagline",
|
||||
"ItemId",
|
||||
"RunTimeTicks",
|
||||
"RunTime",
|
||||
"Year",
|
||||
"Provider_tmdb",
|
||||
"Provider_imdb",
|
||||
"Provider_tmdbcollection",
|
||||
"Provider_tmdbset",
|
||||
"Video_0_Title",
|
||||
"Video_0_Type",
|
||||
"Video_0_Codec",
|
||||
"Video_0_Profile",
|
||||
"Video_0_Level",
|
||||
"Video_0_Height",
|
||||
"Video_0_Width",
|
||||
"Video_0_AspectRatio",
|
||||
"Video_0_Interlaced",
|
||||
"Video_0_FrameRate",
|
||||
"Video_0_VideoRange",
|
||||
"Video_0_ColorSpace",
|
||||
"Video_0_ColorTransfer",
|
||||
"Video_0_ColorPrimaries",
|
||||
"Video_0_PixelFormat",
|
||||
"Video_0_RefFrames",
|
||||
"Audio_0_Title",
|
||||
"Audio_0_Type",
|
||||
"Audio_0_Language",
|
||||
"Audio_0_Codec",
|
||||
"Audio_0_Channels",
|
||||
"Audio_0_Bitrate",
|
||||
"Audio_0_SampleRate",
|
||||
"Audio_0_Default",
|
||||
"Subtitle_0_Title",
|
||||
"Subtitle_0_Type",
|
||||
"Subtitle_0_Language",
|
||||
"Subtitle_0_Codec",
|
||||
"Subtitle_0_Default",
|
||||
"Subtitle_0_Forced",
|
||||
"Subtitle_0_External",
|
||||
"Subtitle_1_Title",
|
||||
"Subtitle_1_Type",
|
||||
"Subtitle_1_Language",
|
||||
"Subtitle_1_Codec",
|
||||
"Subtitle_1_Default",
|
||||
"Subtitle_1_Forced",
|
||||
"Subtitle_1_External",
|
||||
"Subtitle_2_Title",
|
||||
"Subtitle_2_Type",
|
||||
"Subtitle_2_Language",
|
||||
"Subtitle_2_Codec",
|
||||
"Subtitle_2_Default",
|
||||
"Subtitle_2_Forced",
|
||||
"Subtitle_2_External",
|
||||
"PlaybackPositionTicks",
|
||||
"PlaybackPosition",
|
||||
"MediaSourceId",
|
||||
"IsPaused",
|
||||
"IsAutomated",
|
||||
"DeviceId",
|
||||
"DeviceName",
|
||||
"ClientName",
|
||||
"NotificationUsername",
|
||||
"UserId",
|
||||
]
|
||||
VARIOUS_ARTIST_DICT = {
|
||||
"name": "Various Artists",
|
||||
"theaudiodb_id": "113641",
|
||||
@ -6,10 +81,13 @@ VARIOUS_ARTIST_DICT = {
|
||||
|
||||
JELLYFIN_POST_KEYS = {
|
||||
"ITEM_TYPE": "ItemType",
|
||||
"ITEM_ID": "ItemId",
|
||||
"RUN_TIME": "RunTime",
|
||||
"TITLE": "Name",
|
||||
"TRACK_TITLE": "Name",
|
||||
"TIMESTAMP": "UtcTimestamp",
|
||||
"YEAR": "Year",
|
||||
"OVERVIEW": "Overview",
|
||||
"TAGLINE": "TAGLINE",
|
||||
"PLAYBACK_POSITION_TICKS": "PlaybackPositionTicks",
|
||||
"PLAYBACK_POSITION": "PlaybackPosition",
|
||||
"ARTIST_MB_ID": "Provider_musicbrainzartist",
|
||||
@ -18,4 +96,25 @@ JELLYFIN_POST_KEYS = {
|
||||
"TRACK_MB_ID": "Provider_musicbrainztrack",
|
||||
"ALBUM_NAME": "Album",
|
||||
"ARTIST_NAME": "Artist",
|
||||
"STATUS": "Status",
|
||||
"SOURCE": "ClientName",
|
||||
"VIDEO_TITLE": "Name",
|
||||
"IMDB_ID": "Provider_imdb",
|
||||
"TMDB_ID": "Provider_tmdb",
|
||||
}
|
||||
|
||||
MOPIDY_POST_KEYS = {
|
||||
"ITEM_TYPE": None,
|
||||
"RUN_TIME": "run_time",
|
||||
"TRACK_TITLE": "name",
|
||||
"TIMESTAMP": None,
|
||||
"YEAR": None,
|
||||
"PLAYBACK_POSITION_TICKS": "playback_time_ticks",
|
||||
"ARTIST_MB_ID": "muscibrainz_artist_id",
|
||||
"ALBUM_MB_ID": "musicbrainz_album_id",
|
||||
"RELEASEGROUP_MB_ID": None,
|
||||
"TRACK_MB_ID": "musicbrainz_track_id",
|
||||
"ALBUM_NAME": "album",
|
||||
"ARTIST_NAME": "artist",
|
||||
"STATUS": "status",
|
||||
}
|
||||
|
||||
@ -46,7 +46,6 @@ class LastFM:
|
||||
|
||||
new_scrobbles = []
|
||||
source = "Last.fm"
|
||||
source_id = ""
|
||||
lastfm_scrobbles = self.get_last_scrobbles(time_from=last_processed)
|
||||
|
||||
for lfm_scrobble in lastfm_scrobbles:
|
||||
@ -60,12 +59,16 @@ class LastFM:
|
||||
lfm_scrobble["album"] = album
|
||||
track = get_or_create_track(**lfm_scrobble)
|
||||
|
||||
timezone = settings.TIME_ZONE
|
||||
if self.vrobbler_user.profile:
|
||||
timezone = self.vrobbler_user.profile.timezone
|
||||
|
||||
new_scrobble = Scrobble(
|
||||
user=self.vrobbler_user,
|
||||
timestamp=timestamp,
|
||||
source=source,
|
||||
source_id=source_id,
|
||||
track=track,
|
||||
timezone=timezone,
|
||||
played_to_completion=True,
|
||||
in_progress=False,
|
||||
media_type=Scrobble.MediaType.TRACK,
|
||||
|
||||
@ -404,7 +404,7 @@ class Album(TimeStampedModel):
|
||||
|
||||
|
||||
class Track(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 90)
|
||||
COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 100)
|
||||
|
||||
class Opinion(models.IntegerChoices):
|
||||
DOWN = -1, "Thumbs down"
|
||||
|
||||
@ -104,19 +104,37 @@ def lookup_artist_from_mb(artist_name: str) -> dict:
|
||||
|
||||
|
||||
def lookup_track_from_mb(
|
||||
track_name: str, artist_mbid: str, album_mbid: str
|
||||
) -> str:
|
||||
track_name: str, artist_mb_id: str, album_mb_id: str
|
||||
) -> dict:
|
||||
logger.info(
|
||||
"[lookup_track_from_mb] called",
|
||||
extra={
|
||||
"track_name": track_name,
|
||||
"artist_mb_id": artist_mb_id,
|
||||
"album_mb_id": album_mb_id,
|
||||
},
|
||||
)
|
||||
musicbrainzngs.set_useragent("vrobbler", "0.3.0")
|
||||
|
||||
top_result = musicbrainzngs.search_recordings(
|
||||
query=track_name, artist=artist_mbid, release=album_mbid
|
||||
)["recording-list"][0]
|
||||
try:
|
||||
results = musicbrainzngs.search_recordings(
|
||||
query=track_name, artist=artist_mb_id, release=album_mb_id
|
||||
)["recording-list"]
|
||||
logger.info(
|
||||
"[lookup_track_from_mb] musicbrainz recordings search results",
|
||||
extra={"results": results},
|
||||
)
|
||||
top_result = results[0]
|
||||
except IndexError:
|
||||
logger.error("[lookup_track_from_mb] no results found")
|
||||
return {}
|
||||
|
||||
score = int(top_result.get("ext:score"))
|
||||
if score < 85:
|
||||
logger.debug(
|
||||
"Track lookup score below 85 threshold",
|
||||
extra={"result": top_result},
|
||||
logger.info(
|
||||
"[lookup_track_from_mb] no results above 85% certainty ",
|
||||
extra={"results": results},
|
||||
)
|
||||
return ""
|
||||
return {}
|
||||
|
||||
return top_result
|
||||
|
||||
@ -8,6 +8,7 @@ from music.musicbrainz import (
|
||||
lookup_track_from_mb,
|
||||
)
|
||||
from music.constants import VARIOUS_ARTIST_DICT
|
||||
from scrobbles.utils import convert_to_seconds
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -89,32 +90,55 @@ def get_or_create_album(
|
||||
return album
|
||||
|
||||
|
||||
def get_or_create_track(
|
||||
title: str,
|
||||
artist: Artist,
|
||||
album: Album = None,
|
||||
mbid: str = None,
|
||||
run_time_seconds=None,
|
||||
) -> Track:
|
||||
track = None
|
||||
if not mbid and album:
|
||||
mbid = lookup_track_from_mb(
|
||||
title,
|
||||
artist.musicbrainz_id,
|
||||
album.musicbrainz_id,
|
||||
)["id"]
|
||||
def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
|
||||
try:
|
||||
track_run_time_seconds = int(
|
||||
post_data.get(post_keys.get("RUN_TIME"), 0)
|
||||
)
|
||||
except ValueError: # Sometimes we get run time as a string like "01:35"
|
||||
track_run_time_seconds = convert_to_seconds(
|
||||
post_data.get(post_keys.get("RUN_TIME"), 0)
|
||||
)
|
||||
|
||||
track = Track.objects.filter(musicbrainz_id=mbid).first()
|
||||
artist_name = post_data.get(post_keys.get("ARTIST_NAME"), "")
|
||||
artist_mb_id = post_data.get(post_keys.get("ARTIST_MB_ID"), "")
|
||||
album_title = post_data.get(post_keys.get("ALBUM_NAME"), "")
|
||||
album_mb_id = post_data.get(post_keys.get("ALBUM_MB_ID"), "")
|
||||
track_title = post_data.get(post_keys.get("TRACK_TITLE"), "")
|
||||
track_mb_id = post_data.get(post_keys.get("TRACK_MB_ID"), "")
|
||||
|
||||
artist = get_or_create_artist(
|
||||
artist_name,
|
||||
mbid=artist_mb_id,
|
||||
)
|
||||
album = get_or_create_album(
|
||||
album_title,
|
||||
artist=artist,
|
||||
mbid=album_mb_id,
|
||||
)
|
||||
|
||||
track = None
|
||||
if not track_mb_id and album:
|
||||
try:
|
||||
track_mb_id = lookup_track_from_mb(
|
||||
track_title,
|
||||
artist.musicbrainz_id,
|
||||
album.musicbrainz_id,
|
||||
).get("id", 0)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if track_mb_id:
|
||||
track = Track.objects.filter(musicbrainz_id=track_mb_id).first()
|
||||
|
||||
if not track:
|
||||
track = Track.objects.create(
|
||||
title=title,
|
||||
title=track_title,
|
||||
artist=artist,
|
||||
album=album,
|
||||
musicbrainz_id=mbid,
|
||||
run_time_seconds=run_time_seconds,
|
||||
musicbrainz_id=track_mb_id,
|
||||
run_time_seconds=track_run_time_seconds,
|
||||
)
|
||||
|
||||
return track
|
||||
|
||||
|
||||
|
||||
@ -4,20 +4,15 @@ from music.models import Album, Artist, Track
|
||||
from scrobbles.models import ChartRecord
|
||||
from scrobbles.stats import get_scrobble_count_qs
|
||||
|
||||
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
|
||||
|
||||
class TrackListView(generic.ListView):
|
||||
|
||||
class TrackListView(ScrobbleableListView):
|
||||
model = Track
|
||||
paginate_by = 200
|
||||
|
||||
def get_queryset(self):
|
||||
return get_scrobble_count_qs(user=self.request.user).order_by(
|
||||
"-scrobble_count"
|
||||
)
|
||||
|
||||
|
||||
class TrackDetailView(generic.DetailView):
|
||||
class TrackDetailView(ScrobbleableDetailView):
|
||||
model = Track
|
||||
slug_field = "uuid"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from podcasts.models import Episode, Podcast, Producer
|
||||
from podcasts.models import PodcastEpisode, Podcast, Producer
|
||||
from scrobbles.admin import ScrobbleInline
|
||||
|
||||
|
||||
@ -21,8 +21,8 @@ class PodcastAdmin(admin.ModelAdmin):
|
||||
ordering = ("name",)
|
||||
|
||||
|
||||
@admin.register(Episode)
|
||||
class EpisodeAdmin(admin.ModelAdmin):
|
||||
@admin.register(PodcastEpisode)
|
||||
class PodcastEpisodeAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "created"
|
||||
list_display = (
|
||||
"title",
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-10 18:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0048_alter_scrobble_media_type"),
|
||||
("podcasts", "0012_alter_episode_genre"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="Episode",
|
||||
new_name="PodcastEpisode",
|
||||
),
|
||||
]
|
||||
@ -76,7 +76,7 @@ class Podcast(TimeStampedModel):
|
||||
self.cover_image.save(fname, ContentFile(r.content), save=True)
|
||||
|
||||
|
||||
class Episode(ScrobblableMixin):
|
||||
class PodcastEpisode(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90)
|
||||
|
||||
podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)
|
||||
|
||||
79
vrobbler/apps/podcasts/utils.py
Normal file
79
vrobbler/apps/podcasts/utils.py
Normal file
@ -0,0 +1,79 @@
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import unquote
|
||||
|
||||
from dateutil.parser import ParserError, parse
|
||||
from podcasts.models import PodcastEpisode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO This should be configurable in settings or per deploy
|
||||
PODCAST_DATE_FORMAT = "YYYY-MM-DD"
|
||||
|
||||
|
||||
def parse_mopidy_uri(uri: str) -> dict:
|
||||
logger.debug(f"Parsing URI: {uri}")
|
||||
parsed_uri = os.path.splitext(unquote(uri))[0].split("/")
|
||||
|
||||
episode_str = parsed_uri[-1]
|
||||
podcast_name = parsed_uri[-2].strip()
|
||||
episode_num = None
|
||||
episode_num_pad = 0
|
||||
|
||||
try:
|
||||
# Without episode numbers the date will lead
|
||||
pub_date = parse(episode_str[0:10])
|
||||
except ParserError:
|
||||
episode_num = int(episode_str.split("-")[0])
|
||||
episode_num_pad = len(str(episode_num)) + 1
|
||||
|
||||
try:
|
||||
# Beacuse we have epsiode numbers on
|
||||
pub_date = parse(
|
||||
episode_str[
|
||||
episode_num_pad : len(PODCAST_DATE_FORMAT)
|
||||
+ episode_num_pad
|
||||
]
|
||||
)
|
||||
except ParserError:
|
||||
pub_date = ""
|
||||
|
||||
gap_to_strip = 0
|
||||
if pub_date:
|
||||
gap_to_strip += len(PODCAST_DATE_FORMAT)
|
||||
if episode_num:
|
||||
gap_to_strip += episode_num_pad
|
||||
|
||||
episode_name = episode_str[gap_to_strip:].replace("-", " ").strip()
|
||||
|
||||
return {
|
||||
"episode_filename": episode_name,
|
||||
"episode_num": episode_num,
|
||||
"podcast_name": podcast_name,
|
||||
"pub_date": pub_date,
|
||||
}
|
||||
|
||||
|
||||
def get_or_create_podcast(post_data: dict) -> PodcastEpisode:
|
||||
mopidy_uri = post_data.get("mopidy_uri", "")
|
||||
parsed_data = parse_mopidy_uri(mopidy_uri)
|
||||
|
||||
producer_dict = {"name": post_data.get("artist")}
|
||||
|
||||
podcast_name = post_data.get("album")
|
||||
if not podcast_name:
|
||||
podcast_name = parsed_data.get("podcast_name")
|
||||
podcast_dict = {"name": podcast_name}
|
||||
|
||||
episode_name = parsed_data.get("episode_filename")
|
||||
episode_dict = {
|
||||
"title": episode_name,
|
||||
"run_time_seconds": post_data.get("run_time"),
|
||||
"number": parsed_data.get("episode_num"),
|
||||
"pub_date": parsed_data.get("pub_date"),
|
||||
"mopidy_uri": mopidy_uri,
|
||||
}
|
||||
|
||||
return PodcastEpisode.find_or_create(
|
||||
podcast_dict, producer_dict, episode_dict
|
||||
)
|
||||
@ -11,4 +11,5 @@ class UserProfileAdmin(admin.ModelAdmin):
|
||||
"twitch_token",
|
||||
"twitch_client_secret",
|
||||
"lastfm_password",
|
||||
"archivebox_password",
|
||||
)
|
||||
|
||||
8
vrobbler/apps/profiles/apps.py
Normal file
8
vrobbler/apps/profiles/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProfilesConfig(AppConfig):
|
||||
name = "profiles"
|
||||
|
||||
def ready(self):
|
||||
import profiles.signals
|
||||
@ -0,0 +1,601 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-27 04:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0010_userprofile_redirect_to_webpage"),
|
||||
]
|
||||
|
||||
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"),
|
||||
("America/Adak", "(GMT-1000) America/Adak"),
|
||||
("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/Anchorage", "(GMT-0900) America/Anchorage"),
|
||||
("America/Juneau", "(GMT-0900) America/Juneau"),
|
||||
("America/Metlakatla", "(GMT-0900) America/Metlakatla"),
|
||||
("America/Nome", "(GMT-0900) America/Nome"),
|
||||
("America/Sitka", "(GMT-0900) America/Sitka"),
|
||||
("America/Yakutat", "(GMT-0900) America/Yakutat"),
|
||||
("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"),
|
||||
("US/Alaska", "(GMT-0900) US/Alaska"),
|
||||
("America/Los_Angeles", "(GMT-0800) America/Los_Angeles"),
|
||||
("America/Tijuana", "(GMT-0800) America/Tijuana"),
|
||||
("America/Vancouver", "(GMT-0800) America/Vancouver"),
|
||||
("Canada/Pacific", "(GMT-0800) Canada/Pacific"),
|
||||
("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"),
|
||||
("US/Pacific", "(GMT-0800) US/Pacific"),
|
||||
("America/Boise", "(GMT-0700) America/Boise"),
|
||||
(
|
||||
"America/Cambridge_Bay",
|
||||
"(GMT-0700) America/Cambridge_Bay",
|
||||
),
|
||||
(
|
||||
"America/Ciudad_Juarez",
|
||||
"(GMT-0700) America/Ciudad_Juarez",
|
||||
),
|
||||
("America/Creston", "(GMT-0700) America/Creston"),
|
||||
("America/Dawson", "(GMT-0700) America/Dawson"),
|
||||
(
|
||||
"America/Dawson_Creek",
|
||||
"(GMT-0700) America/Dawson_Creek",
|
||||
),
|
||||
("America/Denver", "(GMT-0700) America/Denver"),
|
||||
("America/Edmonton", "(GMT-0700) America/Edmonton"),
|
||||
("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"),
|
||||
("America/Hermosillo", "(GMT-0700) America/Hermosillo"),
|
||||
("America/Inuvik", "(GMT-0700) America/Inuvik"),
|
||||
("America/Mazatlan", "(GMT-0700) America/Mazatlan"),
|
||||
("America/Phoenix", "(GMT-0700) America/Phoenix"),
|
||||
("America/Whitehorse", "(GMT-0700) America/Whitehorse"),
|
||||
("America/Yellowknife", "(GMT-0700) America/Yellowknife"),
|
||||
("Canada/Mountain", "(GMT-0700) Canada/Mountain"),
|
||||
("US/Arizona", "(GMT-0700) US/Arizona"),
|
||||
("US/Mountain", "(GMT-0700) US/Mountain"),
|
||||
(
|
||||
"America/Bahia_Banderas",
|
||||
"(GMT-0600) America/Bahia_Banderas",
|
||||
),
|
||||
("America/Belize", "(GMT-0600) America/Belize"),
|
||||
("America/Chicago", "(GMT-0600) America/Chicago"),
|
||||
("America/Chihuahua", "(GMT-0600) America/Chihuahua"),
|
||||
("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"),
|
||||
("America/El_Salvador", "(GMT-0600) America/El_Salvador"),
|
||||
("America/Guatemala", "(GMT-0600) America/Guatemala"),
|
||||
(
|
||||
"America/Indiana/Knox",
|
||||
"(GMT-0600) America/Indiana/Knox",
|
||||
),
|
||||
(
|
||||
"America/Indiana/Tell_City",
|
||||
"(GMT-0600) America/Indiana/Tell_City",
|
||||
),
|
||||
("America/Managua", "(GMT-0600) America/Managua"),
|
||||
("America/Matamoros", "(GMT-0600) America/Matamoros"),
|
||||
("America/Menominee", "(GMT-0600) America/Menominee"),
|
||||
("America/Merida", "(GMT-0600) America/Merida"),
|
||||
("America/Mexico_City", "(GMT-0600) America/Mexico_City"),
|
||||
("America/Monterrey", "(GMT-0600) America/Monterrey"),
|
||||
(
|
||||
"America/North_Dakota/Beulah",
|
||||
"(GMT-0600) America/North_Dakota/Beulah",
|
||||
),
|
||||
(
|
||||
"America/North_Dakota/Center",
|
||||
"(GMT-0600) America/North_Dakota/Center",
|
||||
),
|
||||
(
|
||||
"America/North_Dakota/New_Salem",
|
||||
"(GMT-0600) America/North_Dakota/New_Salem",
|
||||
),
|
||||
("America/Ojinaga", "(GMT-0600) America/Ojinaga"),
|
||||
(
|
||||
"America/Rankin_Inlet",
|
||||
"(GMT-0600) America/Rankin_Inlet",
|
||||
),
|
||||
("America/Regina", "(GMT-0600) America/Regina"),
|
||||
("America/Resolute", "(GMT-0600) America/Resolute"),
|
||||
(
|
||||
"America/Swift_Current",
|
||||
"(GMT-0600) America/Swift_Current",
|
||||
),
|
||||
("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"),
|
||||
("America/Winnipeg", "(GMT-0600) America/Winnipeg"),
|
||||
("Canada/Central", "(GMT-0600) Canada/Central"),
|
||||
("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"),
|
||||
("US/Central", "(GMT-0600) US/Central"),
|
||||
("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/Detroit", "(GMT-0500) America/Detroit"),
|
||||
("America/Eirunepe", "(GMT-0500) America/Eirunepe"),
|
||||
("America/Grand_Turk", "(GMT-0500) America/Grand_Turk"),
|
||||
("America/Guayaquil", "(GMT-0500) America/Guayaquil"),
|
||||
("America/Havana", "(GMT-0500) America/Havana"),
|
||||
(
|
||||
"America/Indiana/Indianapolis",
|
||||
"(GMT-0500) America/Indiana/Indianapolis",
|
||||
),
|
||||
(
|
||||
"America/Indiana/Marengo",
|
||||
"(GMT-0500) America/Indiana/Marengo",
|
||||
),
|
||||
(
|
||||
"America/Indiana/Petersburg",
|
||||
"(GMT-0500) America/Indiana/Petersburg",
|
||||
),
|
||||
(
|
||||
"America/Indiana/Vevay",
|
||||
"(GMT-0500) America/Indiana/Vevay",
|
||||
),
|
||||
(
|
||||
"America/Indiana/Vincennes",
|
||||
"(GMT-0500) America/Indiana/Vincennes",
|
||||
),
|
||||
(
|
||||
"America/Indiana/Winamac",
|
||||
"(GMT-0500) America/Indiana/Winamac",
|
||||
),
|
||||
("America/Iqaluit", "(GMT-0500) America/Iqaluit"),
|
||||
("America/Jamaica", "(GMT-0500) America/Jamaica"),
|
||||
(
|
||||
"America/Kentucky/Louisville",
|
||||
"(GMT-0500) America/Kentucky/Louisville",
|
||||
),
|
||||
(
|
||||
"America/Kentucky/Monticello",
|
||||
"(GMT-0500) America/Kentucky/Monticello",
|
||||
),
|
||||
("America/Lima", "(GMT-0500) America/Lima"),
|
||||
("America/Nassau", "(GMT-0500) America/Nassau"),
|
||||
("America/New_York", "(GMT-0500) America/New_York"),
|
||||
("America/Panama", "(GMT-0500) America/Panama"),
|
||||
(
|
||||
"America/Port-au-Prince",
|
||||
"(GMT-0500) America/Port-au-Prince",
|
||||
),
|
||||
("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"),
|
||||
("America/Toronto", "(GMT-0500) America/Toronto"),
|
||||
("Canada/Eastern", "(GMT-0500) Canada/Eastern"),
|
||||
("Pacific/Easter", "(GMT-0500) Pacific/Easter"),
|
||||
("US/Eastern", "(GMT-0500) US/Eastern"),
|
||||
("America/Anguilla", "(GMT-0400) America/Anguilla"),
|
||||
("America/Antigua", "(GMT-0400) America/Antigua"),
|
||||
("America/Aruba", "(GMT-0400) America/Aruba"),
|
||||
("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/Dominica", "(GMT-0400) America/Dominica"),
|
||||
("America/Glace_Bay", "(GMT-0400) America/Glace_Bay"),
|
||||
("America/Goose_Bay", "(GMT-0400) America/Goose_Bay"),
|
||||
("America/Grenada", "(GMT-0400) America/Grenada"),
|
||||
("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"),
|
||||
("America/Guyana", "(GMT-0400) America/Guyana"),
|
||||
("America/Halifax", "(GMT-0400) America/Halifax"),
|
||||
("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/Moncton", "(GMT-0400) America/Moncton"),
|
||||
("America/Montserrat", "(GMT-0400) America/Montserrat"),
|
||||
(
|
||||
"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/Thule", "(GMT-0400) America/Thule"),
|
||||
("America/Tortola", "(GMT-0400) America/Tortola"),
|
||||
("Atlantic/Bermuda", "(GMT-0400) Atlantic/Bermuda"),
|
||||
("Canada/Atlantic", "(GMT-0400) Canada/Atlantic"),
|
||||
("America/St_Johns", "(GMT-0330) America/St_Johns"),
|
||||
("Canada/Newfoundland", "(GMT-0330) Canada/Newfoundland"),
|
||||
("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/Asuncion", "(GMT-0300) America/Asuncion"),
|
||||
("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/Maceio", "(GMT-0300) America/Maceio"),
|
||||
("America/Miquelon", "(GMT-0300) America/Miquelon"),
|
||||
("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"),
|
||||
("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"),
|
||||
("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"),
|
||||
("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"),
|
||||
("America/Noronha", "(GMT-0200) America/Noronha"),
|
||||
("America/Nuuk", "(GMT-0200) America/Nuuk"),
|
||||
(
|
||||
"Atlantic/South_Georgia",
|
||||
"(GMT-0200) Atlantic/South_Georgia",
|
||||
),
|
||||
(
|
||||
"America/Scoresbysund",
|
||||
"(GMT-0100) America/Scoresbysund",
|
||||
),
|
||||
("Atlantic/Azores", "(GMT-0100) Atlantic/Azores"),
|
||||
("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",
|
||||
),
|
||||
("Antarctica/Troll", "(GMT+0000) Antarctica/Troll"),
|
||||
("Atlantic/Canary", "(GMT+0000) Atlantic/Canary"),
|
||||
("Atlantic/Faroe", "(GMT+0000) Atlantic/Faroe"),
|
||||
("Atlantic/Madeira", "(GMT+0000) Atlantic/Madeira"),
|
||||
("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"),
|
||||
("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"),
|
||||
("Europe/Dublin", "(GMT+0000) Europe/Dublin"),
|
||||
("Europe/Guernsey", "(GMT+0000) Europe/Guernsey"),
|
||||
("Europe/Isle_of_Man", "(GMT+0000) Europe/Isle_of_Man"),
|
||||
("Europe/Jersey", "(GMT+0000) Europe/Jersey"),
|
||||
("Europe/Lisbon", "(GMT+0000) Europe/Lisbon"),
|
||||
("Europe/London", "(GMT+0000) Europe/London"),
|
||||
("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/Ceuta", "(GMT+0100) Africa/Ceuta"),
|
||||
("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"),
|
||||
("Arctic/Longyearbyen", "(GMT+0100) Arctic/Longyearbyen"),
|
||||
("Europe/Amsterdam", "(GMT+0100) Europe/Amsterdam"),
|
||||
("Europe/Andorra", "(GMT+0100) Europe/Andorra"),
|
||||
("Europe/Belgrade", "(GMT+0100) Europe/Belgrade"),
|
||||
("Europe/Berlin", "(GMT+0100) Europe/Berlin"),
|
||||
("Europe/Bratislava", "(GMT+0100) Europe/Bratislava"),
|
||||
("Europe/Brussels", "(GMT+0100) Europe/Brussels"),
|
||||
("Europe/Budapest", "(GMT+0100) Europe/Budapest"),
|
||||
("Europe/Busingen", "(GMT+0100) Europe/Busingen"),
|
||||
("Europe/Copenhagen", "(GMT+0100) Europe/Copenhagen"),
|
||||
("Europe/Gibraltar", "(GMT+0100) Europe/Gibraltar"),
|
||||
("Europe/Ljubljana", "(GMT+0100) Europe/Ljubljana"),
|
||||
("Europe/Luxembourg", "(GMT+0100) Europe/Luxembourg"),
|
||||
("Europe/Madrid", "(GMT+0100) Europe/Madrid"),
|
||||
("Europe/Malta", "(GMT+0100) Europe/Malta"),
|
||||
("Europe/Monaco", "(GMT+0100) Europe/Monaco"),
|
||||
("Europe/Oslo", "(GMT+0100) Europe/Oslo"),
|
||||
("Europe/Paris", "(GMT+0100) Europe/Paris"),
|
||||
("Europe/Podgorica", "(GMT+0100) Europe/Podgorica"),
|
||||
("Europe/Prague", "(GMT+0100) Europe/Prague"),
|
||||
("Europe/Rome", "(GMT+0100) Europe/Rome"),
|
||||
("Europe/San_Marino", "(GMT+0100) Europe/San_Marino"),
|
||||
("Europe/Sarajevo", "(GMT+0100) Europe/Sarajevo"),
|
||||
("Europe/Skopje", "(GMT+0100) Europe/Skopje"),
|
||||
("Europe/Stockholm", "(GMT+0100) Europe/Stockholm"),
|
||||
("Europe/Tirane", "(GMT+0100) Europe/Tirane"),
|
||||
("Europe/Vaduz", "(GMT+0100) Europe/Vaduz"),
|
||||
("Europe/Vatican", "(GMT+0100) Europe/Vatican"),
|
||||
("Europe/Vienna", "(GMT+0100) Europe/Vienna"),
|
||||
("Europe/Warsaw", "(GMT+0100) Europe/Warsaw"),
|
||||
("Europe/Zagreb", "(GMT+0100) Europe/Zagreb"),
|
||||
("Europe/Zurich", "(GMT+0100) Europe/Zurich"),
|
||||
("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"),
|
||||
("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"),
|
||||
("Africa/Cairo", "(GMT+0200) Africa/Cairo"),
|
||||
("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"),
|
||||
("Asia/Beirut", "(GMT+0200) Asia/Beirut"),
|
||||
("Asia/Famagusta", "(GMT+0200) Asia/Famagusta"),
|
||||
("Asia/Gaza", "(GMT+0200) Asia/Gaza"),
|
||||
("Asia/Hebron", "(GMT+0200) Asia/Hebron"),
|
||||
("Asia/Jerusalem", "(GMT+0200) Asia/Jerusalem"),
|
||||
("Asia/Nicosia", "(GMT+0200) Asia/Nicosia"),
|
||||
("Europe/Athens", "(GMT+0200) Europe/Athens"),
|
||||
("Europe/Bucharest", "(GMT+0200) Europe/Bucharest"),
|
||||
("Europe/Chisinau", "(GMT+0200) Europe/Chisinau"),
|
||||
("Europe/Helsinki", "(GMT+0200) Europe/Helsinki"),
|
||||
("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"),
|
||||
("Europe/Kyiv", "(GMT+0200) Europe/Kyiv"),
|
||||
("Europe/Mariehamn", "(GMT+0200) Europe/Mariehamn"),
|
||||
("Europe/Riga", "(GMT+0200) Europe/Riga"),
|
||||
("Europe/Sofia", "(GMT+0200) Europe/Sofia"),
|
||||
("Europe/Tallinn", "(GMT+0200) Europe/Tallinn"),
|
||||
("Europe/Vilnius", "(GMT+0200) Europe/Vilnius"),
|
||||
("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/Damascus", "(GMT+0300) Asia/Damascus"),
|
||||
("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"),
|
||||
("Asia/Qatar", "(GMT+0300) Asia/Qatar"),
|
||||
("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"),
|
||||
("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"),
|
||||
("Europe/Kirov", "(GMT+0300) Europe/Kirov"),
|
||||
("Europe/Minsk", "(GMT+0300) Europe/Minsk"),
|
||||
("Europe/Moscow", "(GMT+0300) Europe/Moscow"),
|
||||
("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"),
|
||||
("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/Darwin", "(GMT+0930) Australia/Darwin"),
|
||||
(
|
||||
"Antarctica/DumontDUrville",
|
||||
"(GMT+1000) Antarctica/DumontDUrville",
|
||||
),
|
||||
("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"),
|
||||
("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"),
|
||||
("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"),
|
||||
("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"),
|
||||
("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/Adelaide", "(GMT+1030) Australia/Adelaide"),
|
||||
(
|
||||
"Australia/Broken_Hill",
|
||||
"(GMT+1030) Australia/Broken_Hill",
|
||||
),
|
||||
("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"),
|
||||
(
|
||||
"Antarctica/Macquarie",
|
||||
"(GMT+1100) Antarctica/Macquarie",
|
||||
),
|
||||
("Asia/Magadan", "(GMT+1100) Asia/Magadan"),
|
||||
("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"),
|
||||
("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"),
|
||||
("Australia/Hobart", "(GMT+1100) Australia/Hobart"),
|
||||
("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"),
|
||||
("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"),
|
||||
("Australia/Sydney", "(GMT+1100) Australia/Sydney"),
|
||||
(
|
||||
"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/Noumea", "(GMT+1100) Pacific/Noumea"),
|
||||
("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"),
|
||||
("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"),
|
||||
("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"),
|
||||
("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/Norfolk", "(GMT+1200) Pacific/Norfolk"),
|
||||
("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"),
|
||||
("Pacific/Wake", "(GMT+1200) Pacific/Wake"),
|
||||
("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"),
|
||||
("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"),
|
||||
("Pacific/Apia", "(GMT+1300) Pacific/Apia"),
|
||||
("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"),
|
||||
("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"),
|
||||
("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"),
|
||||
("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"),
|
||||
("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"),
|
||||
("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"),
|
||||
],
|
||||
default="UTC",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,601 @@
|
||||
# Generated by Django 4.2.9 on 2024-03-27 01:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0011_alter_userprofile_timezone"),
|
||||
]
|
||||
|
||||
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",
|
||||
),
|
||||
(
|
||||
"America/Scoresbysund",
|
||||
"(GMT-0100) America/Scoresbysund",
|
||||
),
|
||||
("Atlantic/Azores", "(GMT-0100) Atlantic/Azores"),
|
||||
("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/Casablanca", "(GMT+0000) Africa/Casablanca"),
|
||||
("Africa/Conakry", "(GMT+0000) Africa/Conakry"),
|
||||
("Africa/Dakar", "(GMT+0000) Africa/Dakar"),
|
||||
("Africa/El_Aaiun", "(GMT+0000) Africa/El_Aaiun"),
|
||||
("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",
|
||||
),
|
||||
("Antarctica/Troll", "(GMT+0000) Antarctica/Troll"),
|
||||
("Atlantic/Canary", "(GMT+0000) Atlantic/Canary"),
|
||||
("Atlantic/Faroe", "(GMT+0000) Atlantic/Faroe"),
|
||||
("Atlantic/Madeira", "(GMT+0000) Atlantic/Madeira"),
|
||||
("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"),
|
||||
("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"),
|
||||
("Europe/Dublin", "(GMT+0000) Europe/Dublin"),
|
||||
("Europe/Guernsey", "(GMT+0000) Europe/Guernsey"),
|
||||
("Europe/Isle_of_Man", "(GMT+0000) Europe/Isle_of_Man"),
|
||||
("Europe/Jersey", "(GMT+0000) Europe/Jersey"),
|
||||
("Europe/Lisbon", "(GMT+0000) Europe/Lisbon"),
|
||||
("Europe/London", "(GMT+0000) Europe/London"),
|
||||
("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/Ceuta", "(GMT+0100) Africa/Ceuta"),
|
||||
("Africa/Douala", "(GMT+0100) Africa/Douala"),
|
||||
("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"),
|
||||
("Arctic/Longyearbyen", "(GMT+0100) Arctic/Longyearbyen"),
|
||||
("Europe/Amsterdam", "(GMT+0100) Europe/Amsterdam"),
|
||||
("Europe/Andorra", "(GMT+0100) Europe/Andorra"),
|
||||
("Europe/Belgrade", "(GMT+0100) Europe/Belgrade"),
|
||||
("Europe/Berlin", "(GMT+0100) Europe/Berlin"),
|
||||
("Europe/Bratislava", "(GMT+0100) Europe/Bratislava"),
|
||||
("Europe/Brussels", "(GMT+0100) Europe/Brussels"),
|
||||
("Europe/Budapest", "(GMT+0100) Europe/Budapest"),
|
||||
("Europe/Busingen", "(GMT+0100) Europe/Busingen"),
|
||||
("Europe/Copenhagen", "(GMT+0100) Europe/Copenhagen"),
|
||||
("Europe/Gibraltar", "(GMT+0100) Europe/Gibraltar"),
|
||||
("Europe/Ljubljana", "(GMT+0100) Europe/Ljubljana"),
|
||||
("Europe/Luxembourg", "(GMT+0100) Europe/Luxembourg"),
|
||||
("Europe/Madrid", "(GMT+0100) Europe/Madrid"),
|
||||
("Europe/Malta", "(GMT+0100) Europe/Malta"),
|
||||
("Europe/Monaco", "(GMT+0100) Europe/Monaco"),
|
||||
("Europe/Oslo", "(GMT+0100) Europe/Oslo"),
|
||||
("Europe/Paris", "(GMT+0100) Europe/Paris"),
|
||||
("Europe/Podgorica", "(GMT+0100) Europe/Podgorica"),
|
||||
("Europe/Prague", "(GMT+0100) Europe/Prague"),
|
||||
("Europe/Rome", "(GMT+0100) Europe/Rome"),
|
||||
("Europe/San_Marino", "(GMT+0100) Europe/San_Marino"),
|
||||
("Europe/Sarajevo", "(GMT+0100) Europe/Sarajevo"),
|
||||
("Europe/Skopje", "(GMT+0100) Europe/Skopje"),
|
||||
("Europe/Stockholm", "(GMT+0100) Europe/Stockholm"),
|
||||
("Europe/Tirane", "(GMT+0100) Europe/Tirane"),
|
||||
("Europe/Vaduz", "(GMT+0100) Europe/Vaduz"),
|
||||
("Europe/Vatican", "(GMT+0100) Europe/Vatican"),
|
||||
("Europe/Vienna", "(GMT+0100) Europe/Vienna"),
|
||||
("Europe/Warsaw", "(GMT+0100) Europe/Warsaw"),
|
||||
("Europe/Zagreb", "(GMT+0100) Europe/Zagreb"),
|
||||
("Europe/Zurich", "(GMT+0100) Europe/Zurich"),
|
||||
("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"),
|
||||
("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"),
|
||||
("Africa/Cairo", "(GMT+0200) Africa/Cairo"),
|
||||
("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"),
|
||||
("Asia/Beirut", "(GMT+0200) Asia/Beirut"),
|
||||
("Asia/Famagusta", "(GMT+0200) Asia/Famagusta"),
|
||||
("Asia/Gaza", "(GMT+0200) Asia/Gaza"),
|
||||
("Asia/Hebron", "(GMT+0200) Asia/Hebron"),
|
||||
("Asia/Jerusalem", "(GMT+0200) Asia/Jerusalem"),
|
||||
("Asia/Nicosia", "(GMT+0200) Asia/Nicosia"),
|
||||
("Europe/Athens", "(GMT+0200) Europe/Athens"),
|
||||
("Europe/Bucharest", "(GMT+0200) Europe/Bucharest"),
|
||||
("Europe/Chisinau", "(GMT+0200) Europe/Chisinau"),
|
||||
("Europe/Helsinki", "(GMT+0200) Europe/Helsinki"),
|
||||
("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"),
|
||||
("Europe/Kyiv", "(GMT+0200) Europe/Kyiv"),
|
||||
("Europe/Mariehamn", "(GMT+0200) Europe/Mariehamn"),
|
||||
("Europe/Riga", "(GMT+0200) Europe/Riga"),
|
||||
("Europe/Sofia", "(GMT+0200) Europe/Sofia"),
|
||||
("Europe/Tallinn", "(GMT+0200) Europe/Tallinn"),
|
||||
("Europe/Vilnius", "(GMT+0200) Europe/Vilnius"),
|
||||
("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/Damascus", "(GMT+0300) Asia/Damascus"),
|
||||
("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"),
|
||||
("Asia/Qatar", "(GMT+0300) Asia/Qatar"),
|
||||
("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"),
|
||||
("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"),
|
||||
("Europe/Kirov", "(GMT+0300) Europe/Kirov"),
|
||||
("Europe/Minsk", "(GMT+0300) Europe/Minsk"),
|
||||
("Europe/Moscow", "(GMT+0300) Europe/Moscow"),
|
||||
("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"),
|
||||
("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/Darwin", "(GMT+0930) Australia/Darwin"),
|
||||
(
|
||||
"Antarctica/DumontDUrville",
|
||||
"(GMT+1000) Antarctica/DumontDUrville",
|
||||
),
|
||||
("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"),
|
||||
("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"),
|
||||
("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"),
|
||||
("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"),
|
||||
("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/Adelaide", "(GMT+1030) Australia/Adelaide"),
|
||||
(
|
||||
"Australia/Broken_Hill",
|
||||
"(GMT+1030) Australia/Broken_Hill",
|
||||
),
|
||||
("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"),
|
||||
(
|
||||
"Antarctica/Macquarie",
|
||||
"(GMT+1100) Antarctica/Macquarie",
|
||||
),
|
||||
("Asia/Magadan", "(GMT+1100) Asia/Magadan"),
|
||||
("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"),
|
||||
("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"),
|
||||
("Australia/Hobart", "(GMT+1100) Australia/Hobart"),
|
||||
("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"),
|
||||
("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"),
|
||||
("Australia/Sydney", "(GMT+1100) Australia/Sydney"),
|
||||
(
|
||||
"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/Noumea", "(GMT+1100) Pacific/Noumea"),
|
||||
("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"),
|
||||
("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"),
|
||||
("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"),
|
||||
("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/Norfolk", "(GMT+1200) Pacific/Norfolk"),
|
||||
("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"),
|
||||
("Pacific/Wake", "(GMT+1200) Pacific/Wake"),
|
||||
("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"),
|
||||
("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"),
|
||||
("Pacific/Apia", "(GMT+1300) Pacific/Apia"),
|
||||
("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"),
|
||||
("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"),
|
||||
("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"),
|
||||
("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"),
|
||||
("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"),
|
||||
("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"),
|
||||
],
|
||||
default="UTC",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,617 @@
|
||||
# Generated by Django 4.2.11 on 2024-04-13 02:03
|
||||
|
||||
from django.db import migrations, models
|
||||
import encrypted_field.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0012_alter_userprofile_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="archivebox_password",
|
||||
field=encrypted_field.fields.EncryptedField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="archivebox_url",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="archivebox_username",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
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/Easter", "(GMT-0600) Pacific/Easter"),
|
||||
("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"),
|
||||
("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/Santiago", "(GMT-0400) America/Santiago"),
|
||||
(
|
||||
"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/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/Casablanca", "(GMT+0000) Africa/Casablanca"),
|
||||
("Africa/Conakry", "(GMT+0000) Africa/Conakry"),
|
||||
("Africa/Dakar", "(GMT+0000) Africa/Dakar"),
|
||||
("Africa/El_Aaiun", "(GMT+0000) Africa/El_Aaiun"),
|
||||
("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/Douala", "(GMT+0100) Africa/Douala"),
|
||||
("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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,601 @@
|
||||
# Generated by Django 4.2.11 on 2024-04-19 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0013_userprofile_archivebox_password_and_more"),
|
||||
]
|
||||
|
||||
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/Easter", "(GMT-0600) Pacific/Easter"),
|
||||
("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"),
|
||||
("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/Santiago", "(GMT-0400) America/Santiago"),
|
||||
(
|
||||
"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/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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.13 on 2024-08-06 15:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0014_alter_userprofile_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="bgg_username",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -17,9 +17,7 @@ class UserProfile(TimeStampedModel):
|
||||
User, on_delete=models.CASCADE, related_name="profile"
|
||||
)
|
||||
timezone = models.CharField(
|
||||
max_length=255,
|
||||
choices=PRETTY_TIMEZONE_CHOICES,
|
||||
**BNULL,
|
||||
max_length=255, choices=PRETTY_TIMEZONE_CHOICES, default="UTC"
|
||||
)
|
||||
lastfm_username = models.CharField(max_length=255, **BNULL)
|
||||
lastfm_password = EncryptedField(**BNULL)
|
||||
@ -28,6 +26,12 @@ class UserProfile(TimeStampedModel):
|
||||
retroarch_path = models.CharField(max_length=255, **BNULL)
|
||||
retroarch_auto_import = models.BooleanField(default=False)
|
||||
|
||||
archivebox_username = models.CharField(max_length=255, **BNULL)
|
||||
archivebox_password = EncryptedField(**BNULL)
|
||||
archivebox_url = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
bgg_username = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
redirect_to_webpage = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@ -7,7 +7,6 @@ from scrobbles.models import (
|
||||
LastFmImport,
|
||||
RetroarchImport,
|
||||
Scrobble,
|
||||
ScrobbledPage,
|
||||
)
|
||||
from scrobbles.mixins import Genre
|
||||
|
||||
@ -24,10 +23,17 @@ class ScrobbleInline(admin.TabularInline):
|
||||
"sport_event",
|
||||
"board_game",
|
||||
"geo_location",
|
||||
"webpage",
|
||||
"trail",
|
||||
"web_page",
|
||||
"life_event",
|
||||
"user",
|
||||
)
|
||||
exclude = ("source_id", "scrobble_log")
|
||||
exclude = (
|
||||
"scrobble_log",
|
||||
"timezone",
|
||||
"videogame_save_data",
|
||||
"screenshot",
|
||||
)
|
||||
|
||||
|
||||
class ImportBaseAdmin(admin.ModelAdmin):
|
||||
@ -90,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",
|
||||
@ -108,6 +114,10 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
"sport_event",
|
||||
"book",
|
||||
"video_game",
|
||||
"board_game",
|
||||
"geo_location",
|
||||
"web_page",
|
||||
"life_event",
|
||||
)
|
||||
list_filter = (
|
||||
"is_paused",
|
||||
@ -123,13 +133,3 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
|
||||
def playback_percent(self, obj):
|
||||
return obj.percent_played
|
||||
|
||||
|
||||
@admin.register(ScrobbledPage)
|
||||
class ScrobbledPageAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"number",
|
||||
"scrobble",
|
||||
"notes",
|
||||
)
|
||||
raw_id_fields = ("scrobble",)
|
||||
|
||||
@ -6,14 +6,20 @@ JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]
|
||||
LONG_PLAY_MEDIA = {
|
||||
"videogames": "VideoGame",
|
||||
"books": "Book",
|
||||
"bricksets": "BrickSet",
|
||||
}
|
||||
|
||||
PLAY_AGAIN_MEDIA = {
|
||||
"videogames": "VideoGame",
|
||||
"books": "Book",
|
||||
"boardgames": "BoardGame",
|
||||
"moods": "Mood",
|
||||
"bricksets": "BrickSet",
|
||||
}
|
||||
|
||||
MEDIA_END_PADDING_SECONDS = {
|
||||
"Video": 3600, # 60 min
|
||||
}
|
||||
|
||||
EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",)
|
||||
|
||||
|
||||
191
vrobbler/apps/scrobbles/dataclasses.py
Normal file
191
vrobbler/apps/scrobbles/dataclasses.py
Normal file
@ -0,0 +1,191 @@
|
||||
from functools import cached_property
|
||||
import inspect
|
||||
import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Optional
|
||||
|
||||
from dataclass_wizard import JSONWizard
|
||||
from django.contrib.auth import get_user_model
|
||||
from locations.models import GeoLocation
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ScrobbleLogDataEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
|
||||
|
||||
class ScrobbleLogDataDecoder(json.JSONDecoder):
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
|
||||
|
||||
class JSONDataclass(JSONWizard):
|
||||
@property
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
return json.dumps(self.asdict)
|
||||
|
||||
|
||||
class LongPlayLogData(JSONDataclass):
|
||||
serial_scrobble_id: Optional[int]
|
||||
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):
|
||||
user_id: Optional[int] = None
|
||||
name_str: str = ""
|
||||
bgg_username: str = ""
|
||||
color: Optional[str] = None
|
||||
character: Optional[str] = None
|
||||
team: Optional[str] = None
|
||||
score: Optional[int] = None
|
||||
win: Optional[bool] = None
|
||||
new: Optional[bool] = None
|
||||
|
||||
@property
|
||||
def user(self) -> Optional[User]:
|
||||
user = None
|
||||
if self.user_id:
|
||||
user = User.objects.filter(id=self.user_id).first()
|
||||
return user
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
name = self.name_str
|
||||
if self.user_id:
|
||||
name = self.user.first_name
|
||||
return name
|
||||
|
||||
def __str__(self) -> str:
|
||||
out = self.name
|
||||
if self.score:
|
||||
out += f" {self.score}"
|
||||
if self.color:
|
||||
out += f" ({self.color})"
|
||||
if self.win:
|
||||
out += f" [W]"
|
||||
return out
|
||||
|
||||
|
||||
@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
|
||||
difficulty: Optional[int] = None
|
||||
solo: Optional[bool] = None
|
||||
two_handed: Optional[bool] = None
|
||||
|
||||
@cached_property
|
||||
def geo_location(self) -> Optional[GeoLocation]:
|
||||
if self.geo_location_id:
|
||||
return GeoLocation.objects.filter(id=self.geo_location_id).first()
|
||||
|
||||
|
||||
@dataclass
|
||||
class BookPageLogData(JSONDataclass):
|
||||
page_number: Optional[int] = None
|
||||
end_ts: Optional[int] = None
|
||||
start_ts: Optional[int] = None
|
||||
duration: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BookLogData(LongPlayLogData):
|
||||
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(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 geo_location(self):
|
||||
return GeoLocation.objects.filter(id=self.geo_location_id).first()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MoodLogData(JSONDataclass):
|
||||
reasons: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoLogData(JSONDataclass):
|
||||
title: str
|
||||
video_type: str
|
||||
run_time_seconds: int
|
||||
kind: str
|
||||
year: Optional[int]
|
||||
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
|
||||
emulator: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
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
|
||||
@ -1,5 +1,5 @@
|
||||
from books.models import Book
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
from django.core.management.base import BaseCommand
|
||||
from scrobbles.models import ScrobbledPage
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--commit",
|
||||
action="store_true",
|
||||
help="Actually migrate data",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = True
|
||||
if options["commit"]:
|
||||
dry_run = False
|
||||
|
||||
scrobbles_with_logs = (
|
||||
Scrobble.objects.filter(scrobble_log__isnull=False)
|
||||
.exclude(scrobble_log="")
|
||||
.exclude(scrobble_log="\n")
|
||||
)
|
||||
updated_scrobble_count = 0
|
||||
|
||||
for scrobble in scrobbles_with_logs:
|
||||
if "\n" in scrobble.scrobble_log:
|
||||
lines = scrobble.scrobble_log.split("\n")
|
||||
else:
|
||||
lines = [scrobble.scrobble_log]
|
||||
|
||||
old_data = {}
|
||||
for line_num, line in enumerate(lines):
|
||||
if line_num == 0 or line == "":
|
||||
continue
|
||||
old_data[str(line_num)] = line
|
||||
|
||||
if old_data:
|
||||
scrobble.scrobble_log = {"legacy_data": old_data}
|
||||
updated_scrobble_count += 1
|
||||
|
||||
if not dry_run:
|
||||
scrobble.save(update_fields=["scrobble_log"])
|
||||
else:
|
||||
print(
|
||||
f"Scrobble {scrobble} scrobble_log updated to {old_data}"
|
||||
)
|
||||
|
||||
print(f"Migrated scrobble logs for {updated_scrobble_count} scrobbles")
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-22 00:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0045_scrobble_webpage_alter_scrobble_media_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="book_page_data",
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="chartrecord",
|
||||
name="year",
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-29 06:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0046_scrobble_book_page_data_alter_chartrecord_year"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="book_koreader_hash",
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-10 18:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0047_scrobble_book_koreader_hash"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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"),
|
||||
("WebPage", "Web Page"),
|
||||
],
|
||||
default="Video",
|
||||
max_length=14,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-13 03:57
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0048_alter_scrobble_media_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="scrobble",
|
||||
old_name="webpage",
|
||||
new_name="web_page",
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,16 @@
|
||||
# Generated by Django 4.2.9 on 2024-02-20 00:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0049_rename_webpage_scrobble_web_page"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="ScrobbledPage",
|
||||
),
|
||||
]
|
||||
46
vrobbler/apps/scrobbles/migrations/0051_scrobble_timezone.py
Normal file
46
vrobbler/apps/scrobbles/migrations/0051_scrobble_timezone.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Generated by Django 4.2.11 on 2024-04-19 13:10
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_default_timezone(apps, schema_editor):
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
for s in Scrobble.objects.all():
|
||||
if not s.timezone:
|
||||
s.timezone = settings.TIME_ZONE
|
||||
|
||||
if s.user and s.user.profile:
|
||||
s.timezone = s.user.profile.timezone
|
||||
|
||||
# A shim to adjust for our change to European time for 3 months
|
||||
if (
|
||||
datetime(2023, 10, 15).replace(
|
||||
tzinfo=pytz.timezone("Europe/Paris")
|
||||
)
|
||||
<= s.timestamp
|
||||
<= datetime(2023, 12, 15).replace(
|
||||
tzinfo=pytz.timezone("Europe/Paris")
|
||||
)
|
||||
):
|
||||
s.timezone = "Europe/Paris"
|
||||
s.save(update_fields=["timezone"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0050_delete_scrobbledpage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="timezone",
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.RunPython(set_default_timezone),
|
||||
]
|
||||
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.2.9 on 2024-04-05 14:39
|
||||
|
||||
import json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def convert_log_to_json(apps, schema_editor):
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
for s in Scrobble.objects.filter(scrobble_log__isnull=False):
|
||||
s.scrobble_log = json.dumps({"migrated_data": s.scrobble_log})
|
||||
s.save(update_fields=["scrobble_log"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0051_scrobble_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(convert_log_to_json),
|
||||
migrations.AlterField(
|
||||
model_name="scrobble",
|
||||
name="scrobble_log",
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.9 on 2024-04-19 18:57
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0052_alter_scrobble_scroble_log"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="source_id",
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.2.9 on 2024-04-19 19:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0053_remove_scrobble_source_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="notes",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="playback_position_ticks",
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-06 02:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0054_remove_scrobble_notes_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="scrobble",
|
||||
old_name="scrobble_log",
|
||||
new_name="log",
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,45 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-07 13:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("lifeevents", "0001_initial"),
|
||||
("scrobbles", "0055_rename_scrobble_log_scrobble_log"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="life_event",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to="lifeevents.lifeevent",
|
||||
),
|
||||
),
|
||||
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"),
|
||||
("WebPage", "Web Page"),
|
||||
("LifeEvent", "Life event"),
|
||||
],
|
||||
default="Video",
|
||||
max_length=14,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.2.13 on 2024-08-06 15:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import scrobbles.dataclasses
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="scrobble",
|
||||
name="log",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
decoder=scrobbles.dataclasses.ScrobbleLogDataDecoder,
|
||||
encoder=scrobbles.dataclasses.ScrobbleLogDataEncoder,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,57 @@
|
||||
# Generated by Django 4.2.13 on 2024-08-10 20:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import scrobbles.dataclasses
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("moods", "0001_initial"),
|
||||
("scrobbles", "0056_scrobble_life_event_alter_scrobble_media_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="mood",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to="moods.mood",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scrobble",
|
||||
name="log",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
decoder=scrobbles.dataclasses.ScrobbleLogDataDecoder,
|
||||
encoder=scrobbles.dataclasses.ScrobbleLogDataEncoder,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
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"),
|
||||
("WebPage", "Web Page"),
|
||||
("LifeEvent", "Life event"),
|
||||
("Mood", "Mood"),
|
||||
],
|
||||
default="Video",
|
||||
max_length=14,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,13 @@
|
||||
# Generated by Django 4.2.13 on 2024-08-11 02:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0057_alter_scrobble_log"),
|
||||
("scrobbles", "0057_scrobble_mood_alter_scrobble_log_and_more"),
|
||||
]
|
||||
|
||||
operations = []
|
||||
@ -0,0 +1,47 @@
|
||||
# Generated by Django 4.2.15 on 2024-08-20 14:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0058_merge_20240810_2234"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="book_koreader_hash",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="book_page_data",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="book_pages_read",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="scrobble",
|
||||
name="videogame_screenshot",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="screenshot",
|
||||
field=models.ImageField(
|
||||
blank=True,
|
||||
null=True,
|
||||
upload_to="scrobbles/videogame_screenshot/",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scrobble",
|
||||
name="videogame_save_data",
|
||||
field=models.FileField(
|
||||
blank=True,
|
||||
null=True,
|
||||
upload_to="scrobbles/videogame_save_data/",
|
||||
),
|
||||
),
|
||||
]
|
||||
25
vrobbler/apps/scrobbles/migrations/0060_scrobble_brickset.py
Normal file
25
vrobbler/apps/scrobbles/migrations/0060_scrobble_brickset.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 4.2.15 on 2024-09-07 05:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bricksets", "0001_initial"),
|
||||
("scrobbles", "0059_remove_scrobble_book_koreader_hash_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scrobble",
|
||||
name="brickset",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
to="bricksets.brickset",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,35 @@
|
||||
# Generated by Django 4.2.15 on 2024-09-07 05:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("scrobbles", "0060_scrobble_brickset"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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"),
|
||||
("WebPage", "Web Page"),
|
||||
("LifeEvent", "Life event"),
|
||||
("Mood", "Mood"),
|
||||
("BrickSet", "Brick set"),
|
||||
],
|
||||
default="Video",
|
||||
max_length=14,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -2,12 +2,14 @@ import logging
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from taggit.managers import TaggableManager
|
||||
from scrobbles.utils import get_scrobbles_for_media
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
from taggit.managers import TaggableManager
|
||||
from taggit.models import GenericTaggedItemBase, TagBase
|
||||
|
||||
BNULL = {"blank": True, "null": True}
|
||||
|
||||
@ -33,6 +35,7 @@ class ObjectWithGenres(GenericTaggedItemBase):
|
||||
|
||||
class ScrobblableMixin(TimeStampedModel):
|
||||
SECONDS_TO_STALE = 1600
|
||||
COMPLETION_PERCENT = 100
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
title = models.CharField(max_length=255, **BNULL)
|
||||
@ -44,22 +47,59 @@ class ScrobblableMixin(TimeStampedModel):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def scrobble_for_user(
|
||||
self,
|
||||
user_id,
|
||||
source: str = "Vrobbler",
|
||||
playback_position_seconds: int = 0,
|
||||
status: str = "started",
|
||||
):
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
scrobble_data = {
|
||||
"user_id": user_id,
|
||||
"timestamp": timezone.now(),
|
||||
"source": source,
|
||||
"playback_position_seconds": playback_position_seconds,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[scrobble_for_user] called",
|
||||
extra={
|
||||
"id": self.id,
|
||||
"media_type": self.__class__,
|
||||
"user_id": user_id,
|
||||
"scrobble_data": scrobble_data,
|
||||
"media_type": Scrobble.MediaType.WEBPAGE,
|
||||
},
|
||||
)
|
||||
return Scrobble.create_or_update(self, user_id, scrobble_data)
|
||||
|
||||
@property
|
||||
def primary_image_url(self) -> str:
|
||||
logger.warn(f"Not implemented yet")
|
||||
logger.warning(f"Not implemented yet")
|
||||
return ""
|
||||
|
||||
def fix_metadata(self):
|
||||
logger.warn("fix_metadata() not implemented yet")
|
||||
@property
|
||||
def logdata_cls(self) -> None:
|
||||
logger.warning("logdata_cls() not implemented yet")
|
||||
return None
|
||||
|
||||
@property
|
||||
def subtitle(self) -> str:
|
||||
return ""
|
||||
|
||||
def fix_metadata(self) -> None:
|
||||
logger.warning("fix_metadata() not implemented yet")
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls):
|
||||
logger.warn("find_or_create() not implemented yet")
|
||||
def find_or_create(cls) -> None:
|
||||
logger.warning("find_or_create() not implemented yet")
|
||||
|
||||
def scrobble(self, user_id, **kwargs):
|
||||
"""Given a user ID and a dictionary of data, attempts to scrobble it"""
|
||||
from scrobbles.models import Scrobble
|
||||
Scrobble.create_or_update(self, user_id, **kwargs)
|
||||
def __str__(self):
|
||||
if self.title:
|
||||
return self.title
|
||||
return str(self.uuid)
|
||||
|
||||
|
||||
class LongPlayScrobblableMixin(ScrobblableMixin):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user