Merge branch 'develop'

This commit is contained in:
2025-06-09 11:26:25 -04:00
18 changed files with 365 additions and 81 deletions

View File

@ -16,7 +16,9 @@ class BrickSet(LongPlayScrobblableMixin):
number = models.CharField(max_length=10, **BNULL) number = models.CharField(max_length=10, **BNULL)
release_year = models.IntegerField(**BNULL) release_year = models.IntegerField(**BNULL)
piece_count = models.IntegerField(**BNULL) piece_count = models.IntegerField(**BNULL)
brickset_rating = models.DecimalField(max_digits=3, decimal_places=1, **BNULL) brickset_rating = models.DecimalField(
max_digits=3, decimal_places=1, **BNULL
)
lego_item_number = models.CharField(max_length=10, **BNULL) lego_item_number = models.CharField(max_length=10, **BNULL)
box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL) box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
box_image_small = ImageSpecField( box_image_small = ImageSpecField(
@ -48,13 +50,23 @@ class BrickSet(LongPlayScrobblableMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("bricksets:brickset_detail", kwargs={"slug": self.uuid}) return reverse("bricksets:brickset_detail", kwargs={"slug": self.uuid})
def __str__(self) -> str:
name = str(self.title)
if not self.title:
name = str(self.number)
return name
@property @property
def logdata_cls(self): def logdata_cls(self):
return BrickSetLogData return BrickSetLogData
@classmethod @classmethod
def find_or_create(cls, title: str) -> "BrickSet": def find_or_create(cls, brickset_id: str) -> "BrickSet":
return cls.objects.filter(title=title).first() brickset = cls.objects.filter(number=brickset_id).first()
if not brickset:
# TODO: enrich this from the website
brickset = cls.objects.create(number=brickset_id)
return brickset
@property @property
def primary_image_url(self) -> str: def primary_image_url(self) -> str:

View File

@ -16,6 +16,7 @@ class AlbumAdmin(admin.ModelAdmin):
"theaudiodb_mood", "theaudiodb_mood",
"musicbrainz_id", "musicbrainz_id",
) )
raw_id_fields = ("album_artist",)
list_filter = ( list_filter = (
"theaudiodb_score", "theaudiodb_score",
"theaudiodb_genre", "theaudiodb_genre",
@ -53,6 +54,10 @@ class TrackAdmin(admin.ModelAdmin):
"artist", "artist",
"musicbrainz_id", "musicbrainz_id",
) )
raw_id_fields = (
"album",
"artist",
)
list_filter = ("album", "artist") list_filter = ("album", "artist")
search_fields = ("title",) search_fields = ("title",)
ordering = ("-created",) ordering = ("-created",)

View File

@ -27,7 +27,7 @@ class ScrobbleInline(admin.TabularInline):
"geo_location", "geo_location",
"task", "task",
"mood", "mood",
"brickset", "brick_set",
"trail", "trail",
"beer", "beer",
"web_page", "web_page",
@ -122,9 +122,12 @@ class ScrobbleAdmin(admin.ModelAdmin):
"video_game", "video_game",
"board_game", "board_game",
"geo_location", "geo_location",
"puzzle",
"paper",
"food",
"task", "task",
"mood", "mood",
"brickset", "brick_set",
"trail", "trail",
"beer", "beer",
"web_page", "web_page",

View File

@ -27,14 +27,14 @@ MEDIA_END_PADDING_SECONDS = {
TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}" TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}"
SCROBBLE_CONTENT_URLS = { SCROBBLE_CONTENT_URLS = {
"-i": "https://www.imdb.com/title/", "-i": ["https://www.imdb.com/title/", "https://www.youtube.com/watch?v="],
"-s": "https://www.thesportsdb.com/event/", "-s": ["https://www.thesportsdb.com/event/"],
"-g": "https://boardgamegeek.com/boardgame/", "-g": ["https://boardgamegeek.com/boardgame/"],
"-u": "https://untappd.com/", "-u": ["https://untappd.com/"],
"-b": "https://www.amazon.com/", "-b": ["https://www.amazon.com/"],
"-t": "https://app.todoist.com/app/task/{id}", "-t": ["https://app.todoist.com/app/task/{id}"],
"-i": "https://www.youtube.com/watch?v=", "-p": ["https://www.ipdb.plus/IPDb/puzzle.php?id="],
"-p": "https://www.ipdb.plus/IPDb/puzzle.php?id=", "-l": ["https://brickset.com/sets/"],
} }
EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",) EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",)
@ -49,6 +49,7 @@ MANUAL_SCROBBLE_FNS = {
"-w": "manual_scrobble_webpage", "-w": "manual_scrobble_webpage",
"-t": "manual_scrobble_task", "-t": "manual_scrobble_task",
"-p": "manual_scrobble_puzzle", "-p": "manual_scrobble_puzzle",
"-l": "manual_scrobble_brickset",
} }

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.19 on 2025-06-09 14:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("scrobbles", "0069_scrobble_puzzle_alter_scrobble_media_type"),
]
operations = [
migrations.RenameField(
model_name="scrobble",
old_name="brickset",
new_name="brick_set",
),
]

