Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31888a85cb | |||
| 22d8b0787e | |||
| 8cc559752b | |||
| db3f9696fa | |||
| 407d570c82 | |||
| 033239260f | |||
| 9f854dc735 | |||
| f29272a853 | |||
| 4e56d9420a | |||
| 852a257159 |
65
PROJECT.org
65
PROJECT.org
@ -88,7 +88,7 @@ fetching and simple saving.
|
|||||||
*** Metadata sources
|
*** Metadata sources
|
||||||
**** Scraper
|
**** Scraper
|
||||||
|
|
||||||
* Backlog [0/21] :vrobbler:project:personal:
|
* Backlog [0/22] :vrobbler:project:personal:
|
||||||
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
|
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
|
||||||
@ -579,6 +579,18 @@ named constants for maintainability.
|
|||||||
- ~vrobbler/apps/scrobbles/importers/tsv.py~ (line 55) -- ="S"= completion status
|
- ~vrobbler/apps/scrobbles/importers/tsv.py~ (line 55) -- ="S"= completion status
|
||||||
|
|
||||||
|
|
||||||
|
** TODO [#A] Deduplicate BGG plays before posting :boardgames:bgg:duplication:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: e9b842bf-0049-42e7-a060-f3ebd0067d2f
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
No check for existing BGG plays before posting, which can create duplicates.
|
||||||
|
Should look up past plays by =bggeek_id= first.
|
||||||
|
|
||||||
|
File: ~vrobbler/apps/boardgames/bgg.py~ (line 117)
|
||||||
|
|
||||||
** TODO [#C] Clean up naming of =bgsplay= parsing :importers:refactoring:
|
** TODO [#C] Clean up naming of =bgsplay= parsing :importers:refactoring:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: c751dbbc-464a-4e63-9fe3-e034303f7b54
|
:ID: c751dbbc-464a-4e63-9fe3-e034303f7b54
|
||||||
@ -591,6 +603,57 @@ a helper method to create board game scrobbles given a json blob. It's
|
|||||||
independent of the email flow it was originally creatdd for
|
independent of the email flow it was originally creatdd for
|
||||||
|
|
||||||
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
|
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
|
||||||
|
|
||||||
|
* Version 55.6 [1/1]
|
||||||
|
** DONE [#A] Figure out why historical Lastfm imports don't work :importers:lastfm:music:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 71b18c1b-de96-6d93-20fa-de2ec0df1288
|
||||||
|
:END:
|
||||||
|
|
||||||
|
* Version 55.5 [1/1]
|
||||||
|
** DONE [#B] Fix bug in lastfm import for new users :importers:lastfm:music:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: d034966d-0c7f-e512-4cf8-7329c9026b6f
|
||||||
|
:END:
|
||||||
|
|
||||||
|
* Version 55.4 [1/1]
|
||||||
|
** DONE [#A] Tighten up the speed of startup and first request :perf:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 9ee8834c-6be2-d04b-df6d-56375504083f
|
||||||
|
:END:
|
||||||
|
|
||||||
|
|
||||||
|
* Version 55.3 [3/3]
|
||||||
|
** DONE [#C] =alt_names= feature for artists (commented out / dead code) :music:dead-code:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: e22060a2-5f7a-4f33-9056-309ecd27159c
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
File: ~vrobbler/apps/music/models.py~ (line 236)
|
||||||
|
|
||||||
|
An entire block of code for tracking alternate artist names is commented
|
||||||
|
out. The TODO questions whether it even works. Review: either implement
|
||||||
|
properly or remove the dead code.
|
||||||
|
|
||||||
|
** DONE [#A] Put chart rebuilds in a lower priority task queue :charts:tasks:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 43c90de0-fc1c-1139-dac7-9b7c82006b2e
|
||||||
|
:END:
|
||||||
|
** DONE [#A] Check for existing book scrobble and update page count :books:scrobbling:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 1a0609bc-6b16-4da4-96c1-59588229e4b4
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
File: ~vrobbler/apps/scrobbles/scrobblers.py~ (line 330)
|
||||||
|
|
||||||
|
When scrobbling a book (comic), the code doesn't check for prior scrobbles to
|
||||||
|
update reading progress. Needed for proper page-count tracking.
|
||||||
|
|
||||||
|
|
||||||
* Version 55.2 [2/2]
|
* Version 55.2 [2/2]
|
||||||
** DONE [#A] Fix bug in scrobble id in calendar view :templates:
|
** DONE [#A] Fix bug in scrobble id in calendar view :templates:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
2
Procfile
2
Procfile
@ -1,2 +1,2 @@
|
|||||||
web: python manage.py runserver 0.0.0.0:8014
|
web: python manage.py runserver 0.0.0.0:8014
|
||||||
worker: celery -A vrobbler worker -l DEBUG
|
worker: celery -A vrobbler worker -Q default,charts -l DEBUG
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "55.2"
|
version = "55.6"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,15 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from vrobbler import context_processors
|
||||||
from vrobbler.context_processors import version_info
|
from vrobbler.context_processors import version_info
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_git_cache():
|
||||||
|
context_processors._GIT_COMMIT = None
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_request():
|
def mock_request():
|
||||||
return MagicMock()
|
return MagicMock()
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-06-19 16:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("charts", "0002_chartrecord_charts_char_user_id_1adcde_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("artist__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "artist"),
|
||||||
|
name="unique_chart_artist_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("album__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "album"),
|
||||||
|
name="unique_chart_album_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("track__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "track"),
|
||||||
|
name="unique_chart_track_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("tv_series__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "tv_series"),
|
||||||
|
name="unique_chart_tv_series_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("video__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "video"),
|
||||||
|
name="unique_chart_video_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("podcast__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "podcast"),
|
||||||
|
name="unique_chart_podcast_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("podcast_episode__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "podcast_episode"),
|
||||||
|
name="unique_chart_podcast_episode_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("board_game__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "board_game"),
|
||||||
|
name="unique_chart_board_game_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("trail__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "trail"),
|
||||||
|
name="unique_chart_trail_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("geo_location__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "geo_location"),
|
||||||
|
name="unique_chart_geo_location_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("food__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "food"),
|
||||||
|
name="unique_chart_food_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="chartrecord",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("book__isnull", False)),
|
||||||
|
fields=("user", "year", "month", "week", "day", "book"),
|
||||||
|
name="unique_chart_book_period",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -2,6 +2,7 @@ import calendar
|
|||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
|
|
||||||
@ -84,6 +85,68 @@ class ChartRecord(TimeStampedModel):
|
|||||||
models.Index(fields=["user", "year", "month", "day", "album", "rank"]),
|
models.Index(fields=["user", "year", "month", "day", "album", "rank"]),
|
||||||
models.Index(fields=["user", "year", "month", "day", "tv_series", "rank"]),
|
models.Index(fields=["user", "year", "month", "day", "tv_series", "rank"]),
|
||||||
]
|
]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "artist"],
|
||||||
|
condition=Q(artist__isnull=False),
|
||||||
|
name="unique_chart_artist_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "album"],
|
||||||
|
condition=Q(album__isnull=False),
|
||||||
|
name="unique_chart_album_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "track"],
|
||||||
|
condition=Q(track__isnull=False),
|
||||||
|
name="unique_chart_track_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "tv_series"],
|
||||||
|
condition=Q(tv_series__isnull=False),
|
||||||
|
name="unique_chart_tv_series_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "video"],
|
||||||
|
condition=Q(video__isnull=False),
|
||||||
|
name="unique_chart_video_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "podcast"],
|
||||||
|
condition=Q(podcast__isnull=False),
|
||||||
|
name="unique_chart_podcast_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "podcast_episode"],
|
||||||
|
condition=Q(podcast_episode__isnull=False),
|
||||||
|
name="unique_chart_podcast_episode_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "board_game"],
|
||||||
|
condition=Q(board_game__isnull=False),
|
||||||
|
name="unique_chart_board_game_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "trail"],
|
||||||
|
condition=Q(trail__isnull=False),
|
||||||
|
name="unique_chart_trail_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "geo_location"],
|
||||||
|
condition=Q(geo_location__isnull=False),
|
||||||
|
name="unique_chart_geo_location_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "food"],
|
||||||
|
condition=Q(food__isnull=False),
|
||||||
|
name="unique_chart_food_period",
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "year", "month", "week", "day", "book"],
|
||||||
|
condition=Q(book__isnull=False),
|
||||||
|
name="unique_chart_book_period",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_obj(self):
|
def media_obj(self):
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from typing import Optional
|
|||||||
import pytz
|
import pytz
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -186,60 +187,64 @@ def build_charts(
|
|||||||
ranks = {count: rank for rank, count in enumerate(unique_counts, start=1)}
|
ranks = {count: rank for rank, count in enumerate(unique_counts, start=1)}
|
||||||
|
|
||||||
media_field = f"{media_type}_id"
|
media_field = f"{media_type}_id"
|
||||||
records_to_create = []
|
|
||||||
records_to_update = []
|
|
||||||
|
|
||||||
existing = ChartRecord.objects.filter(
|
with transaction.atomic():
|
||||||
period_filter, user=user, **{media_field + "__isnull": False}
|
records_to_create = []
|
||||||
)
|
records_to_update = []
|
||||||
existing_by_media_id = {getattr(r, media_field): r for r in existing}
|
|
||||||
found_media_ids = set()
|
|
||||||
|
|
||||||
for result in results:
|
existing = ChartRecord.objects.select_for_update().filter(
|
||||||
media_id = result[config["values"]]
|
period_filter, user=user, **{media_field + "__isnull": False}
|
||||||
if media_id is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
found_media_ids.add(media_id)
|
|
||||||
|
|
||||||
chart_record_data = {
|
|
||||||
"user_id": user.id,
|
|
||||||
"year": year,
|
|
||||||
"month": month,
|
|
||||||
"week": week,
|
|
||||||
"day": day,
|
|
||||||
"rank": ranks[result["scrobble_count"]],
|
|
||||||
"count": result["scrobble_count"],
|
|
||||||
}
|
|
||||||
chart_record_data[media_field] = media_id
|
|
||||||
|
|
||||||
if media_id in existing_by_media_id:
|
|
||||||
existing_record = existing_by_media_id[media_id]
|
|
||||||
existing_record.rank = chart_record_data["rank"]
|
|
||||||
existing_record.count = chart_record_data["count"]
|
|
||||||
records_to_update.append(existing_record)
|
|
||||||
else:
|
|
||||||
records_to_create.append(ChartRecord(**chart_record_data))
|
|
||||||
|
|
||||||
ids_to_delete = [
|
|
||||||
r.id for r in existing if getattr(r, media_field) not in found_media_ids
|
|
||||||
]
|
|
||||||
if ids_to_delete:
|
|
||||||
ChartRecord.objects.filter(id__in=ids_to_delete).delete()
|
|
||||||
|
|
||||||
if records_to_update:
|
|
||||||
ChartRecord.objects.bulk_update(
|
|
||||||
records_to_update, ["rank", "count"], batch_size=500
|
|
||||||
)
|
)
|
||||||
|
existing_by_media_id = {getattr(r, media_field): r for r in existing}
|
||||||
|
found_media_ids = set()
|
||||||
|
|
||||||
if records_to_create:
|
for result in results:
|
||||||
ChartRecord.objects.bulk_create(records_to_create, batch_size=500)
|
media_id = result[config["values"]]
|
||||||
|
if media_id is None:
|
||||||
|
continue
|
||||||
|
|
||||||
logger.info(
|
found_media_ids.add(media_id)
|
||||||
f"Built {len(records_to_create)} new, {len(records_to_update)} updated "
|
|
||||||
f"chart records for {media_type}, period "
|
chart_record_data = {
|
||||||
f"{year}-{month or ''}-{week or ''}-{day or ''} for {user}"
|
"user_id": user.id,
|
||||||
)
|
"year": year,
|
||||||
|
"month": month,
|
||||||
|
"week": week,
|
||||||
|
"day": day,
|
||||||
|
"rank": ranks[result["scrobble_count"]],
|
||||||
|
"count": result["scrobble_count"],
|
||||||
|
}
|
||||||
|
chart_record_data[media_field] = media_id
|
||||||
|
|
||||||
|
if media_id in existing_by_media_id:
|
||||||
|
existing_record = existing_by_media_id[media_id]
|
||||||
|
existing_record.rank = chart_record_data["rank"]
|
||||||
|
existing_record.count = chart_record_data["count"]
|
||||||
|
records_to_update.append(existing_record)
|
||||||
|
else:
|
||||||
|
records_to_create.append(ChartRecord(**chart_record_data))
|
||||||
|
|
||||||
|
ids_to_delete = [
|
||||||
|
r.id
|
||||||
|
for r in existing
|
||||||
|
if getattr(r, media_field) not in found_media_ids
|
||||||
|
]
|
||||||
|
if ids_to_delete:
|
||||||
|
ChartRecord.objects.filter(id__in=ids_to_delete).delete()
|
||||||
|
|
||||||
|
if records_to_update:
|
||||||
|
ChartRecord.objects.bulk_update(
|
||||||
|
records_to_update, ["rank", "count"], batch_size=500
|
||||||
|
)
|
||||||
|
|
||||||
|
if records_to_create:
|
||||||
|
ChartRecord.objects.bulk_create(records_to_create, batch_size=500)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Built {len(records_to_create)} new, {len(records_to_update)} updated "
|
||||||
|
f"chart records for {media_type}, period "
|
||||||
|
f"{year}-{month or ''}-{week or ''}-{day or ''} for {user}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_yesterdays_charts(user, media_types: Optional[list] = None) -> None:
|
def build_yesterdays_charts(user, media_types: Optional[list] = None) -> None:
|
||||||
|
|||||||
@ -1,8 +1,22 @@
|
|||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
from music.models import Artist, Album
|
from music.models import Artist, Album
|
||||||
|
|
||||||
|
CACHE_TTL = 300
|
||||||
|
|
||||||
|
|
||||||
def music_lists(request):
|
def music_lists(request):
|
||||||
|
artist_list = cache.get("music_lists_artist_list")
|
||||||
|
if artist_list is None:
|
||||||
|
artist_list = list(Artist.objects.all().only("id", "name"))
|
||||||
|
cache.set("music_lists_artist_list", artist_list, CACHE_TTL)
|
||||||
|
|
||||||
|
album_list = cache.get("music_lists_album_list")
|
||||||
|
if album_list is None:
|
||||||
|
album_list = list(Album.objects.all().only("id", "name"))
|
||||||
|
cache.set("music_lists_album_list", album_list, CACHE_TTL)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"artist_list": Artist.objects.all(),
|
"artist_list": artist_list,
|
||||||
"album_list": Album.objects.all(),
|
"album_list": album_list,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -236,19 +236,6 @@ class Artist(TimeStampedModel):
|
|||||||
)
|
)
|
||||||
artist.fix_metadata()
|
artist.fix_metadata()
|
||||||
|
|
||||||
# TODO: See if this alt_names stuff actually works or causes hard to debug problems
|
|
||||||
# If we did find our artist, but the found name is slightly differnt, record that
|
|
||||||
# if artist and alt_name:
|
|
||||||
# if not artist.alt_names:
|
|
||||||
# artist.alt_names = alt_name
|
|
||||||
# else:
|
|
||||||
# artist.alt_names += f"\\{alt_name}"
|
|
||||||
# logger.info(
|
|
||||||
# f"Add alt_name {alt_name} to artist {artist}",
|
|
||||||
# extra={"alt_name": alt_name, "artist_id": artist.id},
|
|
||||||
# )
|
|
||||||
# artist.save(update_fields=["alt_names"])
|
|
||||||
|
|
||||||
return artist
|
return artist
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import pytz
|
import pytz
|
||||||
|
from django.core.cache import cache
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from scrobbles.constants import EXCLUDE_FROM_NOW_PLAYING
|
from scrobbles.constants import EXCLUDE_FROM_NOW_PLAYING
|
||||||
@ -19,6 +20,8 @@ MONTH_COLORS = [
|
|||||||
"#db7a7a", # Dec
|
"#db7a7a", # Dec
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CACHE_TTL = 60
|
||||||
|
|
||||||
|
|
||||||
def month_color(request):
|
def month_color(request):
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@ -27,15 +30,25 @@ def month_color(request):
|
|||||||
|
|
||||||
def now_playing(request):
|
def now_playing(request):
|
||||||
user = request.user
|
user = request.user
|
||||||
now = timezone.now()
|
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return {}
|
return {}
|
||||||
return {
|
|
||||||
"now_playing_list": Scrobble.objects.filter(
|
cache_key = f"now_playing_list_{user.id}"
|
||||||
in_progress=True,
|
now_playing_list = cache.get(cache_key)
|
||||||
is_paused=False,
|
if now_playing_list is None:
|
||||||
user=user,
|
now_playing_list = list(
|
||||||
).exclude(
|
Scrobble.objects.filter(
|
||||||
media_type__in=EXCLUDE_FROM_NOW_PLAYING,
|
in_progress=True,
|
||||||
|
is_paused=False,
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
.exclude(
|
||||||
|
media_type__in=EXCLUDE_FROM_NOW_PLAYING,
|
||||||
|
)
|
||||||
|
.select_related("track", "video", "podcast_episode")
|
||||||
)
|
)
|
||||||
|
cache.set(cache_key, now_playing_list, CACHE_TTL)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"now_playing_list": now_playing_list,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -330,8 +330,6 @@ def manual_scrobble_book(
|
|||||||
|
|
||||||
source = READCOMICSONLINE_URL.replace("https://", "")
|
source = READCOMICSONLINE_URL.replace("https://", "")
|
||||||
|
|
||||||
# TODO: Check for scrobble of this book already and if so, update the page count
|
|
||||||
|
|
||||||
book = Book.find_or_create(title, url=url, enrich=True)
|
book = Book.find_or_create(title, url=url, enrich=True)
|
||||||
|
|
||||||
scrobble_dict = {
|
scrobble_dict = {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -52,6 +53,11 @@ def _update_charts_for_timestamp(user, ts):
|
|||||||
if ts is None:
|
if ts is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
lock_key = f"chart_update_{user.id}"
|
||||||
|
if not cache.add(lock_key, "locked", timeout=30):
|
||||||
|
logger.info(f"Chart update already queued for user {user.id}, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
if timezone.is_naive(ts):
|
if timezone.is_naive(ts):
|
||||||
ts = timezone.make_aware(ts)
|
ts = timezone.make_aware(ts)
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from charts.utils import (
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -241,6 +242,11 @@ def update_charts_for_timestamp(user_id, year, month, day, week):
|
|||||||
logger.error(f"User with id {user_id} not found")
|
logger.error(f"User with id {user_id} not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
lock_key = f"chart_update_running_{user_id}"
|
||||||
|
if not cache.add(lock_key, "locked", timeout=300):
|
||||||
|
logger.info(f"Chart update already running for user {user_id}, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
build_daily_charts(user, year, month, day, CHARTABLE_MEDIA_TYPES)
|
build_daily_charts(user, year, month, day, CHARTABLE_MEDIA_TYPES)
|
||||||
build_weekly_charts(user, year, week, CHARTABLE_MEDIA_TYPES)
|
build_weekly_charts(user, year, week, CHARTABLE_MEDIA_TYPES)
|
||||||
@ -250,6 +256,8 @@ def update_charts_for_timestamp(user_id, year, month, day, week):
|
|||||||
logger.info(f"[charts] Updated charts for {user} on {date_str}")
|
logger.info(f"[charts] Updated charts for {user} on {date_str}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[charts] Failed to update charts: {e}")
|
logger.error(f"[charts] Failed to update charts: {e}")
|
||||||
|
finally:
|
||||||
|
cache.delete(lock_key)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
|||||||
@ -154,10 +154,11 @@ def import_lastfm_for_all_users(restart=False):
|
|||||||
last_processed = lfm_import.processed_finished
|
last_processed = lfm_import.processed_finished
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Not resuming failed LastFM import {lfm_import.id} for user {user_id}, use restart=True to restart"
|
"No existing LastFM import for user %s, "
|
||||||
"No existing LastFM import, we should start a monthly parsing of lastFm for this user going back to 2002"
|
"starting a full parse",
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
continue
|
last_processed = None
|
||||||
|
|
||||||
lfm_client = LastFM(user=get_user_model().objects.filter(id=user_id).first())
|
lfm_client = LastFM(user=get_user_model().objects.filter(id=user_id).first())
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,24 @@
|
|||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
from videos.models import Video, Series
|
from videos.models import Video, Series
|
||||||
|
|
||||||
|
CACHE_TTL = 300
|
||||||
|
|
||||||
|
|
||||||
def video_lists(request):
|
def video_lists(request):
|
||||||
|
movie_list = cache.get("video_lists_movie_list")
|
||||||
|
if movie_list is None:
|
||||||
|
movie_list = list(
|
||||||
|
Video.objects.filter(video_type=Video.VideoType.MOVIE).only("id", "title")
|
||||||
|
)
|
||||||
|
cache.set("video_lists_movie_list", movie_list, CACHE_TTL)
|
||||||
|
|
||||||
|
series_list = cache.get("video_lists_series_list")
|
||||||
|
if series_list is None:
|
||||||
|
series_list = list(Series.objects.all().only("id", "name"))
|
||||||
|
cache.set("video_lists_series_list", series_list, CACHE_TTL)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"movie_list": Video.objects.filter(video_type=Video.VideoType.MOVIE),
|
"movie_list": movie_list,
|
||||||
"series_list": Series.objects.all(),
|
"series_list": series_list,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,46 +4,55 @@ from importlib.metadata import version as get_version
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
_GIT_COMMIT = None
|
||||||
|
|
||||||
|
|
||||||
def version_info(request):
|
def version_info(request):
|
||||||
|
global _GIT_COMMIT
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_version = get_version("vrobbler")
|
app_version = get_version("vrobbler")
|
||||||
except Exception:
|
except Exception:
|
||||||
app_version = "unknown"
|
app_version = "unknown"
|
||||||
|
|
||||||
commit = os.environ.get("VROBBLER_COMMIT")
|
commit = os.environ.get("VROBBLER_COMMIT")
|
||||||
if not commit:
|
if commit:
|
||||||
# Try to import from _commit.py module first
|
return {"app_version": app_version, "git_commit": commit}
|
||||||
|
|
||||||
|
if _GIT_COMMIT is not None:
|
||||||
|
return {"app_version": app_version, "git_commit": _GIT_COMMIT}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from vrobbler._commit import commit as _commit
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if _commit and _commit != "unknown":
|
||||||
|
_GIT_COMMIT = _commit
|
||||||
|
return {"app_version": app_version, "git_commit": _GIT_COMMIT}
|
||||||
|
|
||||||
|
commit_file = Path("/var/lib/vrobbler/commit.txt")
|
||||||
|
if commit_file.exists():
|
||||||
try:
|
try:
|
||||||
from vrobbler._commit import commit as _commit
|
commit = commit_file.read_text().strip()
|
||||||
except ImportError:
|
if commit:
|
||||||
|
_GIT_COMMIT = commit
|
||||||
|
return {"app_version": app_version, "git_commit": _GIT_COMMIT}
|
||||||
|
except OSError:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
if _commit and _commit != "unknown":
|
|
||||||
return {"app_version": app_version, "git_commit": _commit}
|
|
||||||
|
|
||||||
# Try to read from commit file (written during deploy)
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
commit_file = Path("/var/lib/vrobbler/commit.txt")
|
try:
|
||||||
if commit_file.exists():
|
_GIT_COMMIT = (
|
||||||
try:
|
subprocess.check_output(
|
||||||
commit = commit_file.read_text().strip()
|
["git", "rev-parse", "--short", "HEAD"],
|
||||||
if commit:
|
cwd=PROJECT_ROOT,
|
||||||
return {"app_version": app_version, "git_commit": commit}
|
stderr=subprocess.DEVNULL,
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fall back to git command
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
try:
|
|
||||||
commit = (
|
|
||||||
subprocess.check_output(
|
|
||||||
["git", "rev-parse", "--short", "HEAD"],
|
|
||||||
cwd=PROJECT_ROOT,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
.decode("utf-8")
|
|
||||||
.strip()
|
|
||||||
)
|
)
|
||||||
except (subprocess.SubprocessError, FileNotFoundError):
|
.decode("utf-8")
|
||||||
commit = "unknown"
|
.strip()
|
||||||
|
)
|
||||||
|
except (subprocess.SubprocessError, FileNotFoundError):
|
||||||
|
_GIT_COMMIT = "unknown"
|
||||||
|
|
||||||
return {"app_version": app_version, "git_commit": commit}
|
return {"app_version": app_version, "git_commit": _GIT_COMMIT}
|
||||||
|
|||||||
@ -122,6 +122,15 @@ CELERY_ACCEPT_CONTENT = ["json"]
|
|||||||
CELERY_RESULT_EXTENDED = True
|
CELERY_RESULT_EXTENDED = True
|
||||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
||||||
|
|
||||||
|
CELERY_TASK_CREATE_MISSING_QUEUES = True
|
||||||
|
CELERY_TASK_ROUTES = {
|
||||||
|
"scrobbles.tasks.update_charts_for_timestamp": {"queue": "charts"},
|
||||||
|
"scrobbles.tasks.create_yesterdays_charts": {"queue": "charts"},
|
||||||
|
"scrobbles.tasks.rebuild_weekly_charts": {"queue": "charts"},
|
||||||
|
"scrobbles.tasks.rebuild_monthly_charts": {"queue": "charts"},
|
||||||
|
"scrobbles.tasks.rebuild_yearly_charts": {"queue": "charts"},
|
||||||
|
}
|
||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"build-yesterdays-charts": {
|
"build-yesterdays-charts": {
|
||||||
"task": "scrobbles.tasks.create_yesterdays_charts",
|
"task": "scrobbles.tasks.create_yesterdays_charts",
|
||||||
@ -352,11 +361,11 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||||
#
|
#
|
||||||
from storages.backends import s3boto3
|
|
||||||
|
|
||||||
USE_S3_STORAGE = os.getenv("VROBBLER_USE_S3", "False").lower() in TRUTHY
|
USE_S3_STORAGE = os.getenv("VROBBLER_USE_S3", "False").lower() in TRUTHY
|
||||||
|
|
||||||
if USE_S3_STORAGE:
|
if USE_S3_STORAGE:
|
||||||
|
from storages.backends import s3boto3
|
||||||
|
|
||||||
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL", "")
|
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL", "")
|
||||||
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "")
|
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "")
|
||||||
AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID")
|
AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID")
|
||||||
|
|||||||
Reference in New Issue
Block a user