[importers] Add bgstats import class
All checks were successful
build & deploy / test (push) Successful in 1m57s
build & deploy / build-and-deploy (push) Successful in 29s

This commit is contained in:
2026-05-23 17:03:17 -04:00
parent dce31ed840
commit a4030e89ec
8 changed files with 201 additions and 24 deletions

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from scrobbles.models import (
AudioScrobblerTSVImport,
BGStatsImport,
KoReaderImport,
LastFmImport,
RetroarchImport,
@ -79,6 +80,11 @@ class RetroarchImportAdmin(ImportBaseAdmin):
...
@admin.register(BGStatsImport)
class BGStatsImportAdmin(ImportBaseAdmin):
...
@admin.register(ScaleCSVImport)
class ScaleCSVImportAdmin(ImportBaseAdmin):
...

View File

@ -7,7 +7,6 @@ import json
from books.koreader import fetch_file_from_webdav
from profiles.models import UserProfile
from scrobbles.models import KoReaderImport, TrailGPXImport
from scrobbles.scrobblers import email_scrobble_board_game
from scrobbles.tasks import process_koreader_import, process_trail_gpx_import
from scrobbles.utils import get_file_md5_hash
from webdav.client import get_webdav_client
@ -290,14 +289,15 @@ def scan_webdav_for_retroarch(webdav_client, user_id):
def scan_webdav_for_bgstats(webdav_client, user_id):
"""Download .bgsplay files from WebDAV, parse JSON, and scrobble board games."""
"""Download .bgsplay files from WebDAV and queue imports for new files."""
from scrobbles.models import BGStatsImport
from scrobbles.tasks import process_bgstats_import
bgstats_path = DEFAULT_BGSTATS_PATH
try:
webdav_client.info(bgstats_path)
except:
logger.info(
"No var/bgstats/ directory on webdav", extra={"user_id": user_id}
)
logger.info("No var/bgstats/ directory on webdav", extra={"user_id": user_id})
return 0
try:
@ -309,38 +309,37 @@ def scan_webdav_for_bgstats(webdav_client, user_id):
)
return 0
processed = 0
seen_this_cycle = set()
new_imports = 0
already_imported = set(
BGStatsImport.objects.filter(user_id=user_id).values_list(
"original_filename", flat=True
)
)
for fname in files:
fname = os.path.basename(fname)
if not fname.lower().endswith(".bgsplay"):
continue
if fname in seen_this_cycle:
if fname in already_imported:
logger.debug(f"Skipping already-imported {fname}")
continue
seen_this_cycle.add(fname)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=fname)
try:
webdav_client.download_sync(
remote_path=f"{bgstats_path}/{fname}", local_path=tmp.name
)
with open(tmp.name, "r", encoding="utf-8") as f:
parsed_json = json.load(f)
scrobbles = email_scrobble_board_game(parsed_json, user_id)
if scrobbles:
logger.info(
"BG Stats import from %s for user %d created %d scrobble(s)",
fname,
user_id,
len(scrobbles),
)
processed += 1
except Exception as e:
logger.error(
"Failed to import BG Stats file %s: %s", fname, e
imp = BGStatsImport.objects.create(
user_id=user_id,
original_filename=fname,
)
with open(tmp.name, "rb") as f:
imp.bgsplay_file.save(fname, f, save=True)
process_bgstats_import.delay(imp.id)
new_imports += 1
except Exception as e:
logger.error(f"Failed to import BG Stats file {fname}: {e}")
finally:
os.unlink(tmp.name)
return processed
return new_imports

View File

@ -0,0 +1,74 @@
# Generated by Django 4.2.29 on 2026-05-23 20:54
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
import scrobbles.models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("scrobbles", "0080_retroarchimport_files_hash"),
]
operations = [
migrations.CreateModel(
name="BGStatsImport",
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(default=uuid.uuid4, editable=False)),
("processing_started", models.DateTimeField(blank=True, null=True)),
("processed_finished", models.DateTimeField(blank=True, null=True)),
("process_log", models.TextField(blank=True, null=True)),
("process_count", models.IntegerField(blank=True, null=True)),
(
"bgsplay_file",
models.FileField(
blank=True,
null=True,
upload_to=scrobbles.models.BGStatsImport.get_path,
),
),
(
"original_filename",
models.CharField(blank=True, max_length=255, null=True),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "BG Stats Import",
},
),
]

View File

