[importers] Add bgstats import class
This commit is contained in:
@ -2,6 +2,7 @@ from django.contrib import admin
|
|||||||
|
|
||||||
from scrobbles.models import (
|
from scrobbles.models import (
|
||||||
AudioScrobblerTSVImport,
|
AudioScrobblerTSVImport,
|
||||||
|
BGStatsImport,
|
||||||
KoReaderImport,
|
KoReaderImport,
|
||||||
LastFmImport,
|
LastFmImport,
|
||||||
RetroarchImport,
|
RetroarchImport,
|
||||||
@ -79,6 +80,11 @@ class RetroarchImportAdmin(ImportBaseAdmin):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(BGStatsImport)
|
||||||
|
class BGStatsImportAdmin(ImportBaseAdmin):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ScaleCSVImport)
|
@admin.register(ScaleCSVImport)
|
||||||
class ScaleCSVImportAdmin(ImportBaseAdmin):
|
class ScaleCSVImportAdmin(ImportBaseAdmin):
|
||||||
...
|
...
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import json
|
|||||||
from books.koreader import fetch_file_from_webdav
|
from books.koreader import fetch_file_from_webdav
|
||||||
from profiles.models import UserProfile
|
from profiles.models import UserProfile
|
||||||
from scrobbles.models import KoReaderImport, TrailGPXImport
|
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.tasks import process_koreader_import, process_trail_gpx_import
|
||||||
from scrobbles.utils import get_file_md5_hash
|
from scrobbles.utils import get_file_md5_hash
|
||||||
from webdav.client import get_webdav_client
|
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):
|
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
|
bgstats_path = DEFAULT_BGSTATS_PATH
|
||||||
try:
|
try:
|
||||||
webdav_client.info(bgstats_path)
|
webdav_client.info(bgstats_path)
|
||||||
except:
|
except:
|
||||||
logger.info(
|
logger.info("No var/bgstats/ directory on webdav", extra={"user_id": user_id})
|
||||||
"No var/bgstats/ directory on webdav", extra={"user_id": user_id}
|
|
||||||
)
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -309,38 +309,37 @@ def scan_webdav_for_bgstats(webdav_client, user_id):
|
|||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
processed = 0
|
new_imports = 0
|
||||||
seen_this_cycle = set()
|
already_imported = set(
|
||||||
|
BGStatsImport.objects.filter(user_id=user_id).values_list(
|
||||||
|
"original_filename", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for fname in files:
|
for fname in files:
|
||||||
fname = os.path.basename(fname)
|
fname = os.path.basename(fname)
|
||||||
if not fname.lower().endswith(".bgsplay"):
|
if not fname.lower().endswith(".bgsplay"):
|
||||||
continue
|
continue
|
||||||
if fname in seen_this_cycle:
|
if fname in already_imported:
|
||||||
|
logger.debug(f"Skipping already-imported {fname}")
|
||||||
continue
|
continue
|
||||||
seen_this_cycle.add(fname)
|
|
||||||
|
|
||||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=fname)
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=fname)
|
||||||
try:
|
try:
|
||||||
webdav_client.download_sync(
|
webdav_client.download_sync(
|
||||||
remote_path=f"{bgstats_path}/{fname}", local_path=tmp.name
|
remote_path=f"{bgstats_path}/{fname}", local_path=tmp.name
|
||||||
)
|
)
|
||||||
with open(tmp.name, "r", encoding="utf-8") as f:
|
imp = BGStatsImport.objects.create(
|
||||||
parsed_json = json.load(f)
|
user_id=user_id,
|
||||||
scrobbles = email_scrobble_board_game(parsed_json, user_id)
|
original_filename=fname,
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
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:
|
finally:
|
||||||
os.unlink(tmp.name)
|
os.unlink(tmp.name)
|
||||||
|
|
||||||
return processed
|
return new_imports
|
||||||
|
|||||||
@ -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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -455,6 +455,53 @@ class RetroarchImport(BaseFileImportMixin):
|
|||||||
self.mark_finished()
|
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):
|
class ScrobbleQuerySet(models.QuerySet):
|
||||||
def with_related(self):
|
def with_related(self):
|
||||||
return self.select_related("user").prefetch_related(
|
return self.select_related("user").prefetch_related(
|
||||||
|
|||||||
@ -107,6 +107,16 @@ def process_retroarch_import(import_id):
|
|||||||
retroarch_import.process()
|
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
|
@shared_task
|
||||||
def process_lastfm_import(import_id):
|
def process_lastfm_import(import_id):
|
||||||
LastFmImport = apps.get_model("scrobbles", "LastFMImport")
|
LastFmImport = apps.get_model("scrobbles", "LastFMImport")
|
||||||
|
|||||||
@ -126,6 +126,11 @@ urlpatterns = [
|
|||||||
views.ScrobbleRetroarchImportDetailView.as_view(),
|
views.ScrobbleRetroarchImportDetailView.as_view(),
|
||||||
name="retroarch-import-detail",
|
name="retroarch-import-detail",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"imports/bgstats/<slug:slug>/",
|
||||||
|
views.ScrobbleBGStatsImportDetailView.as_view(),
|
||||||
|
name="bgstats-import-detail",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"imports/scale/<slug:slug>/",
|
"imports/scale/<slug:slug>/",
|
||||||
views.ScrobbleScaleCSVImportDetailView.as_view(),
|
views.ScrobbleScaleCSVImportDetailView.as_view(),
|
||||||
|
|||||||
@ -69,11 +69,13 @@ from scrobbles.export import export_scrobbles
|
|||||||
from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
|
from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
|
||||||
from scrobbles.models import (
|
from scrobbles.models import (
|
||||||
AudioScrobblerTSVImport,
|
AudioScrobblerTSVImport,
|
||||||
|
BGStatsImport,
|
||||||
KoReaderImport,
|
KoReaderImport,
|
||||||
LastFmImport,
|
LastFmImport,
|
||||||
RetroarchImport,
|
RetroarchImport,
|
||||||
ScaleCSVImport,
|
ScaleCSVImport,
|
||||||
Scrobble,
|
Scrobble,
|
||||||
|
ScrobbleQuerySet,
|
||||||
TrailGPXImport,
|
TrailGPXImport,
|
||||||
)
|
)
|
||||||
from scrobbles.scrobblers import *
|
from scrobbles.scrobblers import *
|
||||||
@ -459,6 +461,9 @@ class ScrobbleImportListView(TemplateView):
|
|||||||
context_data["retroarch_imports"] = RetroarchImport.objects.filter(
|
context_data["retroarch_imports"] = RetroarchImport.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
).order_by("-processing_started")[:10]
|
).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(
|
context_data["birding_csv_imports"] = apps.get_model(
|
||||||
"birds", "BirdingCSVImport"
|
"birds", "BirdingCSVImport"
|
||||||
).objects.filter(
|
).objects.filter(
|
||||||
@ -491,6 +496,8 @@ class BaseScrobbleImportDetailView(DetailView):
|
|||||||
title = "LastFM Import"
|
title = "LastFM Import"
|
||||||
if self.model == RetroarchImport:
|
if self.model == RetroarchImport:
|
||||||
title = "Retroarch Import"
|
title = "Retroarch Import"
|
||||||
|
if self.model == BGStatsImport:
|
||||||
|
title = "BG Stats Import"
|
||||||
if self.model == ScaleCSVImport:
|
if self.model == ScaleCSVImport:
|
||||||
title = "Scale CSV Import"
|
title = "Scale CSV Import"
|
||||||
if self.model == TrailGPXImport:
|
if self.model == TrailGPXImport:
|
||||||
@ -515,6 +522,10 @@ class ScrobbleRetroarchImportDetailView(BaseScrobbleImportDetailView):
|
|||||||
model = RetroarchImport
|
model = RetroarchImport
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobbleBGStatsImportDetailView(BaseScrobbleImportDetailView):
|
||||||
|
model = BGStatsImport
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleScaleCSVImportDetailView(BaseScrobbleImportDetailView):
|
class ScrobbleScaleCSVImportDetailView(BaseScrobbleImportDetailView):
|
||||||
model = ScaleCSVImport
|
model = ScaleCSVImport
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,31 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if koreader_imports %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h3>KOReader</h3>
|
<h3>KOReader</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user