131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
import logging
|
|
|
|
from datetime import timedelta
|
|
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
|
|
from books.koreader import SESSION_GAP_SECONDS, fix_long_play_stats_for_scrobbles
|
|
|
|
logger = logging.getLogger(__name__)
|
|
SESSION_GAP = timedelta(seconds=SESSION_GAP_SECONDS)
|
|
|
|
|
|
def _page_data_keys(pages):
|
|
return sorted(int(k) for k in (pages or {}))
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Merge orphaned 1-page KOReader scrobbles into the preceding scrobble"
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--commit",
|
|
action="store_true",
|
|
help="Commit changes to the database",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
from scrobbles.models import Scrobble
|
|
|
|
commit = options["commit"]
|
|
|
|
qs = Scrobble.objects.filter(
|
|
media_type="Book", source="KOReader"
|
|
).order_by("book_id", "timestamp")
|
|
|
|
if not qs.exists():
|
|
self.stdout.write("No KOReader book scrobbles found.")
|
|
return
|
|
|
|
merged = 0
|
|
affected_books = set()
|
|
|
|
# Group by book_id manually since we're iterating in order
|
|
book_scrobbles = {}
|
|
for s in qs:
|
|
book_scrobbles.setdefault(s.book_id, []).append(s)
|
|
|
|
if not commit:
|
|
self.stdout.write("Dry run — no changes will be saved. Use --commit to apply.")
|
|
|
|
for book_id, scrobbles in book_scrobbles.items():
|
|
batch_merged = 0
|
|
i = 0
|
|
while i < len(scrobbles) - 1:
|
|
current = scrobbles[i]
|
|
orphan = scrobbles[i + 1]
|
|
|
|
orphan_pages = orphan.logdata.page_data if orphan.logdata else {}
|
|
orphan_keys = _page_data_keys(orphan_pages)
|
|
if len(orphan_keys) != 1:
|
|
i += 1
|
|
continue
|
|
|
|
current_pages = current.logdata.page_data if current.logdata else {}
|
|
current_keys = _page_data_keys(current_pages)
|
|
if not current_keys:
|
|
i += 1
|
|
continue
|
|
|
|
orphan_page_num = orphan_keys[0]
|
|
current_last_page = current_keys[-1]
|
|
|
|
if orphan_page_num != current_last_page + 1:
|
|
i += 1
|
|
continue
|
|
|
|
# Check that the orphan is close enough in time
|
|
gap = orphan.timestamp - current.stop_timestamp
|
|
if gap > SESSION_GAP:
|
|
i += 1
|
|
continue
|
|
|
|
# Merge orphan into current
|
|
current_pages[str(orphan_page_num)] = orphan_pages[str(orphan_page_num)]
|
|
current.log["page_data"] = current_pages
|
|
current.log["pages_read"] = len(current_pages)
|
|
current.stop_timestamp = orphan.stop_timestamp
|
|
current.playback_position_seconds += orphan.playback_position_seconds
|
|
|
|
affected_books.add(book_id)
|
|
|
|
if commit:
|
|
with transaction.atomic():
|
|
current.save(
|
|
update_fields=[
|
|
"log",
|
|
"stop_timestamp",
|
|
"playback_position_seconds",
|
|
]
|
|
)
|
|
orphan.delete()
|
|
|
|
merged += 1
|
|
batch_merged += 1
|
|
scrobbles.pop(i + 1)
|
|
|
|
if batch_merged:
|
|
self.stdout.write(
|
|
f" Book {book_id}: merged {batch_merged} orphan scrobble(s)"
|
|
)
|
|
|
|
self.stdout.write(f"\nTotal orphans merged: {merged}")
|
|
|
|
if commit and affected_books:
|
|
self.stdout.write("Recalculating long_play_stats for affected books...")
|
|
for book_id in affected_books:
|
|
scrobbles_to_fix = (
|
|
Scrobble.objects.filter(book_id=book_id, source="KOReader")
|
|
.order_by("timestamp")
|
|
)
|
|
fix_long_play_stats_for_scrobbles(list(scrobbles_to_fix))
|
|
|
|
self.stdout.write(f"Fixed stats for {len(affected_books)} books.")
|
|
|
|
if not commit:
|
|
self.stdout.write(
|
|
f"\nWould merge {merged} orphan scrobble(s) across "
|
|
f"{len(affected_books)} book(s)."
|
|
)
|