View File

@ -549,7 +549,7 @@ class Scrobble(TimeStampedModel):
LifeEvent, on_delete=models.DO_NOTHING, **BNULL LifeEvent, on_delete=models.DO_NOTHING, **BNULL
) )
mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL) mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
brickset = models.ForeignKey( brick_set = models.ForeignKey(
BrickSet, on_delete=models.DO_NOTHING, **BNULL BrickSet, on_delete=models.DO_NOTHING, **BNULL
) )
media_type = models.CharField( media_type = models.CharField(
@ -635,6 +635,14 @@ class Scrobble(TimeStampedModel):
for scrobble in scrobble_qs: for scrobble in scrobble_qs:
scrobbles_by_type[scrobble.media_type].append(scrobble) scrobbles_by_type[scrobble.media_type].append(scrobble)
if not scrobbles_by_type.get(scrobble.media_type + "_count"):
scrobbles_by_type[scrobble.media_type + "_count"] = 0
scrobbles_by_type[scrobble.media_type + "_count"] += 1
if not scrobbles_by_type.get(scrobble.media_type + "_time"):
scrobbles_by_type[scrobble.media_type + "_time"] = 0
scrobbles_by_type[scrobble.media_type + "_time"] += int(
(scrobble.elapsed_time)
)
return scrobbles_by_type return scrobbles_by_type
@ -1011,8 +1019,8 @@ class Scrobble(TimeStampedModel):
media_obj = self.life_event media_obj = self.life_event
if self.mood: if self.mood:
media_obj = self.mood media_obj = self.mood
if self.brickset: if self.brick_set:
media_obj = self.brickset media_obj = self.brick_set
if self.trail: if self.trail:
media_obj = self.trail media_obj = self.trail
if self.beer: if self.beer:

View File

@ -8,6 +8,7 @@ import pytz
from beers.models import Beer from beers.models import Beer
from boardgames.models import BoardGame from boardgames.models import BoardGame
from books.models import Book from books.models import Book
from bricksets.models import BrickSet
from dateutil.parser import parse from dateutil.parser import parse
from django.utils import timezone from django.utils import timezone
from locations.constants import LOCATION_PROVIDERS from locations.constants import LOCATION_PROVIDERS
@ -23,7 +24,7 @@ from scrobbles.constants import (
SCROBBLE_CONTENT_URLS, SCROBBLE_CONTENT_URLS,
) )
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.utils import convert_to_seconds from scrobbles.utils import convert_to_seconds, extract_domain
from sports.models import SportEvent from sports.models import SportEvent
from sports.thesportsdb import lookup_event_from_thesportsdb from sports.thesportsdb import lookup_event_from_thesportsdb
from tasks.models import Task from tasks.models import Task
@ -322,14 +323,12 @@ def manual_scrobble_from_url(
we know about the content type, and routes it to the appropriate media we know about the content type, and routes it to the appropriate media
scrobbler. Otherwise, return nothing.""" scrobbler. Otherwise, return nothing."""
content_key = "" content_key = ""
try: domain = extract_domain(url)
domain = url.split("//")[-1].split("/")[0]
except IndexError:
domain = None
for key, content_url in SCROBBLE_CONTENT_URLS.items(): for key, content_urls in SCROBBLE_CONTENT_URLS.items():
if domain in content_url: for content_url in content_urls:
content_key = key if domain in content_url:
content_key = key
item_id = None item_id = None
if not content_key: if not content_key:
@ -343,8 +342,10 @@ def manual_scrobble_from_url(
except IndexError: except IndexError:
pass pass
if content_key == "-i": if content_key == "-i" and "v=" in url:
item_id = url.split("v=")[1].split("&")[0] item_id = url.split("v=")[1].split("&")[0]
elif content_key == "-i" and "title/tt" in url:
item_id = "tt" + str(item_id)
scrobble_fn = MANUAL_SCROBBLE_FNS[content_key] scrobble_fn = MANUAL_SCROBBLE_FNS[content_key]
return eval(scrobble_fn)(item_id, user_id, action=action) return eval(scrobble_fn)(item_id, user_id, action=action)
@ -500,6 +501,151 @@ def todoist_scrobble_task(
return scrobble return scrobble
def emacs_scrobble_update_task(
emacs_id: str, emacs_notes: dict, user_id: int
) -> Optional[Scrobble]:
scrobble = Scrobble.objects.filter(
in_progress=True,
user_id=user_id,
log__source_id=emacs_id,
log__source="emacs",
).first()
if not scrobble:
logger.info(
"[emacs_scrobble_update_task] no task found",
extra={
"emacs_notes": emacs_notes,
"user_id": user_id,
"media_type": Scrobble.MediaType.TASK,
},
)
return
notes_updated = False
for note in emacs_notes:
existing_note_ts = [
n.get("timestamp") for n in scrobble.log.get("notes", [])
]
if not scrobble.log.get('notes"'):
scrobble.log["notes"] = []
if note.get("timestamp") not in existing_note_ts:
scrobble.log["notes"].append(
{note.get("timestamp"): note.get("content")}
)
notes_updated = True
if notes_updated:
scrobble.save(update_fields=["log"])
logger.info(
"[emacs_scrobble_update_task] emacs note added",
extra={
"emacs_note": emacs_notes,
"user_id": user_id,
"media_type": Scrobble.MediaType.TASK,
},
)
return scrobble
def emacs_scrobble_task(
task_data: dict,
user_id: int,
started: bool = False,
stopped: bool = False,
) -> Scrobble | None:
prefix = ""
suffix = ""
source_id = task_data.get("source_id")
for label in task_data.get("labels"):
if label in TODOIST_TITLE_PREFIX_LABELS:
prefix = label
if label in TODOIST_TITLE_SUFFIX_LABELS:
suffix = label
if not prefix and not suffix:
logger.warning(
"Missing a prefix and suffix tag for task",
extra={"emacs_scrobble_task": task_data},
)
title = " ".join([prefix.capitalize(), suffix.capitalize()])
task = Task.find_or_create(title)
timestamp = pendulum.parse(task_data.get("updated_at", timezone.now()))
in_progress_scrobble = Scrobble.objects.filter(
user_id=user_id,
in_progress=True,
log__source_id=source_id,
log__source="orgmode",
task=task,
).last()
if not in_progress_scrobble and stopped:
logger.info(
"[emacs_scrobble_task] cannot stop already stopped task",
extra={
"emacs_id": source_id,
},
)
return
if in_progress_scrobble and started:
logger.info(
"[emacs_scrobble_task] cannot start already started task",
extra={
"emacs_id": source_id,
},
)
return in_progress_scrobble
# Finish an in-progress scrobble
if in_progress_scrobble and stopped:
logger.info(
"[emacs_scrobble_task] finishing",
extra={
"emacs_id": source_id,
},
)
in_progress_scrobble.stop(timestamp=timestamp, force_finish=True)
return in_progress_scrobble
if in_progress_scrobble:
return in_progress_scrobble
notes = task_data.pop("notes")
if notes:
task_data["notes"] = []
for note in notes:
task_data["notes"].append(
{note.get("timestamp"): note.get("content")}
)
scrobble_dict = {
"user_id": user_id,
"timestamp": timestamp,
"playback_position_seconds": 0,
"source": "Org-mode",
"log": task_data,
}
logger.info(
"[emacs_scrobble_task] creating",
extra={
"task_id": task.id,
"user_id": user_id,
"scrobble_dict": scrobble_dict,
"media_type": Scrobble.MediaType.TASK,
},
)
scrobble = Scrobble.create_or_update(task, user_id, scrobble_dict)
return scrobble
def manual_scrobble_task(url: str, user_id: int, action: Optional[str] = None): def manual_scrobble_task(url: str, user_id: int, action: Optional[str] = None):
source_id = re.findall(r"\d+", url)[0] source_id = re.findall(r"\d+", url)[0]
@ -708,3 +854,34 @@ def manual_scrobble_puzzle(
# TODO Kick out a process to enrich the media here, and in every scrobble event # TODO Kick out a process to enrich the media here, and in every scrobble event
return Scrobble.create_or_update(puzzle, user_id, scrobble_dict) return Scrobble.create_or_update(puzzle, user_id, scrobble_dict)
def manual_scrobble_brickset(
brickset_id: str, user_id: int, action: Optional[str] = None
):
brickset = BrickSet.find_or_create(brickset_id)
if not brickset:
logger.error(f"No brickset found for Brickset ID {brickset_id}")
return
scrobble_dict = {
"user_id": user_id,
"timestamp": timezone.now(),
"playback_position_seconds": 0,
"source": "Vrobbler",
"log": {"serial_scrobble_id": ""},
}
logger.info(
"[vrobbler-scrobble] brickset scrobble request received",
extra={
"brickset_id": brickset.id,
"user_id": user_id,
"scrobble_dict": scrobble_dict,
"media_type": Scrobble.MediaType.BRICKSET,
},
)
# TODO Kick out a process to enrich the media here, and in every scrobble event
# TODO Need to check for past scrobbles and auto populate serial scrobble id if possible
return Scrobble.create_or_update(brickset, user_id, scrobble_dict)

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from scrobbles import views from scrobbles import views
from tasks.webhooks import todoist_webhook from tasks.webhooks import todoist_webhook, emacs_webhook
app_name = "scrobbles" app_name = "scrobbles"
@ -36,11 +36,6 @@ urlpatterns = [
views.web_scrobbler_webhook, views.web_scrobbler_webhook,
name="web-scrobbler-webhook", name="web-scrobbler-webhook",
), ),
path(
"webhook/emacs/",
views.emacs_webhook,
name="emacs-webhook",
),
path( path(
"webhook/gps/", "webhook/gps/",
views.gps_webhook, views.gps_webhook,
@ -57,6 +52,13 @@ urlpatterns = [
name="mopidy-webhook", name="mopidy-webhook",
), ),
path("webhook/todoist/", todoist_webhook, name="todoist-webhook"), path("webhook/todoist/", todoist_webhook, name="todoist-webhook"),
path("webhook/emacs/", emacs_webhook, name="emacs_webhook"),
path("export/", views.export, name="export"),
path(
"imports/",
views.ScrobbleImportListView.as_view(),
name="import-detail",
),
path("export/", views.export, name="export"), path("export/", views.export, name="export"),
path( path(
"imports/", "imports/",

View File

@ -3,6 +3,7 @@ import logging
import re import re
from datetime import datetime, timedelta, tzinfo from datetime import datetime, timedelta, tzinfo
from sqlite3 import IntegrityError from sqlite3 import IntegrityError
from urllib.parse import urlparse
import pytz import pytz
from django.apps import apps from django.apps import apps
@ -17,9 +18,10 @@ from scrobbles.tasks import (
process_lastfm_import, process_lastfm_import,
process_retroarch_import, process_retroarch_import,
) )
from vrobbler.apps.scrobbles.notifications import NtfyNotification
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
from vrobbler.apps.scrobbles.notifications import NtfyNotification
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
@ -67,7 +69,8 @@ def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet:
return Scrobble.objects.filter(media_query, user=user) return Scrobble.objects.filter(media_query, user=user)
def get_recently_played_board_games(user: User) -> dict: ... def get_recently_played_board_games(user: User) -> dict:
...
def get_long_plays_in_progress(user: User) -> dict: def get_long_plays_in_progress(user: User) -> dict:
@ -187,8 +190,8 @@ def delete_zombie_scrobbles(dry_run=True):
def import_from_webdav_for_all_users(restart=False): def import_from_webdav_for_all_users(restart=False):
"""Grab a list of all users with WebDAV enabled and kickoff imports for them""" """Grab a list of all users with WebDAV enabled and kickoff imports for them"""
from scrobbles.models import KoReaderImport
from books.koreader import fetch_file_from_webdav from books.koreader import fetch_file_from_webdav
from scrobbles.models import KoReaderImport
# LastFmImport = apps.get_model("scrobbles", "LastFMImport") # LastFmImport = apps.get_model("scrobbles", "LastFMImport")
webdav_enabled_user_ids = UserProfile.objects.filter( webdav_enabled_user_ids = UserProfile.objects.filter(
@ -271,6 +274,7 @@ def get_file_md5_hash(file_path: str) -> str:
file_hash.update(chunk) file_hash.update(chunk)
return file_hash.hexdigest() return file_hash.hexdigest()
def deduplicate_tracks(commit=False) -> int: def deduplicate_tracks(commit=False) -> int:
from music.models import Track from music.models import Track
@ -293,7 +297,11 @@ def deduplicate_tracks(commit=False) -> int:
try: try:
other.delete() other.delete()
except IntegrityError as e: except IntegrityError as e:
print("could not delete ", other.id, f": IntegrityError {e}") print(
"could not delete ",
other.id,
f": IntegrityError {e}",
)
return len(dups) return len(dups)
@ -318,3 +326,13 @@ def send_stop_notifications_for_in_progress_scrobbles() -> int:
notifications_sent += 1 notifications_sent += 1
return notifications_sent return notifications_sent
def extract_domain(url):
parsed_url = urlparse(url)
domain = (
parsed_url.netloc.split(".")[-2]
+ "."
+ parsed_url.netloc.split(".")[-1]
)
return domain

View File

@ -524,35 +524,6 @@ def gps_webhook(request):
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
@csrf_exempt
@api_view(["POST"])
def emacs_webhook(request):
try:
data_dict = json.loads(request.data)
except TypeError:
data_dict = request.data
logger.info(
"[emacs_webhook] called",
extra={
"post_data": data_dict,
"user_id": 1,
},
)
# TODO Fix this so we have to authenticate!
user_id = 1
if request.user.id:
user_id = request.user.id
# scrobble = gpslogger_scrobble_location(data_dict, user_id)
# if not scrobble:
# return Response({}, status=status.HTTP_200_OK)
return Response({"post_data": post_data}, status=status.HTTP_200_OK)
@csrf_exempt @csrf_exempt
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
@api_view(["POST"]) @api_view(["POST"])

View File

@ -50,7 +50,8 @@ class Task(LongPlayScrobblableMixin):
url = "" url = ""
scrobble = self.scrobbles(user_id).first() scrobble = self.scrobbles(user_id).first()
if scrobble: if scrobble:
url = TODOIST_TASK_URL.format(id=scrobble.logdata.todoist_id) if scrobble.log.get("source") == "todoist":
url = TODOIST_TASK_URL.format(id=scrobble.logdata.todist_id)
return url return url
def subtitle_for_user(self, user_id): def subtitle_for_user(self, user_id):

View File

@ -1,16 +1,18 @@
import json
import logging import logging
import pendulum
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from profiles.models import UserProfile
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from scrobbles.scrobblers import ( from scrobbles.scrobblers import (
emacs_scrobble_task,
emacs_scrobble_update_task,
todoist_scrobble_task, todoist_scrobble_task,
todoist_scrobble_update_task, todoist_scrobble_update_task,
) )
from profiles.models import UserProfile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,7 +31,7 @@ def todoist_webhook(request):
todoist_type, todoist_event = post_data.get("event_name").split(":") todoist_type, todoist_event = post_data.get("event_name").split(":")
event_data = post_data.get("event_data", {}) event_data = post_data.get("event_data", {})
is_item_type = todoist_type == "item" is_item_type = todoist_type == "item"
is_note_type = todoist_type == "note" is_note_type = todoist_tyllll = "note"
new_labels = event_data.get("labels", []) new_labels = event_data.get("labels", [])
old_labels = ( old_labels = (
post_data.get("event_data_extra", {}) post_data.get("event_data_extra", {})
@ -118,3 +120,54 @@ def todoist_webhook(request):
extra={"scrobble_id": scrobble.id}, extra={"scrobble_id": scrobble.id},
) )
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
@csrf_exempt
@permission_classes([IsAuthenticated])
@api_view(["POST"])
def emacs_webhook(request):
try:
post_data = json.loads(request.data)
except TypeError:
post_data = request.data
logger.info(
"[emacs_webhook] called",
extra={"post_data": post_data},
)
task_in_progress = post_data.get("state") == "STRT"
task_stopped = post_data.get("state") == "DONE"
post_data["source_id"] = post_data.pop("emacs_id")
# TODO: Figure out why token auth is not working
user_id = request.user.id
if not user_id:
user_id = 1
scrobble = None
if post_data.get("source_id"):
scrobble = emacs_scrobble_task(
post_data, user_id, started=task_in_progress, stopped=task_stopped
)
if scrobble and task_in_progress and post_data.get("notes"):
emacs_scrobble_update_task(
post_data.get("source_id"),
post_data.get("notes"),
user_id,
)
if not scrobble:
logger.info(
"[emacs_webhook] finished with no note or task found",
extra={"scrobble_id": None},
)
return Response(
{"error": "No scrobble found to be updated"},
status=status.HTTP_304_NOT_MODIFIED,
)
logger.info(
"[emacs_webhook] finished",
extra={"scrobble_id": scrobble.id},
)
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)

View File

@ -57,6 +57,8 @@ class VideoMetadata:
def as_dict_with_cover_and_genres(self) -> tuple: def as_dict_with_cover_and_genres(self) -> tuple:
video_dict = vars(self) video_dict = vars(self)
cover = video_dict.pop("cover_url") cover = None
if "cover_url" in video_dict.keys():
cover = video_dict.pop("cover_url")
genres = video_dict.pop("genres") genres = video_dict.pop("genres")
return video_dict, cover, genres return video_dict, cover, genres

View File

@ -9,7 +9,7 @@
<div class="col-md"> <div class="col-md">
{% if Track %} {% if Track %}
<h2>Music</h2> <h2>Music</h2>
{% with scrobbles=Track %} {% with scrobbles=Track count=Track_count time=Track_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
@ -18,63 +18,70 @@
{% if Task %} {% if Task %}
<h2>Latest tasks</h2> <h2>Latest tasks</h2>
{% with scrobbles=Task %} {% with scrobbles=Task count=Task_count time=Task_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if Video %} {% if Video %}
<h2>Videos</h2> <h2>Videos</h2>
{% with scrobbles=Video %} {% with scrobbles=Video count=Video_count time=Video_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if WebPage %} {% if WebPage %}
<h4>Web pages</h4> <h4>Web pages</h4>
{% with scrobbles=WebPage %} {% with scrobbles=WebPage count=WebPage_count time=WebPage_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if SportEvent %} {% if SportEvent %}
<h2>Sports</h2> <h2>Sports</h2>
{% with scrobbles=SportEvent %} {% with scrobbles=SportEvent count=SportEvent_count time=SportEvent_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if PodcastEpisode %} {% if PodcastEpisode %}
<h2>Latest podcasts</h2> <h2>Latest podcasts</h2>
{% with scrobbles=PodcastEpisode %} {% with scrobbles=PodcastEpisode count=PodcastEpisode_count time=PodcastEpisode_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if VideoGame %} {% if VideoGame %}
<h4>Video games</h4> <h4>Video games</h4>
{% with scrobbles=VideoGame %} {% with scrobbles=VideoGame count=VideoGame_count time=VideoGame_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if BoardGame %} {% if BoardGame %}
<h4>Board games</h4> <h4>Board games</h4>
{% with scrobbles=BoardGame %} {% with scrobbles=BoardGame count=BoardGame_count time=BoardGame_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if Beer %} {% if Beer %}
<h4>Beers</h4> <h4>Beers</h4>
{% with scrobbles=Beer %} {% with scrobbles=Beer count=Beer_count time=Beer_time %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if BrickSet %}
<h4>Brick sets</h4>
{% with scrobbles=BrickSet count=BrickSet_count time=BrickSet_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if Book %} {% if Book %}
<h4>Books</h4> <h4>Books</h4>
{% with scrobbles=Book %} {% with scrobbles=Book count=Book_count time=Book_time %}
{% include "scrobbles/_scrobble_table.html" %} {% include "scrobbles/_scrobble_table.html" %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
{% load humanize %} {% load humanize %}
{% load naturalduration %} {% load naturalduration %}
<tr> <tr {% if scrobble.in_progress %}class="in-progress"{% endif %}>
<td>{% if scrobble.in_progress %}{{scrobble.media_obj.strings.verb}} now | <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>{% else %}{{scrobble.timestamp|naturaltime}}{% endif %}</td> <td>{% if scrobble.in_progress %}{{scrobble.media_obj.strings.verb}} now | <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>{% else %}{{scrobble.timestamp|naturaltime}}{% endif %}</td>
<td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj|truncatechars_html:50}}</a></td> <td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj|truncatechars_html:45}}</a></td>
<td>{{scrobble.elapsed_time|natural_duration}}</td> <td>{{scrobble.elapsed_time|natural_duration}}</td>
</tr> </tr>

View File

@ -4,6 +4,7 @@
<div class="tab-pane fade show" id="latest-beers" role="tabpanel" <div class="tab-pane fade show" id="latest-beers" role="tabpanel"
aria-labelledby="latest-beers-tab"> aria-labelledby="latest-beers-tab">
<div class="table-responsive"> <div class="table-responsive">
{{count}} scrobble {% if time %}| {{time|natural_duration}}{% endif %}
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>

View File

@ -43,6 +43,10 @@
background:rgba(0,0,0,0.4); background:rgba(0,0,0,0.4);
} }
.in-progress {
background: #CCFFFF;
}
.in-progress a { color: black }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -62,6 +62,7 @@
<td>{{scrobble.timestamp}}</td> <td>{{scrobble.timestamp}}</td>
<td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td> <td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td>
<td>{{scrobble.source}}</td> <td>{{scrobble.source}}</td>
<td>{{scrobble.log.notes}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>