Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bef7e683c5 | |||
| ec219ef3ea | |||
| dcc7229e90 |
18
PROJECT.org
18
PROJECT.org
@ -93,7 +93,7 @@ fetching and simple saving.
|
|||||||
:LOGBOOK:
|
:LOGBOOK:
|
||||||
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||||
:END:
|
:END:
|
||||||
* Backlog [-1/13] :vrobbler:project:personal:
|
* Backlog [0/13] :vrobbler:project:personal:
|
||||||
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
||||||
@ -509,6 +509,22 @@ needed import celery task. This is how the WebDAV celery task currently works.
|
|||||||
This would also be an opporunity to clean up the code around WebDAV imports
|
This would also be an opporunity to clean up the code around WebDAV imports
|
||||||
and make them more re-usable for other import services.
|
and make them more re-usable for other import services.
|
||||||
|
|
||||||
|
* Version 45.1 [1/1]
|
||||||
|
** DONE [#B] Mopidy favorites or monthly playlist adds should look at all scrobbles :bug:mopidy:favorites:tracks:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 0be7d11e-e268-2fd5-836a-e5b4d210e0fa
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
When favoriting a track and trying to add it to the Moidy favorite playlist, it
|
||||||
|
sometimes happens that one scrobble did not come from Mopidy, but an earlier or
|
||||||
|
later one did.
|
||||||
|
|
||||||
|
Can we scan all the scrobbles of the track for a given user to see if any have
|
||||||
|
`mopidy_uri` in the log and if so, use that to send along to Mopidy?
|
||||||
|
|
||||||
|
|
||||||
* Version 45.0 [1/1]
|
* Version 45.0 [1/1]
|
||||||
** DONE [#B] Add ability to add mopidy tracks to Monthly playlists :feature:favorites:tracks:
|
** DONE [#B] Add ability to add mopidy tracks to Monthly playlists :feature:favorites:tracks:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
8
justfile
8
justfile
@ -15,9 +15,11 @@ celery:
|
|||||||
celery-beat:
|
celery-beat:
|
||||||
poetry run celery -A vrobbler beat -l info
|
poetry run celery -A vrobbler beat -l info
|
||||||
|
|
||||||
release kind="minor":
|
|
||||||
poetry run python scripts/release.py {{kind}}
|
|
||||||
|
|
||||||
push:
|
push:
|
||||||
git push && git push gitea
|
git push && git push gitea
|
||||||
git push --tags && git push --tags gitea
|
git push --tags && git push --tags gitea
|
||||||
|
|
||||||
|
release kind="minor":
|
||||||
|
poetry run python scripts/release.py {{kind}}
|
||||||
|
@push
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "45.0"
|
version = "45.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -576,3 +576,91 @@ def remove_favorite_from_mopidy_playlist(user_id, track_id):
|
|||||||
track=track,
|
track=track,
|
||||||
)
|
)
|
||||||
remove_track_from_mopidy_favorites_playlist(proxy)
|
remove_track_from_mopidy_favorites_playlist(proxy)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def add_scrobble_to_mopidy_queue(scrobble_id):
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.filter(id=scrobble_id).first()
|
||||||
|
if not scrobble:
|
||||||
|
return
|
||||||
|
|
||||||
|
profile = scrobble.user.profile
|
||||||
|
mopidy_url = profile.mopidy_api_url
|
||||||
|
if not mopidy_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||||
|
track = scrobble.track if scrobble.media_type == "Track" else None
|
||||||
|
if not mopidy_uri and track:
|
||||||
|
sibling = (
|
||||||
|
Scrobble.objects.filter(track=track, user=scrobble.user)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
.iterator()
|
||||||
|
)
|
||||||
|
for s in sibling:
|
||||||
|
uri = (s.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||||
|
if uri:
|
||||||
|
mopidy_uri = uri
|
||||||
|
break
|
||||||
|
|
||||||
|
if not mopidy_uri:
|
||||||
|
logger.warning(
|
||||||
|
"No Mopidy URI found for scrobble",
|
||||||
|
extra={"scrobble_id": scrobble_id, "user_id": scrobble.user_id},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
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"):
|
||||||
|
logger.error(
|
||||||
|
"Mopidy error adding to queue",
|
||||||
|
extra={"error": rpc_result["error"], "scrobble_id": scrobble_id},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Added track to Mopidy queue",
|
||||||
|
extra={"scrobble_id": scrobble_id, "mopidy_uri": mopidy_uri},
|
||||||
|
)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(
|
||||||
|
"Failed to add track to Mopidy queue",
|
||||||
|
extra={"scrobble_id": scrobble_id, "error": str(e)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def add_scrobble_to_mopidy_monthly_playlist(scrobble_id):
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
from scrobbles.utils import add_track_to_mopidy_monthly_playlist
|
||||||
|
|
||||||
|
scrobble = Scrobble.objects.filter(id=scrobble_id).first()
|
||||||
|
if not scrobble:
|
||||||
|
return
|
||||||
|
|
||||||
|
track = scrobble.track if scrobble.media_type == "Track" else None
|
||||||
|
if track:
|
||||||
|
sibling = (
|
||||||
|
Scrobble.objects.filter(track=track, user=scrobble.user)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
.iterator()
|
||||||
|
)
|
||||||
|
for s in sibling:
|
||||||
|
if (s.log or {}).get("raw_data", {}).get("mopidy_uri"):
|
||||||
|
scrobble = s
|
||||||
|
break
|
||||||
|
|
||||||
|
add_track_to_mopidy_monthly_playlist(scrobble)
|
||||||
|
|||||||
@ -468,9 +468,22 @@ def _ensure_mopidy_playlist(profile):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def add_track_to_mopidy_favorites_playlist(favorite):
|
def _scrobble_with_mopidy_uri(track, user):
|
||||||
|
"""Find a scrobble for this track+user that has a mopidy_uri in its log."""
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
for scrobble in (
|
||||||
|
Scrobble.objects.filter(track=track, user=user)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
.iterator()
|
||||||
|
):
|
||||||
|
raw_data = scrobble.log.get("raw_data") or {}
|
||||||
|
if raw_data.get("mopidy_uri"):
|
||||||
|
return scrobble
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_track_to_mopidy_favorites_playlist(favorite):
|
||||||
if favorite.media_type != "Track" or not favorite.track:
|
if favorite.media_type != "Track" or not favorite.track:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -479,15 +492,7 @@ def add_track_to_mopidy_favorites_playlist(favorite):
|
|||||||
return
|
return
|
||||||
|
|
||||||
track = favorite.track
|
track = favorite.track
|
||||||
scrobble = (
|
scrobble = _scrobble_with_mopidy_uri(track, favorite.user)
|
||||||
Scrobble.objects.filter(
|
|
||||||
track=track,
|
|
||||||
user=favorite.user,
|
|
||||||
log__raw_data__mopidy_uri__isnull=False,
|
|
||||||
)
|
|
||||||
.order_by("-timestamp")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not scrobble:
|
if not scrobble:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@ -496,7 +501,7 @@ def add_track_to_mopidy_favorites_playlist(favorite):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
mopidy_uri = scrobble.log["raw_data"]["mopidy_uri"]
|
mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist = _ensure_mopidy_playlist(profile)
|
playlist = _ensure_mopidy_playlist(profile)
|
||||||
@ -557,8 +562,6 @@ def resubmit_favorites_to_mopidy(user):
|
|||||||
|
|
||||||
|
|
||||||
def remove_track_from_mopidy_favorites_playlist(favorite):
|
def remove_track_from_mopidy_favorites_playlist(favorite):
|
||||||
from scrobbles.models import Scrobble
|
|
||||||
|
|
||||||
if favorite.media_type != "Track" or not favorite.track:
|
if favorite.media_type != "Track" or not favorite.track:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -567,15 +570,7 @@ def remove_track_from_mopidy_favorites_playlist(favorite):
|
|||||||
return
|
return
|
||||||
|
|
||||||
track = favorite.track
|
track = favorite.track
|
||||||
scrobble = (
|
scrobble = _scrobble_with_mopidy_uri(track, favorite.user)
|
||||||
Scrobble.objects.filter(
|
|
||||||
track=track,
|
|
||||||
user=favorite.user,
|
|
||||||
log__raw_data__mopidy_uri__isnull=False,
|
|
||||||
)
|
|
||||||
.order_by("-timestamp")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not scrobble:
|
if not scrobble:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@ -584,7 +579,7 @@ def remove_track_from_mopidy_favorites_playlist(favorite):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
mopidy_uri = scrobble.log["raw_data"]["mopidy_uri"]
|
mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist = _ensure_mopidy_playlist(profile)
|
playlist = _ensure_mopidy_playlist(profile)
|
||||||
@ -655,7 +650,7 @@ def add_track_to_mopidy_monthly_playlist(scrobble):
|
|||||||
if not pattern or not profile.mopidy_api_url:
|
if not pattern or not profile.mopidy_api_url:
|
||||||
return
|
return
|
||||||
|
|
||||||
mopidy_uri = scrobble.log.get("raw_data", {}).get("mopidy_uri")
|
mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||||
if not mopidy_uri:
|
if not mopidy_uri:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@ -992,46 +992,11 @@ def add_to_mopidy_queue(request, uuid):
|
|||||||
)
|
)
|
||||||
return redirect("scrobbles:detail", uuid=uuid)
|
return redirect("scrobbles:detail", uuid=uuid)
|
||||||
|
|
||||||
raw_data = scrobble.log.get("raw_data", {})
|
from scrobbles.tasks import add_scrobble_to_mopidy_queue as task
|
||||||
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}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
task.delay(scrobble.id)
|
||||||
|
msg = f'Adding "{scrobble.media_obj}" to Mopidy queue.'
|
||||||
|
messages.add_message(request, messages.SUCCESS, msg)
|
||||||
return redirect("scrobbles:detail", uuid=uuid)
|
return redirect("scrobbles:detail", uuid=uuid)
|
||||||
|
|
||||||
|
|
||||||
@ -1055,13 +1020,13 @@ def add_to_mopidy_monthly_playlist(request, uuid):
|
|||||||
now = now_user_timezone(profile)
|
now = now_user_timezone(profile)
|
||||||
playlist_name = DateFormat(now).format(pattern)
|
playlist_name = DateFormat(now).format(pattern)
|
||||||
|
|
||||||
from scrobbles.utils import add_track_to_mopidy_monthly_playlist
|
from scrobbles.tasks import add_scrobble_to_mopidy_monthly_playlist as task
|
||||||
|
|
||||||
add_track_to_mopidy_monthly_playlist(scrobble)
|
task.delay(scrobble.id)
|
||||||
messages.add_message(
|
messages.add_message(
|
||||||
request,
|
request,
|
||||||
messages.SUCCESS,
|
messages.SUCCESS,
|
||||||
f'Added "{scrobble.media_obj}" to monthly playlist "{playlist_name}".',
|
f'Adding "{scrobble.media_obj}" to monthly playlist "{playlist_name}".',
|
||||||
)
|
)
|
||||||
return redirect("scrobbles:detail", uuid=uuid)
|
return redirect("scrobbles:detail", uuid=uuid)
|
||||||
|
|
||||||
@ -1275,6 +1240,17 @@ class ScrobbleDetailView(DetailView):
|
|||||||
user=self.request.user, **{fk_field: media_obj}
|
user=self.request.user, **{fk_field: media_obj}
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
if media_type == "Track" and media_obj:
|
||||||
|
scrobbles = Scrobble.objects.filter(
|
||||||
|
track=media_obj, user=self.object.user
|
||||||
|
).order_by("-timestamp")[:20]
|
||||||
|
context["has_mopidy_uri"] = any(
|
||||||
|
(s.log or {}).get("raw_data", {}).get("mopidy_uri")
|
||||||
|
for s in scrobbles
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context["has_mopidy_uri"] = False
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@
|
|||||||
<h2>{{ object.logdata.title }}</h2>
|
<h2>{{ object.logdata.title }}</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h3 class="text-muted">{{ object.local_timestamp }}</h3>
|
<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 %}
|
{% if object.media_type == "Track" and has_mopidy_uri and user.profile.mopidy_api_url %}
|
||||||
<form method="post" action="{% url 'scrobbles:add-to-mopidy-queue' object.uuid %}" class="mb-1">
|
<form method="post" action="{% url 'scrobbles:add-to-mopidy-queue' object.uuid %}" class="mb-1">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button>
|
<button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user