Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab728de75f | |||
| 04b7214795 | |||
| 479fee6a5c | |||
| 40a126cf8b | |||
| 83c02aa00f | |||
| 0f44df2b9b | |||
| 16d1dcc125 |
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "0.10.1"
|
version = "0.10.2"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ from scrobbles.models import Scrobble
|
|||||||
|
|
||||||
|
|
||||||
def build_scrobbles(client, request_data, num=7, spacing=2):
|
def build_scrobbles(client, request_data, num=7, spacing=2):
|
||||||
url = reverse('scrobbles:mopidy-websocket')
|
url = reverse('scrobbles:mopidy-webhook')
|
||||||
user = get_user_model().objects.create(username='Test User')
|
user = get_user_model().objects.create(username='Test User')
|
||||||
UserProfile.objects.create(user=user, timezone='US/Eastern')
|
UserProfile.objects.create(user=user, timezone='US/Eastern')
|
||||||
for i in range(num):
|
for i in range(num):
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from podcasts.models import Episode
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_get_not_allowed_from_mopidy(client, valid_auth_token):
|
def test_get_not_allowed_from_mopidy(client, valid_auth_token):
|
||||||
url = reverse('scrobbles:mopidy-websocket')
|
url = reverse('scrobbles:mopidy-webhook')
|
||||||
headers = {'Authorization': f'Token {valid_auth_token}'}
|
headers = {'Authorization': f'Token {valid_auth_token}'}
|
||||||
response = client.get(url, headers=headers)
|
response = client.get(url, headers=headers)
|
||||||
assert response.status_code == 405
|
assert response.status_code == 405
|
||||||
@ -18,7 +18,7 @@ def test_get_not_allowed_from_mopidy(client, valid_auth_token):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_bad_mopidy_request_data(client, valid_auth_token):
|
def test_bad_mopidy_request_data(client, valid_auth_token):
|
||||||
url = reverse('scrobbles:mopidy-websocket')
|
url = reverse('scrobbles:mopidy-webhook')
|
||||||
headers = {'Authorization': f'Token {valid_auth_token}'}
|
headers = {'Authorization': f'Token {valid_auth_token}'}
|
||||||
response = client.post(url, headers)
|
response = client.post(url, headers)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
@ -32,7 +32,7 @@ def test_bad_mopidy_request_data(client, valid_auth_token):
|
|||||||
def test_scrobble_mopidy_track(
|
def test_scrobble_mopidy_track(
|
||||||
client, mopidy_track_request_data, valid_auth_token
|
client, mopidy_track_request_data, valid_auth_token
|
||||||
):
|
):
|
||||||
url = reverse('scrobbles:mopidy-websocket')
|
url = reverse('scrobbles:mopidy-webhook')
|
||||||
headers = {'Authorization': f'Token {valid_auth_token}'}
|
headers = {'Authorization': f'Token {valid_auth_token}'}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url,
|
url,
|
||||||
@ -55,7 +55,7 @@ def test_scrobble_mopidy_same_track_different_album(
|
|||||||
mopidy_track_diff_album_request_data,
|
mopidy_track_diff_album_request_data,
|
||||||
valid_auth_token,
|
valid_auth_token,
|
||||||
):
|
):
|
||||||
url = reverse('scrobbles:mopidy-websocket')
|
url = reverse('scrobbles:mopidy-webhook')
|
||||||
headers = {'Authorization': f'Token {valid_auth_token}'}
|
headers = {'Authorization': f'Token {valid_auth_token}'}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url,
|
url,
|
||||||
@ -84,7 +84,7 @@ def test_scrobble_mopidy_same_track_different_album(
|
|||||||
def test_scrobble_mopidy_podcast(
|
def test_scrobble_mopidy_podcast(
|
||||||
client, mopidy_podcast_request_data, valid_auth_token
|
client, mopidy_podcast_request_data, valid_auth_token
|
||||||
):
|
):
|
||||||
url = reverse('scrobbles:mopidy-websocket')
|
url = reverse('scrobbles:mopidy-webhook')
|
||||||
headers = {'Authorization': f'Token {valid_auth_token}'}
|
headers = {'Authorization': f'Token {valid_auth_token}'}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url,
|
url,
|
||||||
|
|||||||
@ -181,10 +181,18 @@ class Track(ScrobblableMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('music:track_detail', kwargs={'slug': self.uuid})
|
return reverse('music:track_detail', kwargs={'slug': self.uuid})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self):
|
||||||
|
return self.artist
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mb_link(self):
|
def mb_link(self):
|
||||||
return f"https://musicbrainz.org/recording/{self.musicbrainz_id}"
|
return f"https://musicbrainz.org/recording/{self.musicbrainz_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info_link(self):
|
||||||
|
return self.mb_link
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(
|
||||||
cls, artist_dict: Dict, album_dict: Dict, track_dict: Dict
|
cls, artist_dict: Dict, album_dict: Dict, track_dict: Dict
|
||||||
|
|||||||
@ -45,6 +45,14 @@ class Episode(ScrobblableMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title}"
|
return f"{self.title}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self):
|
||||||
|
return self.podcast
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info_link(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(
|
||||||
cls, podcast_dict: Dict, producer_dict: Dict, episode_dict: Dict
|
cls, podcast_dict: Dict, producer_dict: Dict, episode_dict: Dict
|
||||||
|
|||||||
@ -24,6 +24,7 @@ def lookup_event_from_thesportsdb(event_id: str) -> dict:
|
|||||||
)
|
)
|
||||||
|
|
||||||
data_dict = {
|
data_dict = {
|
||||||
|
"EventId": event_id,
|
||||||
"ItemType": sport.default_event_type,
|
"ItemType": sport.default_event_type,
|
||||||
"Name": event.get('strEvent'),
|
"Name": event.get('strEvent'),
|
||||||
"AltName": event.get('strEventAlternate'),
|
"AltName": event.get('strEventAlternate'),
|
||||||
|
|||||||
@ -26,8 +26,20 @@ urlpatterns = [
|
|||||||
views.AudioScrobblerImportCreateView.as_view(),
|
views.AudioScrobblerImportCreateView.as_view(),
|
||||||
name='audioscrobbler-file-upload',
|
name='audioscrobbler-file-upload',
|
||||||
),
|
),
|
||||||
path('lastfm-import/', views.lastfm_import, name='lastfm-import'),
|
path(
|
||||||
path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
|
'lastfm-import/',
|
||||||
path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'),
|
views.lastfm_import,
|
||||||
|
name='lastfm-import',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'webhook/jellyfin/',
|
||||||
|
views.jellyfin_webhook,
|
||||||
|
name='jellyfin-webhook',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'webhook/mopidy/',
|
||||||
|
views.mopidy_webhook,
|
||||||
|
name='mopidy-webhook',
|
||||||
|
),
|
||||||
path('export/', views.export, name='export'),
|
path('export/', views.export, name='export'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -216,7 +216,7 @@ def lastfm_import(request):
|
|||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def jellyfin_websocket(request):
|
def jellyfin_webhook(request):
|
||||||
data_dict = request.data
|
data_dict = request.data
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -248,7 +248,7 @@ def jellyfin_websocket(request):
|
|||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def mopidy_websocket(request):
|
def mopidy_webhook(request):
|
||||||
try:
|
try:
|
||||||
data_dict = json.loads(request.data)
|
data_dict = json.loads(request.data)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-02-23 15:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sports', '0007_sport_default_event_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sportevent',
|
||||||
|
name='thesportsdb_id',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -15,8 +15,9 @@ BNULL = {"blank": True, "null": True}
|
|||||||
|
|
||||||
|
|
||||||
class SportEventType(models.TextChoices):
|
class SportEventType(models.TextChoices):
|
||||||
UNKNOWN = 'UK', _('Unknown')
|
UNKNOWN = 'UK', _('Event')
|
||||||
GAME = 'GA', _('Game')
|
GAME = 'GA', _('Game')
|
||||||
|
RACE = 'RA', _('Race')
|
||||||
MATCH = 'MA', _('Match')
|
MATCH = 'MA', _('Match')
|
||||||
|
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ class Round(TheSportsDbMixin):
|
|||||||
class SportEvent(ScrobblableMixin):
|
class SportEvent(ScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, 'SPORT_COMPLETION_PERCENT', 90)
|
COMPLETION_PERCENT = getattr(settings, 'SPORT_COMPLETION_PERCENT', 90)
|
||||||
|
|
||||||
|
thesportsdb_id = models.CharField(max_length=255, **BNULL)
|
||||||
event_type = models.CharField(
|
event_type = models.CharField(
|
||||||
max_length=2,
|
max_length=2,
|
||||||
choices=SportEventType.choices,
|
choices=SportEventType.choices,
|
||||||
@ -127,6 +129,18 @@ class SportEvent(ScrobblableMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("sports:event_detail", kwargs={'slug': self.uuid})
|
return reverse("sports:event_detail", kwargs={'slug': self.uuid})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self):
|
||||||
|
return self.round.season.league
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sportsdb_link(self):
|
||||||
|
return f"https://thesportsdb.com/event/{self.thesportsdb_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info_link(self):
|
||||||
|
return self.sportsdb_link
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, data_dict: Dict) -> "Event":
|
def find_or_create(cls, data_dict: Dict) -> "Event":
|
||||||
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
|
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
|
||||||
@ -208,6 +222,7 @@ class SportEvent(ScrobblableMixin):
|
|||||||
away_team, _created = Team.objects.get_or_create(**away_team_dict)
|
away_team, _created = Team.objects.get_or_create(**away_team_dict)
|
||||||
|
|
||||||
event_dict = {
|
event_dict = {
|
||||||
|
"thesportsdb_id": data_dict.get("EventId"),
|
||||||
"title": data_dict.get("Name"),
|
"title": data_dict.get("Name"),
|
||||||
"event_type": sport.default_event_type,
|
"event_type": sport.default_event_type,
|
||||||
"home_team": home_team,
|
"home_team": home_team,
|
||||||
|
|||||||
@ -69,10 +69,24 @@ class Video(ScrobblableMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("videos:video_detail", kwargs={'slug': self.uuid})
|
return reverse("videos:video_detail", kwargs={'slug': self.uuid})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subtitle(self):
|
||||||
|
if self.tv_series:
|
||||||
|
return self.tv_series
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def imdb_link(self):
|
def imdb_link(self):
|
||||||
return f"https://www.imdb.com/title/{self.imdb_id}"
|
return f"https://www.imdb.com/title/{self.imdb_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info_link(self):
|
||||||
|
return self.imdb_link
|
||||||
|
|
||||||
|
@property
|
||||||
|
def link(self):
|
||||||
|
return self.imdb_link
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, data_dict: Dict) -> "Video":
|
def find_or_create(cls, data_dict: Dict) -> "Video":
|
||||||
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
|
"""Given a data dict from Jellyfin, does the heavy lifting of looking up
|
||||||
|
|||||||
@ -173,12 +173,14 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
"allauth.account.auth_backends.AuthenticationBackend",
|
"allauth.account.auth_backends.AuthenticationBackend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# We have to ignore content negotiation because Jellyfin is a bad actor
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
|
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
],
|
],
|
||||||
|
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'vrobbler.negotiation.IgnoreClientContentNegotiation',
|
||||||
"DEFAULT_FILTER_BACKENDS": [
|
"DEFAULT_FILTER_BACKENDS": [
|
||||||
"django_filters.rest_framework.DjangoFilterBackend"
|
"django_filters.rest_framework.DjangoFilterBackend"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -210,10 +210,7 @@
|
|||||||
{% for scrobble in now_playing_list %}
|
{% for scrobble in now_playing_list %}
|
||||||
<div>
|
<div>
|
||||||
{{scrobble.media_obj.title}}<br/>
|
{{scrobble.media_obj.title}}<br/>
|
||||||
{% if scrobble.track %}<em>{{scrobble.track.artist}}</em><br/>{% endif %}
|
{% if scrobble.media_obj.subtitle %}<em>{{scrobble.media_obj.subtitle}}</em><br/>{% endif %}
|
||||||
{% if scrobble.podcast_episode%}<em>{{scrobble.podcast_episode.podcast}}</em><br/>{% endif %}
|
|
||||||
{% if scrobble.video.tv_series %}<em>{{scrobble.video.tv_series }}</em><br/>{% endif %}
|
|
||||||
{% if scrobble.sport_event %}<em>{{scrobble.sport_event.round.season.league}}</em><br/>{% endif %}
|
|
||||||
<small>{{scrobble.timestamp|naturaltime}}<br/>
|
<small>{{scrobble.timestamp|naturaltime}}<br/>
|
||||||
from {{scrobble.source}}</small>
|
from {{scrobble.source}}</small>
|
||||||
<div class="progress-bar" style="margin-right:5px;">
|
<div class="progress-bar" style="margin-right:5px;">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||||
<div
|
<div
|
||||||
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2">Dashboard</h1>
|
<h1 class="h2">Dashboard</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user