Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89817110de | |||
| ee01e3d8df | |||
| a70343d6f3 | |||
| 3e72042c24 | |||
| 087c7775ae | |||
| 3f71065ad6 | |||
| 801672124f | |||
| 811e9c1ce9 | |||
| 415b32bdc7 | |||
| 22319c807a |
@ -127,7 +127,7 @@ jobs:
|
||||
pip uninstall -y vrobbler
|
||||
pip install /var/lib/vrobbler/dist/*.whl
|
||||
rm -f /var/lib/vrobbler/dist/*.whl
|
||||
python -c "import vrobbler; print(f'vrobbler {vrobbler.__version__} installed OK')"
|
||||
python3 -c "import vrobbler; print(f'vrobbler {vrobbler.__version__} installed OK')"
|
||||
vrobbler migrate
|
||||
vrobbler collectstatic --noinput
|
||||
immortalctl restart vrobbler-celery && immortalctl restart vrobbler-celerybeat && immortalctl restart vrobbler
|
||||
|
||||
73
PROJECT.org
73
PROJECT.org
@ -505,7 +505,77 @@ different. And the same for comments. If a comment (by timestamp key) is
|
||||
different in the webhook than what's in the scrobble.log, update the comment in
|
||||
the scrobble.log
|
||||
|
||||
** TODO [#C] Fix bug where Weigh-in imports do not set title :bug:tasks:scale:
|
||||
** TODO [#A] Ignore tag 'inprogress' for Tasks :bug:tasks:tags:
|
||||
|
||||
*** Description
|
||||
|
||||
When scrobbling tasks from Todoist, the tag `inprogress` is always in the
|
||||
payload, because that's how we parse tasks starting from the Todoist webhooks.
|
||||
|
||||
But we don't really need anything tagged as `inprogress` Can we ignore this tag
|
||||
when applying tags to Task scrobbles coming from Todoist?`
|
||||
|
||||
* Version 42.0 [1/1]
|
||||
** DONE [#B] Add ability to add track to current Mopidy queue :feature:mopidy:tracks:
|
||||
:PROPERTIES:
|
||||
:ID: 79d5b580-4ea6-461b-4c6c-2c950d8b3e4c
|
||||
:END:
|
||||
|
||||
|
||||
* Version 41.0 [5/5]
|
||||
** DONE [#B] For any scrobble detail page with notes display them better :templates:notes:scrobbles:
|
||||
:PROPERTIES:
|
||||
:ID: c0dcf9da-227f-4a22-bcd9-9d46053607d9
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
Currently notes are displayed as little post-it notes. This is cute, but not terribly useful.
|
||||
|
||||
We should update note rendering to be a simple newest to oldest display in a
|
||||
single column with the timestamp has a small header, and the content rendered as
|
||||
markdown with a small bar or horizontal divider marking them from the next note.
|
||||
|
||||
** DONE [#A] Imports should send notifications :feature:notifications:imports:
|
||||
:PROPERTIES:
|
||||
:ID: 6f78f8d5-ecaa-4d8a-a666-ae4e27653191
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
Currently importing board games sends out a ntfy message when a scrobble is
|
||||
created.
|
||||
|
||||
We should do the same thing for other import types; namely: gpx, ebird, and scale.
|
||||
|
||||
** DONE [#A] Board game imports send duplicate ntfy message :bug:notifications:boardgames:
|
||||
:PROPERTIES:
|
||||
:ID: 8f067432-0399-4b79-9e93-727edcccedbd
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
When a board game scrobble is created via a bgstats import, ntfy messages are
|
||||
sent.
|
||||
|
||||
But right now they are duplicated (two are sent at the same time). Can we review
|
||||
the code to see why this is happening and fix it?
|
||||
|
||||
** DONE [#A] Too many geolocation notifications go out :bug:notifications:geolocations:
|
||||
:PROPERTIES:
|
||||
:ID: 6357ad7a-fe4e-49dd-a063-55d87e459c17
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
Currently ntfy gets overwhelemed when there's more than a hundred or so messages left in a queue on a client.
|
||||
|
||||
It would be nice if we could not spam ntfy, and this is especially true with Geolocations, where we really
|
||||
don't need to alert folks unless they have a named Geolocation (has a title). Can we adjust the ntfy
|
||||
sending for Geolocations to only send if the scrobbled location has a title?
|
||||
|
||||
|
||||
** DONE [#C] Fix bug where Weigh-in imports do not set title :bug:tasks:scale:
|
||||
:PROPERTIES:
|
||||
:ID: 622e354a-8e66-4ecd-9e1c-a53f0a2ec362
|
||||
:END:
|
||||
@ -516,6 +586,7 @@ Currently when we import a scale CSV row and create data, the title is left
|
||||
blank which makes it look funny in a list view. Let's save the weight as the
|
||||
title of the Weigh-in task.
|
||||
|
||||
|
||||
* Version 40.2 [1/1]
|
||||
** DONE [#A] Try fixing deploy bugs again :tooling:releases:bug:
|
||||
:PROPERTIES:
|
||||
|
||||
1
justfile
1
justfile
@ -19,5 +19,4 @@ release kind="minor":
|
||||
poetry run python scripts/release.py {{kind}}
|
||||
|
||||
push:
|
||||
git push && git push gitea
|
||||
git push --tags && git push --tags gitea
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "40.2"
|
||||
version = "42.0"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model
|
||||
|
||||
from birds.models import Bird, BirdSightingEntry, BirdSightingLogData, BirdingLocation
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.notifications import ScrobbleNtfyNotification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
@ -183,4 +184,6 @@ def import_birding_csv(file_path, user_id):
|
||||
|
||||
created = Scrobble.objects.bulk_create(new_scrobbles)
|
||||
logger.info(f"Created {len(created)} birding scrobbles")
|
||||
for scrobble in created:
|
||||
ScrobbleNtfyNotification(scrobble).send()
|
||||
return created
|
||||
|
||||
@ -31,6 +31,7 @@ class UserProfileForm(forms.ModelForm):
|
||||
"webdav_auto_import",
|
||||
"ntfy_url",
|
||||
"ntfy_enabled",
|
||||
"mopidy_api_url",
|
||||
"redirect_to_webpage",
|
||||
"enable_public_widgets",
|
||||
"widget_custom_css",
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.30 on 2026-06-04 16:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0032_userprofile_weigh_in_units"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="mopidy_api_url",
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@ -64,6 +64,8 @@ class UserProfile(TimeStampedModel):
|
||||
ntfy_url = models.CharField(max_length=255, **BNULL)
|
||||
ntfy_enabled = models.BooleanField(default=False)
|
||||
|
||||
mopidy_api_url = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
redirect_to_webpage = models.BooleanField(default=True)
|
||||
|
||||
enable_public_widgets = models.BooleanField(default=False)
|
||||
|
||||
@ -83,43 +83,110 @@ class BaseLogData(JSONDataclass):
|
||||
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _format_timestamp(ts: str) -> str:
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
|
||||
cleaned = ts.strip().strip("[]")
|
||||
dt = None
|
||||
|
||||
if cleaned.isdigit():
|
||||
try:
|
||||
seconds = int(cleaned)
|
||||
dt = datetime.fromtimestamp(seconds, tz=timezone.utc)
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
if dt is None:
|
||||
for fmt in [
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ",
|
||||
"%Y-%m-%dT%H:%M:%SZ",
|
||||
"%Y-%m-%dT%H:%M:%S.%f",
|
||||
"%Y-%m-%dT%H:%M:%S",
|
||||
]:
|
||||
try:
|
||||
dt = datetime.strptime(cleaned, fmt)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if dt is None:
|
||||
m = re.match(
|
||||
r"(\d{4}-\d{2}-\d{2})\s+\w{3}\s+(\d{2}:\d{2})", cleaned
|
||||
)
|
||||
if m:
|
||||
try:
|
||||
dt = datetime.strptime(
|
||||
f"{m.group(1)} {m.group(2)}", "%Y-%m-%d %H:%M"
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if dt is None:
|
||||
try:
|
||||
dt = datetime.fromisoformat(cleaned)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if dt:
|
||||
return dt.strftime("%b %-d, %Y %-I:%M %p")
|
||||
return ts
|
||||
|
||||
def notes_as_html(self) -> str:
|
||||
import html
|
||||
import bleach
|
||||
import markdown
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
if not self.notes:
|
||||
return ""
|
||||
|
||||
html_notes = []
|
||||
md = markdown.Markdown(extensions=["extra"])
|
||||
allowed_tags = [
|
||||
"p", "br", "strong", "em", "a", "ul", "ol", "li",
|
||||
"code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"hr", "img",
|
||||
]
|
||||
|
||||
note_items = []
|
||||
|
||||
if isinstance(self.notes, dict):
|
||||
for ts, text in self.notes.items():
|
||||
note_text = " ".join(text.strip().split())
|
||||
html_notes.append(
|
||||
f'<div class="sticky-note">{ts}: {note_text}</div>'
|
||||
)
|
||||
for ts, text in sorted(self.notes.items(), reverse=True):
|
||||
note_items.append((ts, text.strip()))
|
||||
elif isinstance(self.notes, str):
|
||||
for line in self.notes.split("\n"):
|
||||
if line.strip():
|
||||
escaped_line = html.escape(line)
|
||||
html_notes.append(f'<div class="sticky-note">{escaped_line}</div>')
|
||||
note_items.append((None, line.strip()))
|
||||
elif isinstance(self.notes, list):
|
||||
for note in self.notes:
|
||||
if isinstance(note, dict):
|
||||
timestamp, note_text = next(iter(note.items()))
|
||||
note_text = " ".join(note_text.strip().split())
|
||||
html_notes.append(
|
||||
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
|
||||
)
|
||||
note_items.append((timestamp, note_text.strip()))
|
||||
elif isinstance(note, str):
|
||||
for line in note.split("\n"):
|
||||
if line.strip():
|
||||
escaped_line = html.escape(line)
|
||||
html_notes.append(
|
||||
f'<div class="sticky-note">{escaped_line}</div>'
|
||||
)
|
||||
note_items.append((None, line.strip()))
|
||||
|
||||
return mark_safe("".join(html_notes))
|
||||
html_parts = []
|
||||
for i, (ts, text) in enumerate(note_items):
|
||||
if i > 0:
|
||||
html_parts.append('<hr class="note-divider">')
|
||||
|
||||
ts_html = ""
|
||||
if ts:
|
||||
ts_html = f'<h5 class="note-timestamp">{self._format_timestamp(ts)}</h5>'
|
||||
|
||||
content_html = bleach.clean(
|
||||
md.convert(text),
|
||||
tags=allowed_tags,
|
||||
strip=True,
|
||||
)
|
||||
|
||||
html_parts.append(
|
||||
f'<div class="note-item">{ts_html}<div class="note-content">{content_html}</div></div>'
|
||||
)
|
||||
|
||||
return mark_safe("".join(html_parts))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@ -5,6 +5,7 @@ from datetime import datetime, timedelta
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.notifications import ScrobbleNtfyNotification
|
||||
from tasks.models import Task
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -85,6 +86,11 @@ def import_scale_csv(file_path, user_id):
|
||||
|
||||
log_dict["unit_type"] = user.profile.weigh_in_units
|
||||
|
||||
weight_val = log_dict.get("weight")
|
||||
if weight_val is not None:
|
||||
unit = "kg" if user.profile.weigh_in_units == "metric" else "lbs"
|
||||
log_dict["title"] = f"{weight_val} {unit}"
|
||||
|
||||
existing = Scrobble.objects.filter(
|
||||
timestamp=start,
|
||||
task=weigh_in,
|
||||
@ -110,4 +116,6 @@ def import_scale_csv(file_path, user_id):
|
||||
|
||||
created = Scrobble.objects.bulk_create(new_scrobbles)
|
||||
logger.info(f"Created {len(created)} weigh-in scrobbles")
|
||||
for scrobble in created:
|
||||
ScrobbleNtfyNotification(scrobble).send()
|
||||
return created
|
||||
|
||||
@ -12,6 +12,7 @@ from django.core.files import File
|
||||
|
||||
from locations.models import GeoLocation
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.notifications import ScrobbleNtfyNotification
|
||||
from trails.models import Trail, TrailLogData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -328,4 +329,6 @@ def import_trail_gpx(file_path, user_id, original_filename=None):
|
||||
|
||||
created = Scrobble.objects.bulk_create(new_scrobbles)
|
||||
logger.info(f"Created {len(created)} trail scrobbles")
|
||||
for scrobble in created:
|
||||
ScrobbleNtfyNotification(scrobble).send()
|
||||
return created
|
||||
|
||||
@ -1529,7 +1529,12 @@ class Scrobble(TimeStampedModel):
|
||||
scrobble_data: dict,
|
||||
) -> "Scrobble":
|
||||
scrobble = cls.objects.create(**scrobble_data)
|
||||
ScrobbleNtfyNotification(scrobble).send()
|
||||
if not (
|
||||
scrobble.media_type == cls.MediaType.GEO_LOCATION
|
||||
and scrobble.media_obj
|
||||
and not scrobble.media_obj.title
|
||||
):
|
||||
ScrobbleNtfyNotification(scrobble).send()
|
||||
return scrobble
|
||||
|
||||
def stop(self, timestamp=None, force_finish=False) -> None:
|
||||
|
||||
@ -562,7 +562,6 @@ def email_scrobble_board_game(
|
||||
scrobble.played_to_completion = True
|
||||
scrobble.save()
|
||||
scrobbles_created.append(scrobble)
|
||||
ScrobbleNtfyNotification(scrobble).send()
|
||||
|
||||
return scrobbles_created
|
||||
|
||||
|
||||
@ -158,6 +158,11 @@ urlpatterns = [
|
||||
views.ScrobbleDetailView.as_view(),
|
||||
name="detail",
|
||||
),
|
||||
path(
|
||||
"scrobbles/<slug:uuid>/add-to-mopidy-queue/",
|
||||
views.add_to_mopidy_queue,
|
||||
name="add-to-mopidy-queue",
|
||||
),
|
||||
path("scrobbles/<slug:uuid>/start/", views.scrobble_start, name="start"),
|
||||
path("scrobbles/<slug:uuid>/finish/", views.scrobble_finish, name="finish"),
|
||||
path("scrobbles/<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"),
|
||||
|
||||
@ -4,6 +4,8 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import requests
|
||||
|
||||
import pendulum
|
||||
import pytz
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@ -30,10 +32,11 @@ from django.http import (
|
||||
HttpResponseRedirect,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import DetailView, FormView, TemplateView
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.views.generic.list import ListView
|
||||
@ -963,6 +966,65 @@ def scrobble_cancel(request, uuid):
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
|
||||
|
||||
@require_POST
|
||||
def add_to_mopidy_queue(request, uuid):
|
||||
if not request.user.is_authenticated:
|
||||
return redirect("scrobbles:detail", uuid=uuid)
|
||||
|
||||
scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user)
|
||||
mopidy_url = request.user.profile.mopidy_api_url
|
||||
|
||||
if not mopidy_url:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"Mopidy API URL not configured in your profile settings.",
|
||||
)
|
||||
return redirect("scrobbles:detail", uuid=uuid)
|
||||
|
||||
raw_data = scrobble.log.get("raw_data", {})
|
||||
mopidy_uri = raw_data.get("mopidy_uri")
|
||||
logger.debug(mopidy_uri)
|
||||
|
||||
if not mopidy_uri:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"No Mopidy URI found for this scrobble.",
|
||||
)
|
||||
return redirect("scrobbles:detail", uuid=uuid)
|
||||
|
||||
rpc_url = mopidy_url.rstrip("/") + "/rpc"
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "core.tracklist.add",
|
||||
"params": {"uris": [mopidy_uri]},
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(rpc_url, json=payload, timeout=10)
|
||||
resp.raise_for_status()
|
||||
rpc_result = resp.json()
|
||||
if rpc_result.get("error"):
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
f'Mopidy error: {rpc_result["error"]}',
|
||||
)
|
||||
else:
|
||||
msg = f'Added "{scrobble.media_obj}" to Mopidy queue.'
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
except requests.RequestException as e:
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
f"Failed to contact Mopidy: {e}",
|
||||
)
|
||||
|
||||
return redirect("scrobbles:detail", uuid=uuid)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def export(request):
|
||||
|
||||
@ -76,33 +76,62 @@ class TaskLogData(BaseLogData):
|
||||
return separator.join(lines).encode("utf-8").decode("unicode_escape")
|
||||
|
||||
def notes_as_html(self) -> str:
|
||||
import bleach
|
||||
import markdown
|
||||
from django.utils.safestring import mark_safe
|
||||
from scrobbles.dataclasses import BaseLogData
|
||||
|
||||
if not self.notes:
|
||||
return ""
|
||||
|
||||
md = markdown.Markdown(extensions=["extra"])
|
||||
allowed_tags = [
|
||||
"p", "br", "strong", "em", "a", "ul", "ol", "li",
|
||||
"code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"hr", "img",
|
||||
]
|
||||
|
||||
notes = self.notes
|
||||
if isinstance(notes, dict):
|
||||
notes = [{k: v} for k, v in notes.items()]
|
||||
if isinstance(notes, str):
|
||||
notes = [notes]
|
||||
|
||||
html_notes = []
|
||||
note_items = []
|
||||
for note in notes:
|
||||
if isinstance(note, dict):
|
||||
timestamp, note_text = next(iter(note.items()))
|
||||
note_text = " ".join(note_text.strip().split())
|
||||
html_notes.append(
|
||||
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
|
||||
)
|
||||
note_items.append((timestamp, note_text.strip()))
|
||||
elif isinstance(note, str):
|
||||
escaped = note.encode("utf-8").decode("unicode_escape")
|
||||
for line in escaped.split("\n"):
|
||||
if line.strip():
|
||||
html_notes.append(f'<div class="sticky-note">{line}</div>')
|
||||
note_items.append((None, line.strip()))
|
||||
elif isinstance(note, list):
|
||||
for item in note:
|
||||
if isinstance(item, str):
|
||||
html_notes.append(f'<div class="sticky-note">{item}</div>')
|
||||
return "".join(html_notes)
|
||||
note_items.append((None, item.strip()))
|
||||
|
||||
html_parts = []
|
||||
for i, (ts, text) in enumerate(note_items):
|
||||
if i > 0:
|
||||
html_parts.append('<hr class="note-divider">')
|
||||
|
||||
ts_html = ""
|
||||
if ts:
|
||||
ts_html = f'<h5 class="note-timestamp">{BaseLogData._format_timestamp(ts)}</h5>'
|
||||
|
||||
content_html = bleach.clean(
|
||||
md.convert(text),
|
||||
tags=allowed_tags,
|
||||
strip=True,
|
||||
)
|
||||
|
||||
html_parts.append(
|
||||
f'<div class="note-item">{ts_html}<div class="note-content">{content_html}</div></div>'
|
||||
)
|
||||
|
||||
return mark_safe("".join(html_parts))
|
||||
|
||||
|
||||
class Task(LongPlayScrobblableMixin):
|
||||
|
||||
@ -168,6 +168,35 @@
|
||||
}
|
||||
#scrobble-form { width: 100% }
|
||||
|
||||
.notes-list {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.note-timestamp {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.note-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.note-divider {
|
||||
margin: 8px 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.sticky-notes-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -218,6 +247,9 @@
|
||||
a:not(.nav-link):not(.btn):not(.page-link):hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
a.badge {
|
||||
color: #fff !important;
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(odd) {
|
||||
background-color: color-mix(in srgb, var(--accent) 6%, #fff);
|
||||
}
|
||||
|
||||
@ -35,11 +35,18 @@
|
||||
|
||||
<h1>
|
||||
{% if object.media_type == "Video" %}🎬{% elif object.media_type == "Track" %}🎵{% elif object.media_type == "PodcastEpisode" %}🎙️{% elif object.media_type == "SportEvent" %}⚽{% elif object.media_type == "Book" %}📚{% elif object.media_type == "Paper" %}📄{% elif object.media_type == "VideoGame" %}🎮{% elif object.media_type == "BoardGame" %}🎲{% elif object.media_type == "GeoLocation" %}📍{% elif object.media_type == "Trail" %}🥾{% elif object.media_type == "Beer" %}🍺{% elif object.media_type == "Puzzle" %}🧩{% elif object.media_type == "Food" %}🍔{% elif object.media_type == "Task" %}✅{% elif object.media_type == "WebPage" %}🌐{% elif object.media_type == "LifeEvent" %}🎉{% elif object.media_type == "Mood" %}😊{% elif object.media_type == "BrickSet" %}🧱{% elif object.media_type == "Channel" %}📺{% endif %}
|
||||
{% if object.media_obj.get_absolute_url %}<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}</a>{% endif %}</h1>
|
||||
{% if object.media_obj.get_absolute_url %}<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}</a>{% endif %}
|
||||
</h1>
|
||||
{% if object.media_type == "Task" and object.logdata.title %}
|
||||
<h2>{{ object.logdata.title }}</h2>
|
||||
{% endif %}
|
||||
<h3 class="text-muted">{{ object.local_timestamp }}</h3>
|
||||
{% if object.media_type == "Track" and object.log.raw_data.mopidy_uri and user.profile.mopidy_api_url %}
|
||||
<form method="post" action="{% url 'scrobbles:add-to-mopidy-queue' object.uuid %}" class="mb-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if object.media_type == "Track" %}
|
||||
<p class="text-muted small">Source: {{ object.source }}{% if object.log.mopidy_source %} ({{ object.log.mopidy_source|capfirst }}){% endif %}</p>
|
||||
{% endif %}
|
||||
@ -115,8 +122,8 @@
|
||||
{% with notes_html=object.logdata.notes_as_html %}
|
||||
{% if notes_html %}
|
||||
<div class="mb-3">
|
||||
<h5>Notes</h5>
|
||||
<div class="sticky-notes-container">
|
||||
<h4>Notes</h4>
|
||||
<div class="notes-list">
|
||||
{{ notes_html|safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user