Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 927d0be1b8 | |||
| f6b9245b8b | |||
| 39e035b460 | |||
| cf9da39967 |
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "0.10.0"
|
version = "0.10.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
14
vrobbler/apps/books/api/serializers.py
Normal file
14
vrobbler/apps/books/api/serializers.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from books.models import Author, Book
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Author
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class BookSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Book
|
||||||
|
fields = "__all__"
|
||||||
19
vrobbler/apps/books/api/views.py
Normal file
19
vrobbler/apps/books/api/views.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from rest_framework import permissions, viewsets
|
||||||
|
|
||||||
|
from books.api.serializers import (
|
||||||
|
AuthorSerializer,
|
||||||
|
BookSerializer,
|
||||||
|
)
|
||||||
|
from books.models import Author, Book
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Author.objects.all().order_by('-created')
|
||||||
|
serializer_class = AuthorSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class BookViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Book.objects.all().order_by('-created')
|
||||||
|
serializer_class = BookSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
@ -8,7 +8,8 @@ from django.urls import reverse
|
|||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import ScrobblableMixin
|
||||||
|
|
||||||
from vrobbler.apps.books.utils import lookup_book_from_openlibrary
|
from books.utils import lookup_book_from_openlibrary
|
||||||
|
from scrobbles.utils import get_scrobbles_for_media
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -66,3 +67,7 @@ class Book(ScrobblableMixin):
|
|||||||
logger.warn(f"{self} has no pages, no completion percentage")
|
logger.warn(f"{self} has no pages, no completion percentage")
|
||||||
return 0
|
return 0
|
||||||
return int(self.pages * (self.COMPLETION_PERCENT / 100))
|
return int(self.pages * (self.COMPLETION_PERCENT / 100))
|
||||||
|
|
||||||
|
def progress_for_user(self, user: User) -> int:
|
||||||
|
last_scrobble = get_scrobbles_for_media(self, user).last()
|
||||||
|
return int((last_scrobble.book_pages_read / self.pages) * 100)
|
||||||
|
|||||||
0
vrobbler/apps/music/api/__init__.py
Normal file
0
vrobbler/apps/music/api/__init__.py
Normal file
20
vrobbler/apps/music/api/serializers.py
Normal file
20
vrobbler/apps/music/api/serializers.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from music.models import Album, Artist, Track
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Artist
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Album
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class TrackSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Track
|
||||||
|
fields = "__all__"
|
||||||
26
vrobbler/apps/music/api/views.py
Normal file
26
vrobbler/apps/music/api/views.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from rest_framework import permissions, viewsets
|
||||||
|
|
||||||
|
from music.api.serializers import (
|
||||||
|
TrackSerializer,
|
||||||
|
ArtistSerializer,
|
||||||
|
AlbumSerializer,
|
||||||
|
)
|
||||||
|
from music.models import Artist, Album, Track
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Artist.objects.all().order_by('-created')
|
||||||
|
serializer_class = ArtistSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Album.objects.all().order_by('-created')
|
||||||
|
serializer_class = AlbumSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class TrackViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Track.objects.all().order_by('-created')
|
||||||
|
serializer_class = TrackSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
1
vrobbler/apps/music/serializers.py
Normal file
1
vrobbler/apps/music/serializers.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
@ -4,6 +4,7 @@ import re
|
|||||||
from scrobbles.musicbrainz import (
|
from scrobbles.musicbrainz import (
|
||||||
lookup_album_dict_from_mb,
|
lookup_album_dict_from_mb,
|
||||||
lookup_artist_from_mb,
|
lookup_artist_from_mb,
|
||||||
|
lookup_track_from_mb,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -93,7 +94,11 @@ def get_or_create_track(
|
|||||||
title=title, artist=artist, album=album
|
title=title, artist=artist, album=album
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# TODO Can we look up mbid for tracks?
|
if not mbid:
|
||||||
|
mbid = lookup_track_from_mb(
|
||||||
|
title, artist.musicbrainz_id, album.musicbrainz_id
|
||||||
|
)['id']
|
||||||
|
|
||||||
if not track:
|
if not track:
|
||||||
track = Track.objects.create(
|
track = Track.objects.create(
|
||||||
title=title,
|
title=title,
|
||||||
|
|||||||
1
vrobbler/apps/profiles/api/__init__.py
Normal file
1
vrobbler/apps/profiles/api/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
18
vrobbler/apps/profiles/api/serializers.py
Normal file
18
vrobbler/apps/profiles/api/serializers.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
from profiles.models import UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
exclude = ('password',)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UserProfile
|
||||||
|
exclude = ('lastfm_password',)
|
||||||
28
vrobbler/apps/profiles/api/views.py
Normal file
28
vrobbler/apps/profiles/api/views.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from rest_framework import permissions, viewsets
|
||||||
|
|
||||||
|
from profiles.api.serializers import UserSerializer, UserProfileSerializer
|
||||||
|
from profiles.models import UserProfile
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint that allows users to be viewed or edited.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = User.objects.all().order_by('-date_joined')
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint that allows users to be viewed or edited.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = UserProfile.objects.all().order_by('-created')
|
||||||
|
serializer_class = UserProfileSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
0
vrobbler/apps/scrobbles/api/__init__.py
Normal file
0
vrobbler/apps/scrobbles/api/__init__.py
Normal file
33
vrobbler/apps/scrobbles/api/serializers.py
Normal file
33
vrobbler/apps/scrobbles/api/serializers.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from scrobbles.models import (
|
||||||
|
AudioScrobblerTSVImport,
|
||||||
|
KoReaderImport,
|
||||||
|
LastFmImport,
|
||||||
|
Scrobble,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobbleSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Scrobble
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class KoReaderImportSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = KoReaderImport
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class AudioScrobblerTSVImportSerializer(
|
||||||
|
serializers.HyperlinkedModelSerializer
|
||||||
|
):
|
||||||
|
class Meta:
|
||||||
|
model = AudioScrobblerTSVImport
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class LastFmImportSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = LastFmImport
|
||||||
|
fields = "__all__"
|
||||||
49
vrobbler/apps/scrobbles/api/views.py
Normal file
49
vrobbler/apps/scrobbles/api/views.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from rest_framework import permissions, viewsets
|
||||||
|
from scrobbles.api.serializers import (
|
||||||
|
AudioScrobblerTSVImportSerializer,
|
||||||
|
KoReaderImportSerializer,
|
||||||
|
LastFmImportSerializer,
|
||||||
|
ScrobbleSerializer,
|
||||||
|
)
|
||||||
|
from scrobbles.models import (
|
||||||
|
AudioScrobblerTSVImport,
|
||||||
|
KoReaderImport,
|
||||||
|
Scrobble,
|
||||||
|
LastFmImport,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobbleViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Scrobble.objects.all().order_by('-timestamp')
|
||||||
|
serializer_class = ScrobbleSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class KoReaderImportViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = KoReaderImport.objects.all().order_by('-created')
|
||||||
|
serializer_class = KoReaderImportSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class AudioScrobblerTSVImportViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = AudioScrobblerTSVImport.objects.all().order_by('-created')
|
||||||
|
serializer_class = AudioScrobblerTSVImportSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class LastFmImportViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = LastFmImport.objects.all().order_by('-created')
|
||||||
|
serializer_class = LastFmImportSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
@ -90,16 +90,18 @@ def lookup_artist_from_mb(artist_name: str) -> str:
|
|||||||
return top_result
|
return top_result
|
||||||
|
|
||||||
|
|
||||||
def lookup_track_from_mb(artist_name: str) -> str:
|
def lookup_track_from_mb(
|
||||||
|
track_name: str, artist_mbid: str, album_mbid: str
|
||||||
|
) -> str:
|
||||||
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
|
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
|
||||||
|
|
||||||
top_result = musicbrainzngs.search_recordings(artist=artist_name)[
|
top_result = musicbrainzngs.search_recordings(
|
||||||
'artist-list'
|
query=track_name, artist=artist_mbid, release=album_mbid
|
||||||
][0]
|
)['recording-list'][0]
|
||||||
score = int(top_result.get('ext:score'))
|
score = int(top_result.get('ext:score'))
|
||||||
if score < 85:
|
if score < 85:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Artist lookup score below 85 threshold",
|
"Track lookup score below 85 threshold",
|
||||||
extra={"result": top_result},
|
extra={"result": top_result},
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
from scrobbles.models import Scrobble, AudioScrobblerTSVImport
|
|
||||||
|
|
||||||
|
|
||||||
class AudioScrobblerTSVImportSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = AudioScrobblerTSVImport
|
|
||||||
fields = ('tsv_file',)
|
|
||||||
|
|
||||||
|
|
||||||
class ScrobbleSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Scrobble
|
|
||||||
fields = "__all__"
|
|
||||||
@ -4,7 +4,21 @@ from scrobbles import views
|
|||||||
app_name = 'scrobbles'
|
app_name = 'scrobbles'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.scrobble_endpoint, name='api-list'),
|
path(
|
||||||
|
'manual/imdb/',
|
||||||
|
views.ManualScrobbleView.as_view(),
|
||||||
|
name='imdb-manual-scrobble',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'manual/audioscrobbler/',
|
||||||
|
views.AudioScrobblerImportCreateView.as_view(),
|
||||||
|
name='audioscrobbler-file-upload',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'manual/koreader/',
|
||||||
|
views.KoReaderImportCreateView.as_view(),
|
||||||
|
name='koreader-file-upload',
|
||||||
|
),
|
||||||
path('finish/<slug:uuid>', views.scrobble_finish, name='finish'),
|
path('finish/<slug:uuid>', views.scrobble_finish, name='finish'),
|
||||||
path('cancel/<slug:uuid>', views.scrobble_cancel, name='cancel'),
|
path('cancel/<slug:uuid>', views.scrobble_cancel, name='cancel'),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from dateutil.parser import ParserError, parse
|
from dateutil.parser import ParserError, parse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
def convert_to_seconds(run_time: str) -> int:
|
def convert_to_seconds(run_time: str) -> int:
|
||||||
@ -103,3 +107,11 @@ def check_scrobble_for_finish(
|
|||||||
if getattr(settings, "KEEP_DETAILED_SCROBBLE_LOGS", False):
|
if getattr(settings, "KEEP_DETAILED_SCROBBLE_LOGS", False):
|
||||||
scrobble.scrobble_log += f"\n{str(scrobble.timestamp)} - {scrobble.playback_position} - {str(scrobble.playback_position_ticks)} - {str(scrobble.percent_played)}%"
|
scrobble.scrobble_log += f"\n{str(scrobble.timestamp)} - {scrobble.playback_position} - {str(scrobble.playback_position_ticks)} - {str(scrobble.percent_played)}%"
|
||||||
scrobble.save(update_fields=['scrobble_log'])
|
scrobble.save(update_fields=['scrobble_log'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet:
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
|
||||||
|
if media_obj.__class__.__name__ == 'Book':
|
||||||
|
media_query = models.Q(book=media_obj)
|
||||||
|
return Scrobble.objects.filter(media_query, user=user)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models.fields import timezone
|
from django.db.models.fields import timezone
|
||||||
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
||||||
@ -28,6 +29,7 @@ from rest_framework.decorators import (
|
|||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
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.api import serializers
|
||||||
from scrobbles.constants import (
|
from scrobbles.constants import (
|
||||||
JELLYFIN_AUDIO_ITEM_TYPES,
|
JELLYFIN_AUDIO_ITEM_TYPES,
|
||||||
JELLYFIN_VIDEO_ITEM_TYPES,
|
JELLYFIN_VIDEO_ITEM_TYPES,
|
||||||
@ -49,10 +51,6 @@ from scrobbles.scrobblers import (
|
|||||||
mopidy_scrobble_podcast,
|
mopidy_scrobble_podcast,
|
||||||
mopidy_scrobble_track,
|
mopidy_scrobble_track,
|
||||||
)
|
)
|
||||||
from scrobbles.serializers import (
|
|
||||||
AudioScrobblerTSVImportSerializer,
|
|
||||||
ScrobbleSerializer,
|
|
||||||
)
|
|
||||||
from scrobbles.tasks import (
|
from scrobbles.tasks import (
|
||||||
process_koreader_import,
|
process_koreader_import,
|
||||||
process_lastfm_import,
|
process_lastfm_import,
|
||||||
@ -215,15 +213,6 @@ def lastfm_import(request):
|
|||||||
return HttpResponseRedirect(success_url)
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@api_view(['GET'])
|
|
||||||
def scrobble_endpoint(request):
|
|
||||||
"""List all Scrobbles, or create a new Scrobble"""
|
|
||||||
scrobble = Scrobble.objects.all()
|
|
||||||
serializer = ScrobbleSerializer(scrobble, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@ -293,7 +282,9 @@ def import_audioscrobbler_file(request):
|
|||||||
scrobbles_created = []
|
scrobbles_created = []
|
||||||
# tsv_file = request.FILES[0]
|
# tsv_file = request.FILES[0]
|
||||||
|
|
||||||
file_serializer = AudioScrobblerTSVImportSerializer(data=request.data)
|
file_serializer = serializers.AudioScrobblerTSVImportSerializer(
|
||||||
|
data=request.data
|
||||||
|
)
|
||||||
if file_serializer.is_valid():
|
if file_serializer.is_valid():
|
||||||
import_file = file_serializer.save()
|
import_file = file_serializer.save()
|
||||||
return Response(
|
return Response(
|
||||||
@ -305,39 +296,48 @@ def import_audioscrobbler_file(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
def scrobble_finish(request, uuid):
|
def scrobble_finish(request, uuid):
|
||||||
user = request.user
|
user = request.user
|
||||||
|
success_url = reverse_lazy('vrobbler-home')
|
||||||
|
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return Response({}, status=status.HTTP_403_FORBIDDEN)
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
|
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
|
||||||
if not scrobble:
|
if scrobble:
|
||||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
scrobble.stop(force_finish=True)
|
||||||
scrobble.stop(force_finish=True)
|
messages.add_message(
|
||||||
return Response(
|
request,
|
||||||
{'id': scrobble.id, 'status': scrobble.status},
|
messages.SUCCESS,
|
||||||
status=status.HTTP_200_OK,
|
f"Scrobble of {scrobble.media_obj} finished.",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
messages.add_message(request, messages.ERROR, "Scrobble not found.")
|
||||||
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
def scrobble_cancel(request, uuid):
|
def scrobble_cancel(request, uuid):
|
||||||
user = request.user
|
user = request.user
|
||||||
|
success_url = reverse_lazy('vrobbler-home')
|
||||||
|
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return Response({}, status=status.HTTP_403_FORBIDDEN)
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
|
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()
|
||||||
if not scrobble:
|
if scrobble:
|
||||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
scrobble.cancel()
|
||||||
scrobble.cancel()
|
messages.add_message(
|
||||||
return Response(
|
request,
|
||||||
{'id': scrobble.id, 'status': 'cancelled'}, status=status.HTTP_200_OK
|
messages.SUCCESS,
|
||||||
)
|
f"Scrobble of {scrobble.media_obj} cancelled.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.add_message(request, messages.ERROR, "Scrobble not found.")
|
||||||
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
|
|||||||
0
vrobbler/apps/videos/api/__init__.py
Normal file
0
vrobbler/apps/videos/api/__init__.py
Normal file
14
vrobbler/apps/videos/api/serializers.py
Normal file
14
vrobbler/apps/videos/api/serializers.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from videos.models import Series, Video
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Series
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class VideoSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Video
|
||||||
|
fields = "__all__"
|
||||||
19
vrobbler/apps/videos/api/views.py
Normal file
19
vrobbler/apps/videos/api/views.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from rest_framework import permissions, viewsets
|
||||||
|
|
||||||
|
from videos.api.serializers import (
|
||||||
|
SeriesSerializer,
|
||||||
|
VideoSerializer,
|
||||||
|
)
|
||||||
|
from videos.models import Series, Video
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Series.objects.all().order_by('-created')
|
||||||
|
serializer_class = SeriesSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class VideoViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Video.objects.all().order_by('-created')
|
||||||
|
serializer_class = VideoSerializer
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
@ -140,6 +140,8 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
|
||||||
|
|
||||||
WSGI_APPLICATION = "vrobbler.wsgi.application"
|
WSGI_APPLICATION = "vrobbler.wsgi.application"
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
@ -174,19 +176,14 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
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.BasicAuthentication',
|
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
],
|
],
|
||||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
|
||||||
"DEFAULT_FILTER_BACKENDS": [
|
"DEFAULT_FILTER_BACKENDS": [
|
||||||
"django_filters.rest_framework.DjangoFilterBackend"
|
"django_filters.rest_framework.DjangoFilterBackend"
|
||||||
],
|
],
|
||||||
'DEFAULT_PARSER_CLASSES': [
|
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||||
'rest_framework.parsers.JSONParser',
|
"PAGE_SIZE": 200,
|
||||||
],
|
|
||||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'vrobbler.negotiation.IgnoreClientContentNegotiation',
|
|
||||||
"PAGE_SIZE": 100,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|||||||
@ -177,7 +177,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<form id="scrobble-form" action="{% url 'imdb-manual-scrobble' %}" method="post">
|
<form id="scrobble-form" action="{% url 'scrobbles:imdb-manual-scrobble' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ imdb_form }}
|
{{ imdb_form }}
|
||||||
</form>
|
</form>
|
||||||
@ -197,6 +197,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
||||||
<div class="position-sticky pt-3">
|
<div class="position-sticky pt-3">
|
||||||
|
{% if messages %}
|
||||||
|
<ul style="padding-right:10px;">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li {% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
{% if now_playing_list and user.is_authenticated %}
|
{% if now_playing_list and user.is_authenticated %}
|
||||||
<ul style="padding-right:10px;">
|
<ul style="padding-right:10px;">
|
||||||
<b>Now playing</b>
|
<b>Now playing</b>
|
||||||
@ -219,8 +226,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="/">
|
<a class="nav-link active" aria-current="page" href="/">
|
||||||
@ -271,6 +276,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script>
|
||||||
<script><!-- comment ------------------------------------------------->
|
<script><!-- comment ------------------------------------------------->
|
||||||
/* globals Chart:false, feather:false */
|
/* globals Chart:false, feather:false */
|
||||||
|
|||||||
@ -316,7 +316,7 @@
|
|||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<form action="{% url 'audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
|
<form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -328,7 +328,7 @@
|
|||||||
<button type="submit" class="btn btn-primary">Import</button>
|
<button type="submit" class="btn btn-primary">Import</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form action="{% url 'koreader-file-upload' %}" method="post" enctype="multipart/form-data">
|
<form action="{% url 'scrobbles:koreader-file-upload' %}" method="post" enctype="multipart/form-data">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div
|
||||||
|
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">Manual scrobble</h1>
|
<h1 class="h2">Manual scrobble</h1>
|
||||||
<form action="{% url 'audioscrobbler-file-upload' %}" method="post">
|
<form action="{% url 'scrobbles:audioscrobbler-file-upload' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
<input type="submit" value="Submit">
|
<input type="submit" value="Submit">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -3,34 +3,49 @@ from django.conf import settings
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from scrobbles import urls as scrobble_urls
|
from rest_framework import routers
|
||||||
from music import urls as music_urls
|
|
||||||
from videos import urls as video_urls
|
from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
|
||||||
|
from vrobbler.apps.music import urls as music_urls
|
||||||
|
from vrobbler.apps.music.api.views import (
|
||||||
|
AlbumViewSet,
|
||||||
|
ArtistViewSet,
|
||||||
|
TrackViewSet,
|
||||||
|
)
|
||||||
|
from vrobbler.apps.profiles.api.views import UserProfileViewSet, UserViewSet
|
||||||
|
from vrobbler.apps.scrobbles import urls as scrobble_urls
|
||||||
|
from vrobbler.apps.scrobbles.api.views import (
|
||||||
|
AudioScrobblerTSVImportViewSet,
|
||||||
|
KoReaderImportViewSet,
|
||||||
|
LastFmImportViewSet,
|
||||||
|
ScrobbleViewSet,
|
||||||
|
)
|
||||||
|
from vrobbler.apps.videos import urls as video_urls
|
||||||
|
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r'scrobbles', ScrobbleViewSet)
|
||||||
|
router.register(r'lastfm-imports', LastFmImportViewSet)
|
||||||
|
router.register(r'tsv-imports', AudioScrobblerTSVImportViewSet)
|
||||||
|
router.register(r'koreader-imports', KoReaderImportViewSet)
|
||||||
|
router.register(r'artist', ArtistViewSet)
|
||||||
|
router.register(r'album', AlbumViewSet)
|
||||||
|
router.register(r'tracks', TrackViewSet)
|
||||||
|
router.register(r'series', SeriesViewSet)
|
||||||
|
router.register(r'videos', VideoViewSet)
|
||||||
|
router.register(r'authors', AuthorViewSet)
|
||||||
|
router.register(r'books', BookViewSet)
|
||||||
|
router.register(r'users', UserViewSet)
|
||||||
|
router.register(r'user_profiles', UserProfileViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('api/v1/', include(router.urls)),
|
||||||
|
path('api/v1/auth', include("rest_framework.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
# path("api-auth/", include("rest_framework.urls")),
|
|
||||||
# path("movies/", include(movies, namespace="movies")),
|
|
||||||
# path("shows/", include(shows, namespace="shows")),
|
|
||||||
path("api/v1/scrobbles/", include(scrobble_urls, namespace="scrobbles")),
|
|
||||||
path(
|
|
||||||
'manual/imdb/',
|
|
||||||
scrobbles_views.ManualScrobbleView.as_view(),
|
|
||||||
name='imdb-manual-scrobble',
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
'manual/audioscrobbler/',
|
|
||||||
scrobbles_views.AudioScrobblerImportCreateView.as_view(),
|
|
||||||
name='audioscrobbler-file-upload',
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
'manual/koreader/',
|
|
||||||
scrobbles_views.KoReaderImportCreateView.as_view(),
|
|
||||||
name='koreader-file-upload',
|
|
||||||
),
|
|
||||||
path("", include(music_urls, namespace="music")),
|
path("", include(music_urls, namespace="music")),
|
||||||
path("", include(video_urls, namespace="videos")),
|
path("", include(video_urls, namespace="videos")),
|
||||||
|
path("", include(scrobble_urls, namespace="scrobbles")),
|
||||||
path(
|
path(
|
||||||
"", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"
|
"", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user