@ -455,6 +455,53 @@ class RetroarchImport(BaseFileImportMixin):
self.mark_finished()
class BGStatsImport(BaseFileImportMixin):
class Meta:
verbose_name = "BG Stats Import"
@property
def import_type(self) -> str:
return "BG Stats"
def get_absolute_url(self):
return reverse("scrobbles:bgstats-import-detail", kwargs={"slug": self.uuid})
def get_path(instance, filename):
extension = filename.split(".")[-1]
uuid = instance.uuid
return f"bgstats-uploads/{uuid}.{extension}"
@property
def upload_file_path(self):
if getattr(settings, "USE_S3_STORAGE"):
path = self.bgsplay_file.url
else:
path = self.bgsplay_file.path
return path
bgsplay_file = models.FileField(upload_to=get_path, **BNULL)
original_filename = models.CharField(max_length=255, **BNULL)
def process(self, force=False):
"""Import scrobbles from a single BG Stats bgsplay file"""
if self.processed_finished and not force:
logger.info(f"{self} already processed on {self.processed_finished}")
return
self.mark_started()
import json
from scrobbles.scrobblers import email_scrobble_board_game
with open(self.upload_file_path, "r", encoding="utf-8") as f:
parsed_json = json.load(f)
scrobbles = email_scrobble_board_game(parsed_json, self.user_id)
self.record_log(scrobbles)
self.mark_finished()
class ScrobbleQuerySet(models.QuerySet):
def with_related(self):
return self.select_related("user").prefetch_related(

View File

@ -107,6 +107,16 @@ def process_retroarch_import(import_id):
retroarch_import.process()
@shared_task
def process_bgstats_import(import_id):
BGStatsImport = apps.get_model("scrobbles", "BGStatsImport")
bgstats_import = BGStatsImport.objects.filter(id=import_id).first()
if not bgstats_import:
logger.warn(f"BGStatsImport not found with id {import_id}")
return
bgstats_import.process()
@shared_task
def process_lastfm_import(import_id):
LastFmImport = apps.get_model("scrobbles", "LastFMImport")

View File

@ -126,6 +126,11 @@ urlpatterns = [
views.ScrobbleRetroarchImportDetailView.as_view(),
name="retroarch-import-detail",
),
path(
"imports/bgstats/<slug:slug>/",
views.ScrobbleBGStatsImportDetailView.as_view(),
name="bgstats-import-detail",
),
path(
"imports/scale/<slug:slug>/",
views.ScrobbleScaleCSVImportDetailView.as_view(),

View File

@ -69,11 +69,13 @@ from scrobbles.export import export_scrobbles
from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
from scrobbles.models import (
AudioScrobblerTSVImport,
BGStatsImport,
KoReaderImport,
LastFmImport,
RetroarchImport,
ScaleCSVImport,
Scrobble,
ScrobbleQuerySet,
TrailGPXImport,
)
from scrobbles.scrobblers import *
@ -459,6 +461,9 @@ class ScrobbleImportListView(TemplateView):
context_data["retroarch_imports"] = RetroarchImport.objects.filter(
user=self.request.user,
).order_by("-processing_started")[:10]
context_data["bgstats_imports"] = BGStatsImport.objects.filter(
user=self.request.user,
).order_by("-processing_started")[:10]
context_data["birding_csv_imports"] = apps.get_model(
"birds", "BirdingCSVImport"
).objects.filter(
@ -491,6 +496,8 @@ class BaseScrobbleImportDetailView(DetailView):
title = "LastFM Import"
if self.model == RetroarchImport:
title = "Retroarch Import"
if self.model == BGStatsImport:
title = "BG Stats Import"
if self.model == ScaleCSVImport:
title = "Scale CSV Import"
if self.model == TrailGPXImport:
@ -515,6 +522,10 @@ class ScrobbleRetroarchImportDetailView(BaseScrobbleImportDetailView):
model = RetroarchImport
class ScrobbleBGStatsImportDetailView(BaseScrobbleImportDetailView):
model = BGStatsImport
class ScrobbleScaleCSVImportDetailView(BaseScrobbleImportDetailView):
model = ScaleCSVImport

View File

@ -79,6 +79,31 @@
</div>
{% endif %}
{% if bgstats_imports %}
<div class="row">
<h3>BG Stats</h3>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Started</th>
<th scope="col">Finished</th>
<th scope="col">Scrobbles</th>
</tr>
</thead>
{% for obj in bgstats_imports %}
<tr>
<td><a href="{{obj.get_absolute_url}}">{{obj.human_start}}</a></td>
<td>{{obj.processed_finished}}</td>
<td>{{obj.process_count}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if koreader_imports %}
<div class="row">
<h3>KOReader</h3>