浏览代码

Blacken quotes

Colin Powell 2 年之前
父节点
当前提交
94f1396f2e
共有 60 个文件被更改,包括 595 次插入596 次删除
  1. 2 2
      manage.py
  2. 0 1
      pyproject.toml
  3. 2 2
      tests/scrobbles_tests/conftest.py
  4. 15 15
      tests/scrobbles_tests/test_aggregators.py
  5. 19 19
      tests/scrobbles_tests/test_views.py
  6. 1 1
      tests/videos_tests/test_imdb.py
  7. 1 1
      vrobbler/__init__.py
  8. 2 2
      vrobbler/apps/books/api/views.py
  9. 2 2
      vrobbler/apps/books/koreader.py
  10. 2 2
      vrobbler/apps/books/models.py
  11. 3 3
      vrobbler/apps/books/utils.py
  12. 1 1
      vrobbler/apps/music/admin.py
  13. 15 15
      vrobbler/apps/music/aggregators.py
  14. 3 3
      vrobbler/apps/music/api/views.py
  15. 1 1
      vrobbler/apps/music/apps.py
  16. 14 14
      vrobbler/apps/music/constants.py
  17. 7 7
      vrobbler/apps/music/lastfm.py
  18. 45 45
      vrobbler/apps/music/models.py
  19. 31 31
      vrobbler/apps/music/musicbrainz.py
  20. 31 31
      vrobbler/apps/music/theaudiodb.py
  21. 10 10
      vrobbler/apps/music/urls.py
  22. 9 9
      vrobbler/apps/music/utils.py
  23. 10 10
      vrobbler/apps/music/views.py
  24. 1 1
      vrobbler/apps/podcasts/apps.py
  25. 4 4
      vrobbler/apps/podcasts/models.py
  26. 2 2
      vrobbler/apps/profiles/api/serializers.py
  27. 2 2
      vrobbler/apps/profiles/api/views.py
  28. 7 7
      vrobbler/apps/scrobbles/admin.py
  29. 4 4
      vrobbler/apps/scrobbles/api/views.py
  30. 1 1
      vrobbler/apps/scrobbles/apps.py
  31. 1 1
      vrobbler/apps/scrobbles/context_processors.py
  32. 9 9
      vrobbler/apps/scrobbles/export.py
  33. 6 6
      vrobbler/apps/scrobbles/forms.py
  34. 42 42
      vrobbler/apps/scrobbles/models.py
  35. 3 3
      vrobbler/apps/scrobbles/scrobblers.py
  36. 25 25
      vrobbler/apps/scrobbles/stats.py
  37. 1 1
      vrobbler/apps/scrobbles/templatetags/urlreplace.py
  38. 4 4
      vrobbler/apps/scrobbles/tsv.py
  39. 28 28
      vrobbler/apps/scrobbles/urls.py
  40. 11 11
      vrobbler/apps/scrobbles/utils.py
  41. 83 83
      vrobbler/apps/scrobbles/views.py
  42. 2 2
      vrobbler/apps/sports/admin.py
  43. 7 7
      vrobbler/apps/sports/api/views.py
  44. 23 23
      vrobbler/apps/sports/models.py
  45. 16 16
      vrobbler/apps/sports/thesportsdb.py
  46. 5 5
      vrobbler/apps/sports/urls.py
  47. 2 2
      vrobbler/apps/sports/utils.py
  48. 1 1
      vrobbler/apps/sports/views.py
  49. 1 1
      vrobbler/apps/videos/admin.py
  50. 2 2
      vrobbler/apps/videos/api/views.py
  51. 1 1
      vrobbler/apps/videos/apps.py
  52. 11 11
      vrobbler/apps/videos/imdb.py
  53. 8 8
      vrobbler/apps/videos/models.py
  54. 7 7
      vrobbler/apps/videos/urls.py
  55. 2 2
      vrobbler/apps/videos/views.py
  56. 1 1
      vrobbler/asgi.py
  57. 4 4
      vrobbler/cli.py
  58. 20 20
      vrobbler/settings.py
  59. 21 21
      vrobbler/urls.py
  60. 1 1
      vrobbler/wsgi.py

+ 2 - 2
manage.py

@@ -6,7 +6,7 @@ import sys
 
 def main():
     """Run administrative tasks."""
-    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrobbler.settings')
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vrobbler.settings")
     try:
         from django.core.management import execute_from_command_line
     except ImportError as exc:
@@ -18,5 +18,5 @@ def main():
     execute_from_command_line(sys.argv)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()

+ 0 - 1
pyproject.toml

@@ -63,7 +63,6 @@ DJANGO_SETTINGS_MODULE='vrobbler.settings'
 
 [tool.black]
 line-length = 79
-skip-string-normalization = true
 target-version = ["py39", "py310"]
 include = ".py$"
 exclude = "migrations"

+ 2 - 2
tests/scrobbles_tests/conftest.py

@@ -24,7 +24,7 @@ class MopidyRequest:
 
     def __init__(self, **kwargs):
         self.request_data = {
-            "name": kwargs.get('name', self.name),
+            "name": kwargs.get("name", self.name),
             "artist": kwargs.get("artist", self.artist),
             "album": kwargs.get("album", self.album),
             "track_number": int(kwargs.get("track_number", self.track_number)),
@@ -61,7 +61,7 @@ class MopidyRequest:
 
 @pytest.fixture
 def valid_auth_token():
-    user = User.objects.create(email='test@exmaple.com')
+    user = User.objects.create(email="test@exmaple.com")
     return Token.objects.create(user=user).key
 
 

+ 15 - 15
tests/scrobbles_tests/test_aggregators.py

@@ -11,11 +11,11 @@ from scrobbles.models import Scrobble
 
 
 def build_scrobbles(client, request_data, num=7, spacing=2):
-    url = reverse('scrobbles:mopidy-webhook')
-    user = get_user_model().objects.create(username='Test User')
-    UserProfile.objects.create(user=user, timezone='US/Eastern')
+    url = reverse("scrobbles:mopidy-webhook")
+    user = get_user_model().objects.create(username="Test User")
+    UserProfile.objects.create(user=user, timezone="US/Eastern")
     for i in range(num):
-        client.post(url, request_data, content_type='application/json')
+        client.post(url, request_data, content_type="application/json")
         s = Scrobble.objects.last()
         s.user = user
         s.timestamp = timezone.now() - timedelta(days=i * spacing)
@@ -30,11 +30,11 @@ def test_scrobble_counts_data(client, mopidy_track_request_data):
     user = get_user_model().objects.first()
     count_dict = scrobble_counts(user)
     assert count_dict == {
-        'alltime': 7,
-        'month': 2,
-        'today': 1,
-        'week': 3,
-        'year': 7,
+        "alltime": 7,
+        "month": 2,
+        "today": 1,
+        "week": 3,
+        "year": 7,
     }
 
 
@@ -58,7 +58,7 @@ def test_top_tracks_by_day(client, mopidy_track_request_data):
 def test_top_tracks_by_week(client, mopidy_track_request_data):
     build_scrobbles(client, mopidy_track_request_data, 7, 1)
     user = get_user_model().objects.first()
-    tops = live_charts(user, chart_period='week')
+    tops = live_charts(user, chart_period="week")
     assert tops[0].title == "Same in the End"
 
 
@@ -66,7 +66,7 @@ def test_top_tracks_by_week(client, mopidy_track_request_data):
 def test_top_tracks_by_month(client, mopidy_track_request_data):
     build_scrobbles(client, mopidy_track_request_data, 7, 1)
     user = get_user_model().objects.first()
-    tops = live_charts(user, chart_period='month')
+    tops = live_charts(user, chart_period="month")
     assert tops[0].title == "Same in the End"
 
 
@@ -74,7 +74,7 @@ def test_top_tracks_by_month(client, mopidy_track_request_data):
 def test_top_tracks_by_year(client, mopidy_track_request_data):
     build_scrobbles(client, mopidy_track_request_data, 7, 1)
     user = get_user_model().objects.first()
-    tops = live_charts(user, chart_period='year')
+    tops = live_charts(user, chart_period="year")
     assert tops[0].title == "Same in the End"
 
 
@@ -82,7 +82,7 @@ def test_top_tracks_by_year(client, mopidy_track_request_data):
 def test_top__artists_by_week(client, mopidy_track_request_data):
     build_scrobbles(client, mopidy_track_request_data, 7, 1)
     user = get_user_model().objects.first()
-    tops = live_charts(user, chart_period='week', media_type="Artist")
+    tops = live_charts(user, chart_period="week", media_type="Artist")
     assert tops[0].name == "Sublime"
 
 
@@ -90,7 +90,7 @@ def test_top__artists_by_week(client, mopidy_track_request_data):
 def test_top__artists_by_month(client, mopidy_track_request_data):
     build_scrobbles(client, mopidy_track_request_data, 7, 1)
     user = get_user_model().objects.first()
-    tops = live_charts(user, chart_period='month', media_type="Artist")
+    tops = live_charts(user, chart_period="month", media_type="Artist")
     assert tops[0].name == "Sublime"
 
 
@@ -98,5 +98,5 @@ def test_top__artists_by_month(client, mopidy_track_request_data):
 def test_top__artists_by_year(client, mopidy_track_request_data):
     build_scrobbles(client, mopidy_track_request_data, 7, 1)
     user = get_user_model().objects.first()
-    tops = live_charts(user, chart_period='year', media_type="Artist")
+    tops = live_charts(user, chart_period="year", media_type="Artist")
     assert tops[0].name == "Sublime"

+ 19 - 19
tests/scrobbles_tests/test_views.py

@@ -9,21 +9,21 @@ from scrobbles.models import Scrobble
 
 @pytest.mark.django_db
 def test_get_not_allowed_from_mopidy(client, valid_auth_token):
-    url = reverse('scrobbles:mopidy-webhook')
-    headers = {'Authorization': f'Token {valid_auth_token}'}
+    url = reverse("scrobbles:mopidy-webhook")
+    headers = {"Authorization": f"Token {valid_auth_token}"}
     response = client.get(url, headers=headers)
     assert response.status_code == 405
 
 
 @pytest.mark.django_db
 def test_bad_mopidy_request_data(client, valid_auth_token):
-    url = reverse('scrobbles:mopidy-webhook')
-    headers = {'Authorization': f'Token {valid_auth_token}'}
+    url = reverse("scrobbles:mopidy-webhook")
+    headers = {"Authorization": f"Token {valid_auth_token}"}
     response = client.post(url, headers)
     assert response.status_code == 400
     assert (
-        response.data['detail']
-        == 'JSON parse error - Expecting value: line 1 column 1 (char 0)'
+        response.data["detail"]
+        == "JSON parse error - Expecting value: line 1 column 1 (char 0)"
     )
 
 
@@ -31,16 +31,16 @@ def test_bad_mopidy_request_data(client, valid_auth_token):
 def test_scrobble_mopidy_track(
     client, mopidy_track_request_data, valid_auth_token
 ):
-    url = reverse('scrobbles:mopidy-webhook')
-    headers = {'Authorization': f'Token {valid_auth_token}'}
+    url = reverse("scrobbles:mopidy-webhook")
+    headers = {"Authorization": f"Token {valid_auth_token}"}
     response = client.post(
         url,
         mopidy_track_request_data,
-        content_type='application/json',
+        content_type="application/json",
         headers=headers,
     )
     assert response.status_code == 200
-    assert response.data == {'scrobble_id': 1}
+    assert response.data == {"scrobble_id": 1}
 
     scrobble = Scrobble.objects.get(id=1)
     assert scrobble.media_obj.__class__ == Track
@@ -54,23 +54,23 @@ def test_scrobble_mopidy_same_track_different_album(
     mopidy_track_diff_album_request_data,
     valid_auth_token,
 ):
-    url = reverse('scrobbles:mopidy-webhook')
-    headers = {'Authorization': f'Token {valid_auth_token}'}
+    url = reverse("scrobbles:mopidy-webhook")
+    headers = {"Authorization": f"Token {valid_auth_token}"}
     response = client.post(
         url,
         mopidy_track_request_data,
-        content_type='application/json',
+        content_type="application/json",
         headers=headers,
     )
     assert response.status_code == 200
-    assert response.data == {'scrobble_id': 1}
+    assert response.data == {"scrobble_id": 1}
     scrobble = Scrobble.objects.get(id=1)
     assert scrobble.media_obj.album.name == "Sublime"
 
     response = client.post(
         url,
         mopidy_track_diff_album_request_data,
-        content_type='application/json',
+        content_type="application/json",
     )
 
     scrobble = Scrobble.objects.get(id=2)
@@ -83,16 +83,16 @@ def test_scrobble_mopidy_same_track_different_album(
 def test_scrobble_mopidy_podcast(
     client, mopidy_podcast_request_data, valid_auth_token
 ):
-    url = reverse('scrobbles:mopidy-webhook')
-    headers = {'Authorization': f'Token {valid_auth_token}'}
+    url = reverse("scrobbles:mopidy-webhook")
+    headers = {"Authorization": f"Token {valid_auth_token}"}
     response = client.post(
         url,
         mopidy_podcast_request_data,
-        content_type='application/json',
+        content_type="application/json",
         headers=headers,
     )
     assert response.status_code == 200
-    assert response.data == {'scrobble_id': 1}
+    assert response.data == {"scrobble_id": 1}
 
     scrobble = Scrobble.objects.get(id=1)
     assert scrobble.media_obj.__class__ == Episode

+ 1 - 1
tests/videos_tests/test_imdb.py

@@ -5,7 +5,7 @@ from videos.imdb import lookup_video_from_imdb
 
 @pytest.mark.skip(reason="Need to sort out third party API testing")
 def test_lookup_imdb_bad_id(caplog):
-    data = lookup_video_from_imdb('3409324')
+    data = lookup_video_from_imdb("3409324")
     assert data is None
     assert caplog.records[0].levelname == "WARNING"
     assert caplog.records[0].msg == "IMDB ID should begin with 'tt' 3409324"

+ 1 - 1
vrobbler/__init__.py

@@ -2,4 +2,4 @@
 # Django starts so that shared_task will use this app.
 from .celery import app as celery_app
 
-__all__ = ('celery_app',)
+__all__ = ("celery_app",)

+ 2 - 2
vrobbler/apps/books/api/views.py

@@ -8,12 +8,12 @@ from books.models import Author, Book
 
 
 class AuthorViewSet(viewsets.ModelViewSet):
-    queryset = Author.objects.all().order_by('-created')
+    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')
+    queryset = Book.objects.all().order_by("-created")
     serializer_class = BookSerializer
     permission_classes = [permissions.IsAuthenticated]

+ 2 - 2
vrobbler/apps/books/koreader.py

@@ -46,7 +46,7 @@ def process_koreader_sqlite_file(sqlite_file_path, user_id):
     book_table = cur.execute("SELECT * FROM book")
     new_scrobbles = []
     for book_row in book_table:
-        authors = book_row[KoReaderBookColumn.AUTHORS.value].split('\n')
+        authors = book_row[KoReaderBookColumn.AUTHORS.value].split("\n")
         author_list = []
         for author_str in authors:
             logger.debug(f"Looking up author {author_str}")
@@ -119,6 +119,6 @@ def process_koreader_sqlite_file(sqlite_file_path, user_id):
     created = Scrobble.objects.bulk_create(new_scrobbles)
     logger.info(
         f"Created {len(created)} scrobbles",
-        extra={'created_scrobbles': created},
+        extra={"created_scrobbles": created},
     )
     return created

+ 2 - 2
vrobbler/apps/books/models.py

@@ -28,7 +28,7 @@ class Author(TimeStampedModel):
 
 
 class Book(ScrobblableMixin):
-    COMPLETION_PERCENT = getattr(settings, 'BOOK_COMPLETION_PERCENT', 95)
+    COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
 
     title = models.CharField(max_length=255)
     authors = models.ManyToManyField(Author)
@@ -59,7 +59,7 @@ class Book(ScrobblableMixin):
         return self.authors.first()
 
     def get_absolute_url(self):
-        return reverse("books:book_detail", kwargs={'slug': self.uuid})
+        return reverse("books:book_detail", kwargs={"slug": self.uuid})
 
     @property
     def pages_for_completion(self) -> int:

+ 3 - 3
vrobbler/apps/books/utils.py

@@ -26,12 +26,12 @@ def lookup_book_from_openlibrary(title: str, author: str = None) -> dict:
 
     results = json.loads(response.content)
 
-    if len(results.get('docs')) == 0:
+    if len(results.get("docs")) == 0:
         logger.warn(f"No results found from OL for {title}")
         return {}
 
-    top = results.get('docs')[0]
-    if author and author not in top['author_name']:
+    top = results.get("docs")[0]
+    if author and author not in top["author_name"]:
         logger.warn(
             f"Lookup for {title} found top result with mismatched author"
         )

+ 1 - 1
vrobbler/apps/music/admin.py

@@ -22,7 +22,7 @@ class AlbumAdmin(admin.ModelAdmin):
     )
     ordering = ("name",)
     filter_horizontal = [
-        'artists',
+        "artists",
     ]
 
 

+ 15 - 15
vrobbler/apps/music/aggregators.py

@@ -29,24 +29,24 @@ def scrobble_counts(user=None):
         user_filter, played_to_completion=True
     )
     data = {}
-    data['today'] = finished_scrobbles_qs.filter(
+    data["today"] = finished_scrobbles_qs.filter(
         timestamp__gte=start_of_today
     ).count()
-    data['week'] = finished_scrobbles_qs.filter(
+    data["week"] = finished_scrobbles_qs.filter(
         timestamp__gte=starting_day_of_current_week
     ).count()
-    data['month'] = finished_scrobbles_qs.filter(
+    data["month"] = finished_scrobbles_qs.filter(
         timestamp__gte=starting_day_of_current_month
     ).count()
-    data['year'] = finished_scrobbles_qs.filter(
+    data["year"] = finished_scrobbles_qs.filter(
         timestamp__gte=starting_day_of_current_year
     ).count()
-    data['alltime'] = finished_scrobbles_qs.count()
+    data["alltime"] = finished_scrobbles_qs.count()
     return data
 
 
 def week_of_scrobbles(
-    user=None, start=None, media: str = 'tracks'
+    user=None, start=None, media: str = "tracks"
 ) -> dict[str, int]:
 
     now = timezone.now()
@@ -62,15 +62,15 @@ def week_of_scrobbles(
     base_qs = Scrobble.objects.filter(user_filter, played_to_completion=True)
 
     media_filter = Q(track__isnull=False)
-    if media == 'movies':
+    if media == "movies":
         media_filter = Q(video__video_type=Video.VideoType.MOVIE)
-    if media == 'series':
+    if media == "series":
         media_filter = Q(video__video_type=Video.VideoType.TV_EPISODE)
 
     for day in range(6, -1, -1):
         start_day = start - timedelta(days=day)
         end = datetime.combine(start_day, datetime.max.time(), now.tzinfo)
-        day_of_week = start_day.strftime('%A')
+        day_of_week = start_day.strftime("%A")
 
         scrobble_day_dict[day_of_week] = base_qs.filter(
             media_filter,
@@ -100,14 +100,14 @@ def live_charts(
     start_day_of_month = now.replace(day=1)
     start_day_of_year = now.replace(month=1, day=1)
 
-    media_model = apps.get_model(app_label='music', model_name=media_type)
+    media_model = apps.get_model(app_label="music", model_name=media_type)
 
     period_queries = {
-        'today': {'scrobble__timestamp__gte': start_of_today},
-        'week': {'scrobble__timestamp__gte': start_day_of_week},
-        'month': {'scrobble__timestamp__gte': start_day_of_month},
-        'year': {'scrobble__timestamp__gte': start_day_of_year},
-        'all': {},
+        "today": {"scrobble__timestamp__gte": start_of_today},
+        "week": {"scrobble__timestamp__gte": start_day_of_week},
+        "month": {"scrobble__timestamp__gte": start_day_of_month},
+        "year": {"scrobble__timestamp__gte": start_day_of_year},
+        "all": {},
     }
 
     time_filter = Q()

+ 3 - 3
vrobbler/apps/music/api/views.py

@@ -9,18 +9,18 @@ from music.models import Artist, Album, Track
 
 
 class ArtistViewSet(viewsets.ModelViewSet):
-    queryset = Artist.objects.all().order_by('-created')
+    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')
+    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')
+    queryset = Track.objects.all().order_by("-created")
     serializer_class = TrackSerializer
     permission_classes = [permissions.IsAuthenticated]

+ 1 - 1
vrobbler/apps/music/apps.py

@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class MusicConfig(AppConfig):
-    name = 'music'
+    name = "music"

+ 14 - 14
vrobbler/apps/music/constants.py

@@ -1,16 +1,16 @@
 JELLYFIN_POST_KEYS = {
-    'ITEM_TYPE': 'ItemType',
-    'RUN_TIME_TICKS': 'RunTimeTicks',
-    'RUN_TIME': 'RunTime',
-    'TITLE': 'Name',
-    'TIMESTAMP': 'UtcTimestamp',
-    'YEAR': 'Year',
-    'PLAYBACK_POSITION_TICKS': 'PlaybackPositionTicks',
-    'PLAYBACK_POSITION': 'PlaybackPosition',
-    'ARTIST_MB_ID': 'Provider_musicbrainzartist',
-    'ALBUM_MB_ID': 'Provider_musicbrainzalbum',
-    'RELEASEGROUP_MB_ID': 'Provider_musicbrainzreleasegroup',
-    'TRACK_MB_ID': 'Provider_musicbrainztrack',
-    'ALBUM_NAME': 'Album',
-    'ARTIST_NAME': 'Artist',
+    "ITEM_TYPE": "ItemType",
+    "RUN_TIME_TICKS": "RunTimeTicks",
+    "RUN_TIME": "RunTime",
+    "TITLE": "Name",
+    "TIMESTAMP": "UtcTimestamp",
+    "YEAR": "Year",
+    "PLAYBACK_POSITION_TICKS": "PlaybackPositionTicks",
+    "PLAYBACK_POSITION": "PlaybackPosition",
+    "ARTIST_MB_ID": "Provider_musicbrainzartist",
+    "ALBUM_MB_ID": "Provider_musicbrainzalbum",
+    "RELEASEGROUP_MB_ID": "Provider_musicbrainzreleasegroup",
+    "TRACK_MB_ID": "Provider_musicbrainztrack",
+    "ALBUM_NAME": "Album",
+    "ARTIST_NAME": "Artist",
 }

+ 7 - 7
vrobbler/apps/music/lastfm.py

@@ -50,13 +50,13 @@ class LastFM:
         lastfm_scrobbles = self.get_last_scrobbles(time_from=last_processed)
 
         for lfm_scrobble in lastfm_scrobbles:
-            timestamp = lfm_scrobble.pop('timestamp')
+            timestamp = lfm_scrobble.pop("timestamp")
 
-            artist = get_or_create_artist(lfm_scrobble.pop('artist'))
-            album = get_or_create_album(lfm_scrobble.pop('album'), artist)
+            artist = get_or_create_artist(lfm_scrobble.pop("artist"))
+            album = get_or_create_album(lfm_scrobble.pop("album"), artist)
 
-            lfm_scrobble['artist'] = artist
-            lfm_scrobble['album'] = album
+            lfm_scrobble["artist"] = artist
+            lfm_scrobble["album"] = album
             track = get_or_create_track(**lfm_scrobble)
 
             new_scrobble = Scrobble(
@@ -85,7 +85,7 @@ class LastFM:
         created = Scrobble.objects.bulk_create(new_scrobbles)
         logger.info(
             f"Created {len(created)} scrobbles",
-            extra={'created_scrobbles': created},
+            extra={"created_scrobbles": created},
         )
         return created
 
@@ -100,7 +100,7 @@ class LastFM:
             lfm_params["time_to"] = int(time_to.timestamp())
 
         # if not time_from and not time_to:
-        lfm_params['limit'] = None
+        lfm_params["limit"] = None
 
         found_scrobbles = self.user.get_recent_tracks(**lfm_params)
         # TOOD spin this out into a celery task over certain threshold of found scrobbles?

+ 45 - 45
vrobbler/apps/music/models.py

@@ -28,7 +28,7 @@ class Artist(TimeStampedModel):
     thumbnail = models.ImageField(upload_to="artist/", **BNULL)
 
     class Meta:
-        unique_together = [['name', 'musicbrainz_id']]
+        unique_together = [["name", "musicbrainz_id"]]
 
     def __str__(self):
         return self.name
@@ -38,27 +38,27 @@ class Artist(TimeStampedModel):
         return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
 
     def get_absolute_url(self):
-        return reverse('music:artist_detail', kwargs={'slug': self.uuid})
+        return reverse("music:artist_detail", kwargs={"slug": self.uuid})
 
     def scrobbles(self):
         from scrobbles.models import Scrobble
 
         return Scrobble.objects.filter(
             track__in=self.track_set.all()
-        ).order_by('-timestamp')
+        ).order_by("-timestamp")
 
     @property
     def tracks(self):
         return (
             self.track_set.all()
-            .annotate(scrobble_count=models.Count('scrobble'))
-            .order_by('-scrobble_count')
+            .annotate(scrobble_count=models.Count("scrobble"))
+            .order_by("-scrobble_count")
         )
 
     def charts(self):
         from scrobbles.models import ChartRecord
 
-        return ChartRecord.objects.filter(track__artist=self).order_by('-year')
+        return ChartRecord.objects.filter(track__artist=self).order_by("-year")
 
     def fix_metadata(self):
         tadb_info = lookup_artist_from_tadb(self.name)
@@ -66,19 +66,19 @@ class Artist(TimeStampedModel):
             logger.warn(f"No response from TADB for artist {self.name}")
             return
 
-        self.biography = tadb_info['biography']
-        self.theaudiodb_genre = tadb_info['genre']
-        self.theaudiodb_mood = tadb_info['mood']
+        self.biography = tadb_info["biography"]
+        self.theaudiodb_genre = tadb_info["genre"]
+        self.theaudiodb_mood = tadb_info["mood"]
 
         img_temp = NamedTemporaryFile(delete=True)
-        img_temp.write(urlopen(tadb_info['thumb_url']).read())
+        img_temp.write(urlopen(tadb_info["thumb_url"]).read())
         img_temp.flush()
         img_filename = f"{self.name}_{self.uuid}.jpg"
         self.thumbnail.save(img_filename, File(img_temp))
 
     @property
     def rym_link(self):
-        artist_slug = self.name.lower().replace(' ', '-')
+        artist_slug = self.name.lower().replace(" ", "-")
         return f"https://rateyourmusic.com/artist/{artist_slug}/"
 
     @property
@@ -116,21 +116,21 @@ class Album(TimeStampedModel):
         return self.name
 
     def get_absolute_url(self):
-        return reverse("music:album_detail", kwargs={'slug': self.uuid})
+        return reverse("music:album_detail", kwargs={"slug": self.uuid})
 
     def scrobbles(self):
         from scrobbles.models import Scrobble
 
         return Scrobble.objects.filter(
             track__in=self.track_set.all()
-        ).order_by('-timestamp')
+        ).order_by("-timestamp")
 
     @property
     def tracks(self):
         return (
             self.track_set.all()
-            .annotate(scrobble_count=models.Count('scrobble'))
-            .order_by('-scrobble_count')
+            .annotate(scrobble_count=models.Count("scrobble"))
+            .order_by("-scrobble_count")
         )
 
     @property
@@ -142,7 +142,7 @@ class Album(TimeStampedModel):
         if self.primary_artist:
             artist = self.primary_artist.name
         album_data = lookup_album_from_tadb(self.name, artist)
-        if not album_data.get('theaudiodb_id'):
+        if not album_data.get("theaudiodb_id"):
             logger.info(f"No data for {self} found in TheAudioDB")
             return
 
@@ -154,21 +154,21 @@ class Album(TimeStampedModel):
             or not self.year
             or not self.musicbrainz_releasegroup_id
         ):
-            musicbrainzngs.set_useragent('vrobbler', '0.3.0')
+            musicbrainzngs.set_useragent("vrobbler", "0.3.0")
             mb_data = musicbrainzngs.get_release_by_id(
-                self.musicbrainz_id, includes=['artists', 'release-groups']
+                self.musicbrainz_id, includes=["artists", "release-groups"]
             )
             if not self.musicbrainz_releasegroup_id:
-                self.musicbrainz_releasegroup_id = mb_data['release'][
-                    'release-group'
-                ]['id']
+                self.musicbrainz_releasegroup_id = mb_data["release"][
+                    "release-group"
+                ]["id"]
             if not self.musicbrainz_albumartist_id:
-                self.musicbrainz_albumartist_id = mb_data['release'][
-                    'artist-credit'
-                ][0]['artist']['id']
+                self.musicbrainz_albumartist_id = mb_data["release"][
+                    "artist-credit"
+                ][0]["artist"]["id"]
             if not self.year:
                 try:
-                    self.year = mb_data['release']['date'][0:4]
+                    self.year = mb_data["release"]["date"][0:4]
                 except KeyError:
                     pass
                 except IndexError:
@@ -176,9 +176,9 @@ class Album(TimeStampedModel):
 
             self.save(
                 update_fields=[
-                    'musicbrainz_albumartist_id',
-                    'musicbrainz_releasegroup_id',
-                    'year',
+                    "musicbrainz_albumartist_id",
+                    "musicbrainz_releasegroup_id",
+                    "year",
                 ]
             )
 
@@ -192,7 +192,7 @@ class Album(TimeStampedModel):
                     self.artists.add(t.artist)
             if (
                 not self.cover_image
-                or self.cover_image == 'default-image-replace-me'
+                or self.cover_image == "default-image-replace-me"
             ):
                 self.fetch_artwork()
         self.scrape_theaudiodb()
@@ -206,10 +206,10 @@ class Album(TimeStampedModel):
                     )
                     name = f"{self.name}_{self.uuid}.jpg"
                     self.cover_image = ContentFile(img_data, name=name)
-                    logger.info(f'Setting image to {name}')
+                    logger.info(f"Setting image to {name}")
                 except musicbrainzngs.ResponseError:
                     logger.warning(
-                        f'No cover art found for {self.name} by release'
+                        f"No cover art found for {self.name} by release"
                     )
 
             if (
@@ -222,10 +222,10 @@ class Album(TimeStampedModel):
                     )
                     name = f"{self.name}_{self.uuid}.jpg"
                     self.cover_image = ContentFile(img_data, name=name)
-                    logger.info(f'Setting image to {name}')
+                    logger.info(f"Setting image to {name}")
                 except musicbrainzngs.ResponseError:
                     logger.warning(
-                        f'No cover art found for {self.name} by release group'
+                        f"No cover art found for {self.name} by release group"
                     )
             if not self.cover_image:
                 logger.debug(
@@ -257,8 +257,8 @@ class Album(TimeStampedModel):
 
     @property
     def rym_link(self):
-        artist_slug = self.primary_artist.name.lower().replace(' ', '-')
-        album_slug = self.name.lower().replace(' ', '-')
+        artist_slug = self.primary_artist.name.lower().replace(" ", "-")
+        album_slug = self.name.lower().replace(" ", "-")
         return f"https://rateyourmusic.com/release/album/{artist_slug}/{album_slug}/"
 
     @property
@@ -269,25 +269,25 @@ class Album(TimeStampedModel):
 
 
 class Track(ScrobblableMixin):
-    COMPLETION_PERCENT = getattr(settings, 'MUSIC_COMPLETION_PERCENT', 90)
+    COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 90)
 
     class Opinion(models.IntegerChoices):
-        DOWN = -1, 'Thumbs down'
-        NEUTRAL = 0, 'No opinion'
-        UP = 1, 'Thumbs up'
+        DOWN = -1, "Thumbs down"
+        NEUTRAL = 0, "No opinion"
+        UP = 1, "Thumbs up"
 
     artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
     album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
     musicbrainz_id = models.CharField(max_length=255, **BNULL)
 
     class Meta:
-        unique_together = [['album', 'musicbrainz_id']]
+        unique_together = [["album", "musicbrainz_id"]]
 
     def __str__(self):
         return f"{self.title} by {self.artist}"
 
     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):
@@ -310,8 +310,8 @@ class Track(ScrobblableMixin):
         exist.
 
         """
-        if not artist_dict.get('name') or not artist_dict.get(
-            'musicbrainz_id'
+        if not artist_dict.get("name") or not artist_dict.get(
+            "musicbrainz_id"
         ):
             logger.warning(
                 f"No artist or artist musicbrainz ID found in message from source, not scrobbling"
@@ -325,8 +325,8 @@ class Track(ScrobblableMixin):
         if not album.cover_image:
             album.fetch_artwork()
 
-        track_dict['album_id'] = getattr(album, "id", None)
-        track_dict['artist_id'] = artist.id
+        track_dict["album_id"] = getattr(album, "id", None)
+        track_dict["artist_id"] = artist.id
 
         track, created = cls.objects.get_or_create(**track_dict)
 

+ 31 - 31
vrobbler/apps/music/musicbrainz.py

@@ -9,40 +9,40 @@ logger = logging.getLogger(__name__)
 def lookup_album_from_mb(musicbrainz_id: str) -> dict:
     release_dict = {}
 
-    musicbrainzngs.set_useragent('vrobbler', '0.3.0')
+    musicbrainzngs.set_useragent("vrobbler", "0.3.0")
     release_data = musicbrainzngs.get_release_by_id(
         musicbrainz_id,
-        includes=['artists', 'release-groups', 'recordings'],
-    ).get('release')
+        includes=["artists", "release-groups", "recordings"],
+    ).get("release")
 
     if not release_data:
         return release_dict
 
-    primary_artist = release_data.get('artist-credit')[0]
+    primary_artist = release_data.get("artist-credit")[0]
     release_dict = {
-        'artist': {
-            'name': primary_artist.get('name'),
-            'musicbrainz_id': primary_artist.get('id'),
+        "artist": {
+            "name": primary_artist.get("name"),
+            "musicbrainz_id": primary_artist.get("id"),
         },
-        'album': {
-            'name': release_data.get('title'),
-            'musicbrainz_id': musicbrainz_id,
-            'musicbrainz_releasegroup_id': release_data.get(
-                'release-group'
-            ).get('id'),
-            'musicbrainz_albumaritist_id': primary_artist.get('id'),
-            'year': release_data.get('year')[0:4],
+        "album": {
+            "name": release_data.get("title"),
+            "musicbrainz_id": musicbrainz_id,
+            "musicbrainz_releasegroup_id": release_data.get(
+                "release-group"
+            ).get("id"),
+            "musicbrainz_albumaritist_id": primary_artist.get("id"),
+            "year": release_data.get("year")[0:4],
         },
     }
 
-    release_dict['tracks'] = []
-    for track in release_data.get('medium-list')[0]['track-list']:
-        recording = track['recording']
-        release_dict['tracks'].append(
+    release_dict["tracks"] = []
+    for track in release_data.get("medium-list")[0]["track-list"]:
+        recording = track["recording"]
+        release_dict["tracks"].append(
             {
-                'title': recording['title'],
-                'musicbrainz_id': recording['id'],
-                'run_time_ticks': track['length'],
+                "title": recording["title"],
+                "musicbrainz_id": recording["id"],
+                "run_time_ticks": track["length"],
             }
         )
 
@@ -50,12 +50,12 @@ def lookup_album_from_mb(musicbrainz_id: str) -> dict:
 
 
 def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
-    musicbrainzngs.set_useragent('vrobbler', '0.3.0')
+    musicbrainzngs.set_useragent("vrobbler", "0.3.0")
 
     top_result = musicbrainzngs.search_releases(
         release_name, artist=artist_name
-    )['release-list'][0]
-    score = int(top_result.get('ext:score'))
+    )["release-list"][0]
+    score = int(top_result.get("ext:score"))
     if score < 85:
         logger.debug(
             "Album lookup score below 85 threshold",
@@ -74,12 +74,12 @@ def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
 
 
 def lookup_artist_from_mb(artist_name: str) -> str:
-    musicbrainzngs.set_useragent('vrobbler', '0.3.0')
+    musicbrainzngs.set_useragent("vrobbler", "0.3.0")
 
     top_result = musicbrainzngs.search_artists(artist=artist_name)[
-        'artist-list'
+        "artist-list"
     ][0]
-    score = int(top_result.get('ext:score'))
+    score = int(top_result.get("ext:score"))
     if score < 85:
         logger.debug(
             "Artist lookup score below 85 threshold",
@@ -93,12 +93,12 @@ def lookup_artist_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(
         query=track_name, artist=artist_mbid, release=album_mbid
-    )['recording-list'][0]
-    score = int(top_result.get('ext:score'))
+    )["recording-list"][0]
+    score = int(top_result.get("ext:score"))
     if score < 85:
         logger.debug(
             "Track lookup score below 85 threshold",

+ 31 - 31
vrobbler/apps/music/theaudiodb.py

@@ -26,13 +26,13 @@ def lookup_artist_from_tadb(name: str) -> dict:
         return {}
 
     results = json.loads(response.content)
-    if results['artists']:
-        artist = results['artists'][0]
+    if results["artists"]:
+        artist = results["artists"][0]
 
-        artist_info['biography'] = artist.get('strBiographyEN')
-        artist_info['genre'] = artist.get('strGenre')
-        artist_info['mood'] = artist.get('strMood')
-        artist_info['thumb_url'] = artist.get('strArtistThumb')
+        artist_info["biography"] = artist.get("strBiographyEN")
+        artist_info["genre"] = artist.get("strGenre")
+        artist_info["mood"] = artist.get("strMood")
+        artist_info["thumb_url"] = artist.get("strArtistThumb")
 
     return artist_info
 
@@ -41,7 +41,7 @@ def lookup_album_from_tadb(name: str, artist: str) -> dict:
     album_info = {}
     artist = urllib.parse.quote(artist)
     name = urllib.parse.quote(name)
-    response = requests.get(''.join([ALBUM_SEARCH_URL, artist, "&a=", name]))
+    response = requests.get("".join([ALBUM_SEARCH_URL, artist, "&a=", name]))
 
     if response.status_code != 200:
         logger.warn(f"Bad response from TADB: {response.status_code}")
@@ -52,31 +52,31 @@ def lookup_album_from_tadb(name: str, artist: str) -> dict:
         return {}
 
     results = json.loads(response.content)
-    if results['album']:
-        album = results['album'][0]
-
-        album_info['theaudiodb_id'] = album.get('idAlbum')
-        album_info['theaudiodb_description'] = album.get('strDescriptionEN')
-        album_info['theaudiodb_genre'] = album.get('strGenre')
-        album_info['theaudiodb_style'] = album.get('strStyle')
-        album_info['theaudiodb_mood'] = album.get('strMood')
-        album_info['theaudiodb_speed'] = album.get('strSpeed')
-        album_info['theaudiodb_theme'] = album.get('strTheme')
-        album_info['allmusic_id'] = album.get('strAllMusicID')
-        album_info['wikipedia_slug'] = album.get('strWikipediaID')
-        album_info['discogs_id'] = album.get('strDiscogsID')
-        album_info['wikidata_id'] = album.get('strWikidataID')
-        album_info['rateyourmusic_id'] = album.get('strRateYourMusicID')
-
-        if album.get('intYearReleased'):
-            album_info['theaudiodb_year_released'] = float(
-                album.get('intYearReleased')
+    if results["album"]:
+        album = results["album"][0]
+
+        album_info["theaudiodb_id"] = album.get("idAlbum")
+        album_info["theaudiodb_description"] = album.get("strDescriptionEN")
+        album_info["theaudiodb_genre"] = album.get("strGenre")
+        album_info["theaudiodb_style"] = album.get("strStyle")
+        album_info["theaudiodb_mood"] = album.get("strMood")
+        album_info["theaudiodb_speed"] = album.get("strSpeed")
+        album_info["theaudiodb_theme"] = album.get("strTheme")
+        album_info["allmusic_id"] = album.get("strAllMusicID")
+        album_info["wikipedia_slug"] = album.get("strWikipediaID")
+        album_info["discogs_id"] = album.get("strDiscogsID")
+        album_info["wikidata_id"] = album.get("strWikidataID")
+        album_info["rateyourmusic_id"] = album.get("strRateYourMusicID")
+
+        if album.get("intYearReleased"):
+            album_info["theaudiodb_year_released"] = float(
+                album.get("intYearReleased")
             )
-        if album.get('intScore'):
-            album_info['theaudiodb_score'] = float(album.get('intScore'))
-        if album.get('intScoreVotes'):
-            album_info['theaudiodb_score_votes'] = int(
-                album.get('intScoreVotes')
+        if album.get("intScore"):
+            album_info["theaudiodb_score"] = float(album.get("intScore"))
+        if album.get("intScoreVotes"):
+            album_info["theaudiodb_score_votes"] = int(
+                album.get("intScoreVotes")
             )
 
     return album_info

+ 10 - 10
vrobbler/apps/music/urls.py

@@ -1,26 +1,26 @@
 from django.urls import path
 from music import views
 
-app_name = 'music'
+app_name = "music"
 
 
 urlpatterns = [
-    path('albums/', views.AlbumListView.as_view(), name='albums_list'),
+    path("albums/", views.AlbumListView.as_view(), name="albums_list"),
     path(
-        'album/<slug:slug>/',
+        "album/<slug:slug>/",
         views.AlbumDetailView.as_view(),
-        name='album_detail',
+        name="album_detail",
     ),
-    path("tracks/", views.TrackListView.as_view(), name='tracks_list'),
+    path("tracks/", views.TrackListView.as_view(), name="tracks_list"),
     path(
-        'tracks/<slug:slug>/',
+        "tracks/<slug:slug>/",
         views.TrackDetailView.as_view(),
-        name='track_detail',
+        name="track_detail",
     ),
-    path('artists/', views.ArtistListView.as_view(), name='artist_list'),
+    path("artists/", views.ArtistListView.as_view(), name="artist_list"),
     path(
-        'artists/<slug:slug>/',
+        "artists/<slug:slug>/",
         views.ArtistDetailView.as_view(),
-        name='artist_detail',
+        name="artist_detail",
     ),
 ]

+ 9 - 9
vrobbler/apps/music/utils.py

@@ -17,19 +17,19 @@ from music.models import Album, Artist, Track
 
 def get_or_create_artist(name: str, mbid: str = None) -> Artist:
     artist = None
-    logger.debug(f'Got artist {name} and mbid: {mbid}')
+    logger.debug(f"Got artist {name} and mbid: {mbid}")
 
-    if 'feat.' in name.lower():
+    if "feat." in name.lower():
         name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip()
-    if 'featuring' in name.lower():
+    if "featuring" in name.lower():
         name = re.split("featuring", name, flags=re.IGNORECASE)[0].strip()
-    if '&' in name.lower():
+    if "&" in name.lower():
         name = re.split("&", name, flags=re.IGNORECASE)[0].strip()
 
     artist_dict = lookup_artist_from_mb(name)
-    mbid = mbid or artist_dict['id']
+    mbid = mbid or artist_dict["id"]
 
-    logger.debug(f'Looking up artist {name} and mbid: {mbid}')
+    logger.debug(f"Looking up artist {name} and mbid: {mbid}")
     artist = Artist.objects.filter(musicbrainz_id=mbid).first()
     if not artist:
         artist = Artist.objects.create(name=name, musicbrainz_id=mbid)
@@ -44,9 +44,9 @@ def get_or_create_artist(name: str, mbid: str = None) -> Artist:
 def get_or_create_album(name: str, artist: Artist, mbid: str = None) -> Album:
     album = None
     album_dict = lookup_album_dict_from_mb(name, artist_name=artist.name)
-    mbid = mbid or album_dict['mb_id']
+    mbid = mbid or album_dict["mb_id"]
 
-    logger.debug(f'Looking up album {name} and mbid: {mbid}')
+    logger.debug(f"Looking up album {name} and mbid: {mbid}")
 
     album = Album.objects.filter(musicbrainz_id=mbid).first()
     if not album:
@@ -81,7 +81,7 @@ def get_or_create_track(
             title,
             artist.musicbrainz_id,
             album.musicbrainz_id,
-        )['id']
+        )["id"]
 
     track = Track.objects.filter(musicbrainz_id=mbid).first()
 

+ 10 - 10
vrobbler/apps/music/views.py

@@ -17,11 +17,11 @@ class TrackListView(generic.ListView):
 
 class TrackDetailView(generic.DetailView):
     model = Track
-    slug_field = 'uuid'
+    slug_field = "uuid"
 
     def get_context_data(self, **kwargs):
         context_data = super().get_context_data(**kwargs)
-        context_data['charts'] = ChartRecord.objects.filter(
+        context_data["charts"] = ChartRecord.objects.filter(
             track=self.object, rank__in=[1, 2, 3]
         )
         return context_data
@@ -35,7 +35,7 @@ class ArtistListView(generic.ListView):
         return (
             super()
             .get_queryset()
-            .annotate(scrobble_count=Count('track__scrobble'))
+            .annotate(scrobble_count=Count("track__scrobble"))
             .order_by("-scrobble_count")
         )
 
@@ -43,17 +43,17 @@ class ArtistListView(generic.ListView):
         context_data = super().get_context_data(
             object_list=object_list, **kwargs
         )
-        context_data['view'] = self.request.GET.get('view')
+        context_data["view"] = self.request.GET.get("view")
         return context_data
 
 
 class ArtistDetailView(generic.DetailView):
     model = Artist
-    slug_field = 'uuid'
+    slug_field = "uuid"
 
     def get_context_data(self, **kwargs):
         context_data = super().get_context_data(**kwargs)
-        artist = context_data['object']
+        artist = context_data["object"]
         rank = 1
         tracks_ranked = []
         scrobbles = artist.tracks.first().scrobble_count
@@ -63,8 +63,8 @@ class ArtistDetailView(generic.DetailView):
             tracks_ranked.append((rank, track))
             scrobbles = track.scrobble_count
 
-        context_data['tracks_ranked'] = tracks_ranked
-        context_data['charts'] = ChartRecord.objects.filter(
+        context_data["tracks_ranked"] = tracks_ranked
+        context_data["charts"] = ChartRecord.objects.filter(
             artist=self.object, rank__in=[1, 2, 3]
         )
         return context_data
@@ -77,14 +77,14 @@ class AlbumListView(generic.ListView):
         return (
             super()
             .get_queryset()
-            .annotate(scrobble_count=Count('track__scrobble'))
+            .annotate(scrobble_count=Count("track__scrobble"))
             .order_by("-scrobble_count")
         )
 
 
 class AlbumDetailView(generic.DetailView):
     model = Album
-    slug_field = 'uuid'
+    slug_field = "uuid"
 
     def get_context_data(self, **kwargs):
         context_data = super().get_context_data(**kwargs)

+ 1 - 1
vrobbler/apps/podcasts/apps.py

@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class PodcastsConfig(AppConfig):
-    name = 'podcasts'
+    name = "podcasts"

+ 4 - 4
vrobbler/apps/podcasts/models.py

@@ -35,7 +35,7 @@ class Podcast(TimeStampedModel):
 
 
 class Episode(ScrobblableMixin):
-    COMPLETION_PERCENT = getattr(settings, 'PODCAST_COMPLETION_PERCENT', 90)
+    COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90)
 
     podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)
     number = models.IntegerField(**BNULL)
@@ -61,12 +61,12 @@ class Episode(ScrobblableMixin):
         producer before saving the epsiode so it can be scrobbled.
 
         """
-        if not podcast_dict.get('name'):
+        if not podcast_dict.get("name"):
             logger.warning(f"No name from source for podcast, not scrobbling")
             return
 
         producer = None
-        if producer_dict.get('name'):
+        if producer_dict.get("name"):
             producer, producer_created = Producer.objects.get_or_create(
                 **producer_dict
             )
@@ -85,7 +85,7 @@ class Episode(ScrobblableMixin):
         else:
             logger.debug(f"Found podcast {podcast}")
 
-        episode_dict['podcast_id'] = podcast.id
+        episode_dict["podcast_id"] = podcast.id
 
         episode, created = cls.objects.get_or_create(**episode_dict)
         if created:

+ 2 - 2
vrobbler/apps/profiles/api/serializers.py

@@ -9,10 +9,10 @@ from profiles.models import UserProfile
 class UserSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = User
-        exclude = ('password',)
+        exclude = ("password",)
 
 
 class UserProfileSerializer(serializers.HyperlinkedModelSerializer):
     class Meta:
         model = UserProfile
-        exclude = ('lastfm_password',)
+        exclude = ("lastfm_password",)

+ 2 - 2
vrobbler/apps/profiles/api/views.py

@@ -13,7 +13,7 @@ class UserViewSet(viewsets.ModelViewSet):
     API endpoint that allows users to be viewed or edited.
     """
 
-    queryset = User.objects.all().order_by('-date_joined')
+    queryset = User.objects.all().order_by("-date_joined")
     serializer_class = UserSerializer
     permission_classes = [permissions.IsAuthenticated]
 
@@ -23,6 +23,6 @@ class UserProfileViewSet(viewsets.ModelViewSet):
     API endpoint that allows users to be viewed or edited.
     """
 
-    queryset = UserProfile.objects.all().order_by('-created')
+    queryset = UserProfile.objects.all().order_by("-created")
     serializer_class = UserProfileSerializer
     permission_classes = [permissions.IsAuthenticated]

+ 7 - 7
vrobbler/apps/scrobbles/admin.py

@@ -11,8 +11,8 @@ from scrobbles.models import (
 class ScrobbleInline(admin.TabularInline):
     model = Scrobble
     extra = 0
-    raw_id_fields = ('video', 'podcast_episode', 'track')
-    exclude = ('source_id', 'scrobble_log')
+    raw_id_fields = ("video", "podcast_episode", "track")
+    exclude = ("source_id", "scrobble_log")
 
 
 class ImportBaseAdmin(admin.ModelAdmin):
@@ -81,11 +81,11 @@ class ScrobbleAdmin(admin.ModelAdmin):
         "played_to_completion",
     )
     raw_id_fields = (
-        'video',
-        'podcast_episode',
-        'track',
-        'sport_event',
-        'book',
+        "video",
+        "podcast_episode",
+        "track",
+        "sport_event",
+        "book",
     )
     list_filter = ("is_paused", "in_progress", "source", "track__artist")
     ordering = ("-timestamp",)

+ 4 - 4
vrobbler/apps/scrobbles/api/views.py

@@ -14,7 +14,7 @@ from scrobbles.models import (
 
 
 class ScrobbleViewSet(viewsets.ModelViewSet):
-    queryset = Scrobble.objects.all().order_by('-timestamp')
+    queryset = Scrobble.objects.all().order_by("-timestamp")
     serializer_class = ScrobbleSerializer
     permission_classes = [permissions.IsAuthenticated]
 
@@ -23,7 +23,7 @@ class ScrobbleViewSet(viewsets.ModelViewSet):
 
 
 class KoReaderImportViewSet(viewsets.ModelViewSet):
-    queryset = KoReaderImport.objects.all().order_by('-created')
+    queryset = KoReaderImport.objects.all().order_by("-created")
     serializer_class = KoReaderImportSerializer
     permission_classes = [permissions.IsAuthenticated]
 
@@ -32,7 +32,7 @@ class KoReaderImportViewSet(viewsets.ModelViewSet):
 
 
 class AudioScrobblerTSVImportViewSet(viewsets.ModelViewSet):
-    queryset = AudioScrobblerTSVImport.objects.all().order_by('-created')
+    queryset = AudioScrobblerTSVImport.objects.all().order_by("-created")
     serializer_class = AudioScrobblerTSVImportSerializer
     permission_classes = [permissions.IsAuthenticated]
 
@@ -41,7 +41,7 @@ class AudioScrobblerTSVImportViewSet(viewsets.ModelViewSet):
 
 
 class LastFmImportViewSet(viewsets.ModelViewSet):
-    queryset = LastFmImport.objects.all().order_by('-created')
+    queryset = LastFmImport.objects.all().order_by("-created")
     serializer_class = LastFmImportSerializer
     permission_classes = [permissions.IsAuthenticated]
 

+ 1 - 1
vrobbler/apps/scrobbles/apps.py

@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class ScrobblesConfig(AppConfig):
-    name = 'scrobbles'
+    name = "scrobbles"

+ 1 - 1
vrobbler/apps/scrobbles/context_processors.py

@@ -9,7 +9,7 @@ def now_playing(request):
     if not user.is_authenticated:
         return {}
     return {
-        'now_playing_list': Scrobble.objects.filter(
+        "now_playing_list": Scrobble.objects.filter(
             in_progress=True,
             is_paused=False,
             user=user,

+ 9 - 9
vrobbler/apps/scrobbles/export.py

@@ -17,19 +17,19 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"):
         start_query, end_query, track__isnull=False
     )
     headers = []
-    extension = 'tsv'
-    delimiter = '\t'
+    extension = "tsv"
+    delimiter = "\t"
 
     if format == "as":
         headers = [
-            ['#AUDIOSCROBBLER/1.1'],
-            ['#TZ/UTC'],
-            ['#CLIENT/Vrobbler 1.0.0'],
+            ["#AUDIOSCROBBLER/1.1"],
+            ["#TZ/UTC"],
+            ["#CLIENT/Vrobbler 1.0.0"],
         ]
 
     if format == "csv":
-        delimiter = ','
-        extension = 'csv'
+        delimiter = ","
+        extension = "csv"
         headers = [
             [
                 "artists",
@@ -43,7 +43,7 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"):
             ]
         ]
 
-    with tempfile.NamedTemporaryFile(mode='w', delete=False) as outfile:
+    with tempfile.NamedTemporaryFile(mode="w", delete=False) as outfile:
         writer = csv.writer(outfile, delimiter=delimiter)
         for row in headers:
             writer.writerow(row)
@@ -60,7 +60,7 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"):
                 track_number,
                 track.run_time,
                 track_rating,
-                scrobble.timestamp.strftime('%s'),
+                scrobble.timestamp.strftime("%s"),
                 track.musicbrainz_id,
             ]
             writer.writerow(row)

+ 6 - 6
vrobbler/apps/scrobbles/forms.py

@@ -5,9 +5,9 @@ class ExportScrobbleForm(forms.Form):
     """Provide options for downloading scrobbles"""
 
     EXPORT_TYPES = (
-        ('as', 'Audioscrobbler'),
-        ('csv', 'CSV'),
-        ('html', 'HTML'),
+        ("as", "Audioscrobbler"),
+        ("csv", "CSV"),
+        ("html", "HTML"),
     )
     export_type = forms.ChoiceField(choices=EXPORT_TYPES)
 
@@ -17,9 +17,9 @@ class ScrobbleForm(forms.Form):
         label="",
         widget=forms.TextInput(
             attrs={
-                'class': "form-control form-control-dark w-100",
-                'placeholder': "Scrobble something (IMDB ID, String, TVDB ID ...)",
-                'aria-label': "Scrobble something",
+                "class": "form-control form-control-dark w-100",
+                "placeholder": "Scrobble something (IMDB ID, String, TVDB ID ...)",
+                "aria-label": "Scrobble something",
             }
         ),
     )

+ 42 - 42
vrobbler/apps/scrobbles/models.py

@@ -49,17 +49,17 @@ class BaseFileImportMixin(TimeStampedModel):
     def human_start(self):
         start = "Unknown"
         if self.processing_started:
-            start = self.processing_started.strftime('%B %d, %Y at %H:%M')
+            start = self.processing_started.strftime("%B %d, %Y at %H:%M")
         return start
 
     @property
     def import_type(self) -> str:
         class_name = self.__class__.__name__
-        if class_name == 'AudioscrobblerTSVImport':
+        if class_name == "AudioscrobblerTSVImport":
             return "Audioscrobbler"
-        if class_name == 'KoReaderImport':
+        if class_name == "KoReaderImport":
             return "KoReader"
-        if self.__class__.__name__ == 'LastFMImport':
+        if self.__class__.__name__ == "LastFMImport":
             return "LastFM"
         return "Generic"
 
@@ -75,7 +75,7 @@ class BaseFileImportMixin(TimeStampedModel):
             logger.warning("No lines in process log found to undo")
             return
 
-        for line in self.process_log.split('\n'):
+        for line in self.process_log.split("\n"):
             scrobble_id = line.split("\t")[0]
             scrobble = Scrobble.objects.filter(id=scrobble_id).first()
             if not scrobble:
@@ -105,7 +105,7 @@ class BaseFileImportMixin(TimeStampedModel):
 
     def mark_finished(self):
         self.processed_finished = timezone.now()
-        self.save(update_fields=['processed_finished'])
+        self.save(update_fields=["processed_finished"])
 
     def record_log(self, scrobbles):
         self.process_log = ""
@@ -133,13 +133,13 @@ class KoReaderImport(BaseFileImportMixin):
 
     def get_absolute_url(self):
         return reverse(
-            'scrobbles:koreader-import-detail', kwargs={'slug': self.uuid}
+            "scrobbles:koreader-import-detail", kwargs={"slug": self.uuid}
         )
 
     def get_path(instance, filename):
-        extension = filename.split('.')[-1]
+        extension = filename.split(".")[-1]
         uuid = instance.uuid
-        return f'koreader-uploads/{uuid}.{extension}'
+        return f"koreader-uploads/{uuid}.{extension}"
 
     sqlite_file = models.FileField(upload_to=get_path, **BNULL)
 
@@ -169,13 +169,13 @@ class AudioScrobblerTSVImport(BaseFileImportMixin):
 
     def get_absolute_url(self):
         return reverse(
-            'scrobbles:tsv-import-detail', kwargs={'slug': self.uuid}
+            "scrobbles:tsv-import-detail", kwargs={"slug": self.uuid}
         )
 
     def get_path(instance, filename):
-        extension = filename.split('.')[-1]
+        extension = filename.split(".")[-1]
         uuid = instance.uuid
-        return f'audioscrobbler-uploads/{uuid}.{extension}'
+        return f"audioscrobbler-uploads/{uuid}.{extension}"
 
     tsv_file = models.FileField(upload_to=get_path, **BNULL)
 
@@ -209,7 +209,7 @@ class LastFmImport(BaseFileImportMixin):
 
     def get_absolute_url(self):
         return reverse(
-            'scrobbles:lastfm-import-detail', kwargs={'slug': self.uuid}
+            "scrobbles:lastfm-import-detail", kwargs={"slug": self.uuid}
         )
 
     def process(self, import_all=False):
@@ -332,13 +332,13 @@ class ChartRecord(TimeStampedModel):
 
     @property
     def period_type(self) -> str:
-        period = 'year'
+        period = "year"
         if self.month:
-            period = 'month'
+            period = "month"
         if self.week:
-            period = 'week'
+            period = "week"
         if self.day:
-            period = 'day'
+            period = "day"
         return period
 
     def __str__(self):
@@ -357,7 +357,7 @@ class ChartRecord(TimeStampedModel):
             get_params = get_params = get_params + f"-{self.day}"
         if self.artist:
             get_params = get_params + "&media=Artist"
-        return reverse('scrobbles:charts-home') + get_params
+        return reverse("scrobbles:charts-home") + get_params
 
     @classmethod
     def build(cls, user, **kwargs):
@@ -424,12 +424,12 @@ class Scrobble(TimeStampedModel):
     @property
     def status(self) -> str:
         if self.is_paused:
-            return 'paused'
+            return "paused"
         if self.played_to_completion:
-            return 'finished'
+            return "finished"
         if self.in_progress:
-            return 'in-progress'
-        return 'zombie'
+            return "in-progress"
+        return "zombie"
 
     @property
     def is_stale(self) -> bool:
@@ -488,7 +488,7 @@ class Scrobble(TimeStampedModel):
         return media_obj
 
     def __str__(self):
-        timestamp = self.timestamp.strftime('%Y-%m-%d')
+        timestamp = self.timestamp.strftime("%Y-%m-%d")
         return f"Scrobble of {self.media_obj} ({timestamp})"
 
     @classmethod
@@ -496,28 +496,28 @@ class Scrobble(TimeStampedModel):
         cls, media, user_id: int, scrobble_data: dict
     ) -> "Scrobble":
 
-        if media.__class__.__name__ == 'Track':
+        if media.__class__.__name__ == "Track":
             media_query = models.Q(track=media)
-            scrobble_data['track_id'] = media.id
-        if media.__class__.__name__ == 'Video':
+            scrobble_data["track_id"] = media.id
+        if media.__class__.__name__ == "Video":
             media_query = models.Q(video=media)
-            scrobble_data['video_id'] = media.id
-        if media.__class__.__name__ == 'Episode':
+            scrobble_data["video_id"] = media.id
+        if media.__class__.__name__ == "Episode":
             media_query = models.Q(podcast_episode=media)
-            scrobble_data['podcast_episode_id'] = media.id
-        if media.__class__.__name__ == 'SportEvent':
+            scrobble_data["podcast_episode_id"] = media.id
+        if media.__class__.__name__ == "SportEvent":
             media_query = models.Q(sport_event=media)
-            scrobble_data['sport_event_id'] = media.id
-        if media.__class__.__name__ == 'Book':
+            scrobble_data["sport_event_id"] = media.id
+        if media.__class__.__name__ == "Book":
             media_query = models.Q(book=media)
-            scrobble_data['book_id'] = media.id
+            scrobble_data["book_id"] = media.id
 
         scrobble = (
             cls.objects.filter(
                 media_query,
                 user_id=user_id,
             )
-            .order_by('-modified')
+            .order_by("-modified")
             .first()
         )
         if scrobble and scrobble.can_be_updated:
@@ -527,21 +527,21 @@ class Scrobble(TimeStampedModel):
             )
             return scrobble.update(scrobble_data)
 
-        source = scrobble_data['source']
+        source = scrobble_data["source"]
         logger.info(
             f"Creating for {media.id} - {source}",
             {"scrobble_data": scrobble_data, "media": media},
         )
         # If creating a new scrobble, we don't need status
-        scrobble_data.pop('mopidy_status', None)
-        scrobble_data.pop('jellyfin_status', None)
+        scrobble_data.pop("mopidy_status", None)
+        scrobble_data.pop("jellyfin_status", None)
         return cls.create(scrobble_data)
 
     def update(self, scrobble_data: dict) -> "Scrobble":
         # Status is a field we get from Mopidy, which refuses to poll us
-        scrobble_status = scrobble_data.pop('mopidy_status', None)
+        scrobble_status = scrobble_data.pop("mopidy_status", None)
         if not scrobble_status:
-            scrobble_status = scrobble_data.pop('jellyfin_status', None)
+            scrobble_status = scrobble_data.pop("jellyfin_status", None)
 
         if self.percent_played < 100:
             # Only worry about ticks if we haven't gotten to the end
@@ -566,7 +566,7 @@ class Scrobble(TimeStampedModel):
         cls,
         scrobble_data: dict,
     ) -> "Scrobble":
-        scrobble_data['scrobble_log'] = ""
+        scrobble_data["scrobble_log"] = ""
         scrobble = cls.objects.create(
             **scrobble_data,
         )
@@ -576,7 +576,7 @@ class Scrobble(TimeStampedModel):
         if not self.in_progress:
             return
         self.in_progress = False
-        self.save(update_fields=['in_progress'])
+        self.save(update_fields=["in_progress"])
         logger.info(f"{self.id} - {self.source}")
         check_scrobble_for_finish(self, force_finish)
 
@@ -607,5 +607,5 @@ class Scrobble(TimeStampedModel):
             f"{self.id} - {self.playback_position_ticks} - {self.source}"
         )
         self.save(
-            update_fields=['playback_position_ticks', 'playback_position']
+            update_fields=["playback_position_ticks", "playback_position"]
         )

+ 3 - 3
vrobbler/apps/scrobbles/scrobblers.py

@@ -98,7 +98,7 @@ def build_scrobble_dict(data_dict: dict, user_id: int) -> dict:
     jellyfin_status = "resumed"
     if data_dict.get("IsPaused"):
         jellyfin_status = "paused"
-    elif data_dict.get("NotificationType") == 'PlaybackStop':
+    elif data_dict.get("NotificationType") == "PlaybackStop":
         jellyfin_status = "stopped"
 
     playback_ticks = data_dict.get("PlaybackPositionTicks", "")
@@ -111,7 +111,7 @@ def build_scrobble_dict(data_dict: dict, user_id: int) -> dict:
         "playback_position_ticks": playback_ticks,
         "playback_position": data_dict.get("PlaybackPosition", ""),
         "source": data_dict.get("ClientName", "Vrobbler"),
-        "source_id": data_dict.get('MediaSourceId'),
+        "source_id": data_dict.get("MediaSourceId"),
         "jellyfin_status": jellyfin_status,
     }
 
@@ -137,7 +137,7 @@ def jellyfin_scrobble_track(
     album = get_or_create_album(
         data_dict.get(JELLYFIN_POST_KEYS["ALBUM_NAME"]),
         artist=artist,
-        mbid=data_dict.get(JELLYFIN_POST_KEYS['ALBUM_MB_ID']),
+        mbid=data_dict.get(JELLYFIN_POST_KEYS["ALBUM_MB_ID"]),
     )
 
     run_time_ticks = (

+ 25 - 25
vrobbler/apps/scrobbles/stats.py

@@ -38,14 +38,14 @@ def get_scrobble_count_qs(
         tz = pytz.timezone(user.profile.timezone)
 
     tz = pytz.utc
-    data_model = apps.get_model(app_label='music', model_name='Track')
+    data_model = apps.get_model(app_label="music", model_name="Track")
     if model_str == "Artist":
-        data_model = apps.get_model(app_label='music', model_name='Artist')
+        data_model = apps.get_model(app_label="music", model_name="Artist")
     if model_str == "Video":
-        data_model = apps.get_model(app_label='videos', model_name='Video')
+        data_model = apps.get_model(app_label="videos", model_name="Video")
     if model_str == "SportEvent":
         data_model = apps.get_model(
-            app_label='sports', model_name='SportEvent'
+            app_label="sports", model_name="SportEvent"
         )
 
     if model_str == "Artist":
@@ -69,14 +69,14 @@ def get_scrobble_count_qs(
     end = datetime(year, 12, 31, tzinfo=tz)
 
     if year and day and month:
-        logger.debug('Filtering by year, month and day')
+        logger.debug("Filtering by year, month and day")
         start = datetime(year, month, day, 0, 0, tzinfo=tz)
         end = datetime(year, month, day, 23, 59, tzinfo=tz)
     elif year and week:
-        logger.debug('Filtering by year and week')
+        logger.debug("Filtering by year and week")
         start, end = get_start_end_dates_by_week(year, week, tz)
     elif month:
-        logger.debug('Filtering by month')
+        logger.debug("Filtering by month")
         end_day = calendar.monthrange(year, month)[1]
         start = datetime(year, month, 1, tzinfo=tz)
         end = datetime(year, month, end_day, tzinfo=tz)
@@ -113,7 +113,7 @@ def build_charts(
     model_str="Track",
 ):
     ChartRecord = apps.get_model(
-        app_label='scrobbles', model_name='ChartRecord'
+        app_label="scrobbles", model_name="ChartRecord"
     )
     results = get_scrobble_count_qs(year, month, week, day, user, model_str)
     unique_counts = list(set([result.scrobble_count for result in results]))
@@ -125,20 +125,20 @@ def build_charts(
     chart_records = []
     for result in results:
         chart_record = {
-            'year': year,
-            'week': week,
-            'month': month,
-            'day': day,
-            'user': user,
+            "year": year,
+            "week": week,
+            "month": month,
+            "day": day,
+            "user": user,
         }
-        chart_record['rank'] = ranks[result.scrobble_count]
-        chart_record['count'] = result.scrobble_count
-        if model_str == 'Track':
-            chart_record['track'] = result
-        if model_str == 'Video':
-            chart_record['video'] = result
-        if model_str == 'Artist':
-            chart_record['artist'] = result
+        chart_record["rank"] = ranks[result.scrobble_count]
+        chart_record["count"] = result.scrobble_count
+        if model_str == "Track":
+            chart_record["track"] = result
+        if model_str == "Video":
+            chart_record["video"] = result
+        if model_str == "Artist":
+            chart_record["artist"] = result
         chart_records.append(ChartRecord(**chart_record))
     ChartRecord.objects.bulk_create(
         chart_records, ignore_conflicts=True, batch_size=500
@@ -148,7 +148,7 @@ def build_charts(
 def build_yesterdays_charts_for_user(user: "User", model_str="Track") -> None:
     """Given a user calculate needed charts."""
     ChartRecord = apps.get_model(
-        app_label='scrobbles', model_name='ChartRecord'
+        app_label="scrobbles", model_name="ChartRecord"
     )
     tz = pytz.timezone(settings.TIME_ZONE)
     if user and user.is_authenticated:
@@ -199,9 +199,9 @@ def build_yesterdays_charts_for_user(user: "User", model_str="Track") -> None:
 def build_missing_charts_for_user(user: "User", model_str="Track") -> None:
     """"""
     ChartRecord = apps.get_model(
-        app_label='scrobbles', model_name='ChartRecord'
+        app_label="scrobbles", model_name="ChartRecord"
     )
-    Scrobble = apps.get_model(app_label='scrobbles', model_name='Scrobble')
+    Scrobble = apps.get_model(app_label="scrobbles", model_name="Scrobble")
 
     logger.info(f"Generating historical charts for {user}")
     tz = pytz.timezone(settings.TIME_ZONE)
@@ -211,7 +211,7 @@ def build_missing_charts_for_user(user: "User", model_str="Track") -> None:
 
     first_scrobble = (
         Scrobble.objects.filter(user=user, played_to_completion=True)
-        .order_by('created')
+        .order_by("created")
         .first()
     )
 

+ 1 - 1
vrobbler/apps/scrobbles/templatetags/urlreplace.py

@@ -5,6 +5,6 @@ register = template.Library()
 
 @register.simple_tag(takes_context=True)
 def urlreplace(context, **kwargs):
-    query = context['request'].GET.copy()
+    query = context["request"].GET.copy()
     query.update(kwargs)
     return query.urlencode()

+ 4 - 4
vrobbler/apps/scrobbles/tsv.py

@@ -20,7 +20,7 @@ def process_audioscrobbler_tsv_file(file_path, user_id, user_tz=None):
         user_tz = pytz.utc
 
     with open(file_path) as infile:
-        source = 'Audioscrobbler File'
+        source = "Audioscrobbler File"
         rows = csv.reader(infile, delimiter="\t")
 
         source_id = ""
@@ -32,8 +32,8 @@ def process_audioscrobbler_tsv_file(file_path, user_id, user_tz=None):
                 continue
             if len(row) > 8:
                 logger.warning(
-                    'Improper row length during Audioscrobbler import',
-                    extra={'row': row},
+                    "Improper row length during Audioscrobbler import",
+                    extra={"row": row},
                 )
                 continue
             artist = get_or_create_artist(row[0])
@@ -75,6 +75,6 @@ def process_audioscrobbler_tsv_file(file_path, user_id, user_tz=None):
         created = Scrobble.objects.bulk_create(new_scrobbles)
         logger.info(
             f"Created {len(created)} scrobbles",
-            extra={'created_scrobbles': created},
+            extra={"created_scrobbles": created},
         )
         return created

+ 28 - 28
vrobbler/apps/scrobbles/urls.py

@@ -1,70 +1,70 @@
 from django.urls import path
 from scrobbles import views
 
-app_name = 'scrobbles'
+app_name = "scrobbles"
 
 urlpatterns = [
     path(
-        'manual/imdb/',
+        "manual/imdb/",
         views.ManualScrobbleView.as_view(),
-        name='imdb-manual-scrobble',
+        name="imdb-manual-scrobble",
     ),
     path(
-        'manual/audioscrobbler/',
+        "manual/audioscrobbler/",
         views.AudioScrobblerImportCreateView.as_view(),
-        name='audioscrobbler-file-upload',
+        name="audioscrobbler-file-upload",
     ),
     path(
-        'manual/koreader/',
+        "manual/koreader/",
         views.KoReaderImportCreateView.as_view(),
-        name='koreader-file-upload',
+        name="koreader-file-upload",
     ),
-    path('finish/<slug:uuid>', views.scrobble_finish, name='finish'),
-    path('cancel/<slug:uuid>', views.scrobble_cancel, name='cancel'),
+    path("finish/<slug:uuid>", views.scrobble_finish, name="finish"),
+    path("cancel/<slug:uuid>", views.scrobble_cancel, name="cancel"),
     path(
-        'upload/',
+        "upload/",
         views.AudioScrobblerImportCreateView.as_view(),
-        name='audioscrobbler-file-upload',
+        name="audioscrobbler-file-upload",
     ),
     path(
-        'lastfm-import/',
+        "lastfm-import/",
         views.lastfm_import,
-        name='lastfm-import',
+        name="lastfm-import",
     ),
     path(
-        'webhook/jellyfin/',
+        "webhook/jellyfin/",
         views.jellyfin_webhook,
-        name='jellyfin-webhook',
+        name="jellyfin-webhook",
     ),
     path(
-        'webhook/mopidy/',
+        "webhook/mopidy/",
         views.mopidy_webhook,
-        name='mopidy-webhook',
+        name="mopidy-webhook",
     ),
-    path('export/', views.export, name='export'),
+    path("export/", views.export, name="export"),
     path(
-        'imports/',
+        "imports/",
         views.ScrobbleImportListView.as_view(),
-        name='import-detail',
+        name="import-detail",
     ),
     path(
-        'imports/tsv/<slug:slug>/',
+        "imports/tsv/<slug:slug>/",
         views.ScrobbleTSVImportDetailView.as_view(),
-        name='tsv-import-detail',
+        name="tsv-import-detail",
     ),
     path(
-        'imports/lastfm/<slug:slug>/',
+        "imports/lastfm/<slug:slug>/",
         views.ScrobbleLastFMImportDetailView.as_view(),
-        name='lastfm-import-detail',
+        name="lastfm-import-detail",
     ),
     path(
-        'imports/koreader/<slug:slug>/',
+        "imports/koreader/<slug:slug>/",
         views.ScrobbleKoReaderImportDetailView.as_view(),
-        name='koreader-import-detail',
+        name="koreader-import-detail",
     ),
     path(
-        'charts/',
+        "charts/",
         views.ChartRecordView.as_view(),
-        name='charts-home',
+        name="charts-home",
     ),
 ]

+ 11 - 11
vrobbler/apps/scrobbles/utils.py

@@ -29,7 +29,7 @@ def convert_to_seconds(run_time: str) -> int:
 
 def parse_mopidy_uri(uri: str) -> dict:
     logger.debug(f"Parsing URI: {uri}")
-    parsed_uri = uri.split('/')
+    parsed_uri = uri.split("/")
 
     episode_str = unquote(parsed_uri.pop(-1).strip(".mp3"))
     podcast_str = unquote(parsed_uri.pop(-1))
@@ -43,9 +43,9 @@ def parse_mopidy_uri(uri: str) -> dict:
 
     try:
         if pub_date:
-            episode_num = int(episode_str.split('-')[3])
+            episode_num = int(episode_str.split("-")[3])
         else:
-            episode_num = int(episode_str.split('-')[0])
+            episode_num = int(episode_str.split("-")[0])
     except IndexError:
         episode_num = None
     except ValueError:
@@ -59,14 +59,14 @@ def parse_mopidy_uri(uri: str) -> dict:
         episode_num_gap = len(str(episode_num)) + 1
         episode_str = episode_str.strip(episode_str[:episode_num_gap])
 
-    episode_str = episode_str.replace('-', ' ')
+    episode_str = episode_str.replace("-", " ")
     logger.debug(f"Found episode name {episode_str} from Mopidy URI")
 
     return {
-        'episode_filename': episode_str,
-        'episode_num': episode_num,
-        'podcast_name': podcast_str,
-        'pub_date': pub_date,
+        "episode_filename": episode_str,
+        "episode_num": episode_num,
+        "podcast_name": podcast_str,
+        "pub_date": pub_date,
     }
 
 
@@ -99,19 +99,19 @@ def check_scrobble_for_finish(
                 "in_progress",
                 "is_paused",
                 "played_to_completion",
-                'playback_position_ticks',
+                "playback_position_ticks",
             ]
         )
 
     if scrobble.percent_played % 5 == 0:
         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.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':
+    if media_obj.__class__.__name__ == "Book":
         media_query = models.Q(book=media_obj)
     return Scrobble.objects.filter(media_query, user=user)

+ 83 - 83
vrobbler/apps/scrobbles/views.py

@@ -70,28 +70,28 @@ class RecentScrobbleList(ListView):
             completed_for_user = Scrobble.objects.filter(
                 played_to_completion=True, user=user
             )
-            data['video_scrobble_list'] = completed_for_user.filter(
+            data["video_scrobble_list"] = completed_for_user.filter(
                 video__isnull=False
-            ).order_by('-timestamp')[:15]
+            ).order_by("-timestamp")[:15]
 
-            data['podcast_scrobble_list'] = completed_for_user.filter(
+            data["podcast_scrobble_list"] = completed_for_user.filter(
                 podcast_episode__isnull=False
-            ).order_by('-timestamp')[:15]
+            ).order_by("-timestamp")[:15]
 
-            data['sport_scrobble_list'] = completed_for_user.filter(
+            data["sport_scrobble_list"] = completed_for_user.filter(
                 sport_event__isnull=False
-            ).order_by('-timestamp')[:15]
+            ).order_by("-timestamp")[:15]
 
-            data['active_imports'] = AudioScrobblerTSVImport.objects.filter(
+            data["active_imports"] = AudioScrobblerTSVImport.objects.filter(
                 processing_started__isnull=False,
                 processed_finished__isnull=True,
                 user=self.request.user,
             )
 
             limit = 14
-            artist = {'user': user, 'media_type': 'Artist', 'limit': limit}
+            artist = {"user": user, "media_type": "Artist", "limit": limit}
             # This is weird. They don't display properly as QuerySets, so we cast to lists
-            data['current_artist_charts'] = {
+            data["current_artist_charts"] = {
                 "today": list(live_charts(**artist, chart_period="today")),
                 "week": list(live_charts(**artist, chart_period="week")),
                 "month": list(live_charts(**artist, chart_period="month")),
@@ -99,8 +99,8 @@ class RecentScrobbleList(ListView):
                 "all": list(live_charts(**artist)),
             }
 
-            track = {'user': user, 'media_type': 'Track', 'limit': limit}
-            data['current_track_charts'] = {
+            track = {"user": user, "media_type": "Track", "limit": limit}
+            data["current_track_charts"] = {
                 "today": list(live_charts(**track, chart_period="today")),
                 "week": list(live_charts(**track, chart_period="week")),
                 "month": list(live_charts(**track, chart_period="month")),
@@ -109,15 +109,15 @@ class RecentScrobbleList(ListView):
             }
 
         data["weekly_data"] = week_of_scrobbles(user=user)
-        data['counts'] = scrobble_counts(user)
-        data['imdb_form'] = ScrobbleForm
-        data['export_form'] = ExportScrobbleForm
+        data["counts"] = scrobble_counts(user)
+        data["imdb_form"] = ScrobbleForm
+        data["export_form"] = ExportScrobbleForm
         return data
 
     def get_queryset(self):
         return Scrobble.objects.filter(
             track__isnull=False, in_progress=False
-        ).order_by('-timestamp')[:15]
+        ).order_by("-timestamp")[:15]
 
 
 class ScrobbleImportListView(TemplateView):
@@ -125,22 +125,22 @@ class ScrobbleImportListView(TemplateView):
 
     def get_context_data(self, **kwargs):
         context_data = super().get_context_data(**kwargs)
-        context_data['object_list'] = []
+        context_data["object_list"] = []
 
         context_data["tsv_imports"] = AudioScrobblerTSVImport.objects.filter(
             user=self.request.user,
-        ).order_by('-processing_started')
+        ).order_by("-processing_started")
         context_data["koreader_imports"] = KoReaderImport.objects.filter(
             user=self.request.user,
-        ).order_by('-processing_started')
+        ).order_by("-processing_started")
         context_data["lastfm_imports"] = LastFmImport.objects.filter(
             user=self.request.user,
-        ).order_by('-processing_started')
+        ).order_by("-processing_started")
         return context_data
 
 
 class BaseScrobbleImportDetailView(DetailView):
-    slug_field = 'uuid'
+    slug_field = "uuid"
     template_name = "scrobbles/import_detail.html"
 
     def get_queryset(self):
@@ -155,7 +155,7 @@ class BaseScrobbleImportDetailView(DetailView):
             title = "Audioscrobbler TSV Import"
         if self.model == LastFmImport:
             title = "LastFM Import"
-        context_data['title'] = title
+        context_data["title"] = title
         return context_data
 
 
@@ -173,15 +173,15 @@ class ScrobbleLastFMImportDetailView(BaseScrobbleImportDetailView):
 
 class ManualScrobbleView(FormView):
     form_class = ScrobbleForm
-    template_name = 'scrobbles/manual_form.html'
+    template_name = "scrobbles/manual_form.html"
 
     def form_valid(self, form):
 
-        item_id = form.cleaned_data.get('item_id')
+        item_id = form.cleaned_data.get("item_id")
         data_dict = None
-        if 'tt' in item_id:
+        if "tt" in item_id:
             data_dict = lookup_video_from_imdb(
-                form.cleaned_data.get('item_id')
+                form.cleaned_data.get("item_id")
             )
             if data_dict:
                 manual_scrobble_video(data_dict, self.request.user.id)
@@ -189,7 +189,7 @@ class ManualScrobbleView(FormView):
         if not data_dict:
             logger.debug(f"Looking for sport event with ID {item_id}")
             data_dict = lookup_event_from_thesportsdb(
-                form.cleaned_data.get('item_id')
+                form.cleaned_data.get("item_id")
             )
             if data_dict:
                 manual_scrobble_event(data_dict, self.request.user.id)
@@ -205,7 +205,7 @@ class JsonableResponseMixin:
 
     def form_invalid(self, form):
         response = super().form_invalid(form)
-        if self.request.accepts('text/html'):
+        if self.request.accepts("text/html"):
             return response
         else:
             return JsonResponse(form.errors, status=400)
@@ -215,11 +215,11 @@ class JsonableResponseMixin:
         # it might do some processing (in the case of CreateView, it will
         # call form.save() for example).
         response = super().form_valid(form)
-        if self.request.accepts('text/html'):
+        if self.request.accepts("text/html"):
             return response
         else:
             data = {
-                'pk': self.object.pk,
+                "pk": self.object.pk,
             }
             return JsonResponse(data)
 
@@ -228,9 +228,9 @@ class AudioScrobblerImportCreateView(
     LoginRequiredMixin, JsonableResponseMixin, CreateView
 ):
     model = AudioScrobblerTSVImport
-    fields = ['tsv_file']
-    template_name = 'scrobbles/upload_form.html'
-    success_url = reverse_lazy('vrobbler-home')
+    fields = ["tsv_file"]
+    template_name = "scrobbles/upload_form.html"
+    success_url = reverse_lazy("vrobbler-home")
 
     def form_valid(self, form):
         self.object = form.save(commit=False)
@@ -244,9 +244,9 @@ class KoReaderImportCreateView(
     LoginRequiredMixin, JsonableResponseMixin, CreateView
 ):
     model = KoReaderImport
-    fields = ['sqlite_file']
-    template_name = 'scrobbles/upload_form.html'
-    success_url = reverse_lazy('vrobbler-home')
+    fields = ["sqlite_file"]
+    template_name = "scrobbles/upload_form.html"
+    success_url = reverse_lazy("vrobbler-home")
 
     def form_valid(self, form):
         self.object = form.save(commit=False)
@@ -257,7 +257,7 @@ class KoReaderImportCreateView(
 
 
 @permission_classes([IsAuthenticated])
-@api_view(['GET'])
+@api_view(["GET"])
 def lastfm_import(request):
     lfm_import, created = LastFmImport.objects.get_or_create(
         user=request.user, processed_finished__isnull=True
@@ -265,19 +265,19 @@ def lastfm_import(request):
 
     process_lastfm_import.delay(lfm_import.id)
 
-    success_url = reverse_lazy('vrobbler-home')
+    success_url = reverse_lazy("vrobbler-home")
     return HttpResponseRedirect(success_url)
 
 
 @csrf_exempt
 @permission_classes([IsAuthenticated])
-@api_view(['POST'])
+@api_view(["POST"])
 def jellyfin_webhook(request):
     data_dict = request.data
 
     if (
-        data_dict['NotificationType'] == 'PlaybackProgress'
-        and data_dict['ItemType'] == 'Audio'
+        data_dict["NotificationType"] == "PlaybackProgress"
+        and data_dict["ItemType"] == "Audio"
     ):
         return Response({}, status=status.HTTP_304_NOT_MODIFIED)
 
@@ -298,17 +298,17 @@ def jellyfin_webhook(request):
     if not scrobble:
         return Response({}, status=status.HTTP_400_BAD_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
 @permission_classes([IsAuthenticated])
-@api_view(['POST'])
+@api_view(["POST"])
 def mopidy_webhook(request):
     try:
         data_dict = json.loads(request.data)
     except TypeError:
-        logger.warning('Received Mopidy data as dict, rather than a string')
+        logger.warning("Received Mopidy data as dict, rather than a string")
         data_dict = request.data
 
     # For making things easier to build new input processors
@@ -316,7 +316,7 @@ def mopidy_webhook(request):
         json_data = json.dumps(data_dict, indent=4)
         logger.debug(f"{json_data}")
 
-    if 'podcast' in data_dict.get('mopidy_uri'):
+    if "podcast" in data_dict.get("mopidy_uri"):
         scrobble = mopidy_scrobble_podcast(data_dict, request.user.id)
     else:
         scrobble = mopidy_scrobble_track(data_dict, request.user.id)
@@ -324,12 +324,12 @@ def mopidy_webhook(request):
     if not scrobble:
         return Response({}, status=status.HTTP_400_BAD_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
 @permission_classes([IsAuthenticated])
-@api_view(['POST'])
+@api_view(["POST"])
 @parser_classes([MultiPartParser])
 def import_audioscrobbler_file(request):
     """Takes a TSV file in the Audioscrobbler format, saves it and processes the
@@ -344,7 +344,7 @@ def import_audioscrobbler_file(request):
     if file_serializer.is_valid():
         import_file = file_serializer.save()
         return Response(
-            {'scrobble_ids': scrobbles_created}, status=status.HTTP_200_OK
+            {"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK
         )
     else:
         return Response(
@@ -353,10 +353,10 @@ def import_audioscrobbler_file(request):
 
 
 @permission_classes([IsAuthenticated])
-@api_view(['GET'])
+@api_view(["GET"])
 def scrobble_finish(request, uuid):
     user = request.user
-    success_url = reverse_lazy('vrobbler-home')
+    success_url = reverse_lazy("vrobbler-home")
 
     if not user.is_authenticated:
         return HttpResponseRedirect(success_url)
@@ -375,10 +375,10 @@ def scrobble_finish(request, uuid):
 
 
 @permission_classes([IsAuthenticated])
-@api_view(['GET'])
+@api_view(["GET"])
 def scrobble_cancel(request, uuid):
     user = request.user
-    success_url = reverse_lazy('vrobbler-home')
+    success_url = reverse_lazy("vrobbler-home")
 
     if not user.is_authenticated:
         return HttpResponseRedirect(success_url)
@@ -397,11 +397,11 @@ def scrobble_cancel(request, uuid):
 
 
 @permission_classes([IsAuthenticated])
-@api_view(['GET'])
+@api_view(["GET"])
 def export(request):
-    format = request.GET.get('export_type', 'csv')
-    start = request.GET.get('start')
-    end = request.GET.get('end')
+    format = request.GET.get("export_type", "csv")
+    start = request.GET.get("start")
+    end = request.GET.get("end")
     logger.debug(f"Exporting all scrobbles in format {format}")
 
     temp_file, extension = export_scrobbles(
@@ -410,14 +410,14 @@ def export(request):
 
     now = datetime.now()
     filename = f"vrobbler-export-{str(now)}.{extension}"
-    response = FileResponse(open(temp_file, 'rb'))
+    response = FileResponse(open(temp_file, "rb"))
     response["Content-Disposition"] = f'attachment; filename="{filename}"'
 
     return response
 
 
 class ChartRecordView(TemplateView):
-    template_name = 'scrobbles/chart_index.html'
+    template_name = "scrobbles/chart_index.html"
 
     @staticmethod
     def get_media_filter(media_type: str = "") -> Q:
@@ -450,19 +450,19 @@ class ChartRecordView(TemplateView):
     ) -> QuerySet:
         now = timezone.now()
         params = {}
-        params['media_type'] = media
+        params["media_type"] = media
         if period == "today":
-            params['day'] = now.day
-            params['month'] = now.month
-            params['year'] = now.year
+            params["day"] = now.day
+            params["month"] = now.month
+            params["year"] = now.year
         if period == "week":
-            params['week'] = now.ioscalendar()[1]
-            params['year'] = now.year
+            params["week"] = now.ioscalendar()[1]
+            params["year"] = now.year
         if period == "month":
-            params['month'] = now.month
-            params['year'] = now.year
+            params["month"] = now.month
+            params["year"] = now.year
         if period == "year":
-            params['year'] = now.year
+            params["year"] = now.year
         return self.get_chart_records(**params)[:limit]
 
     def get_context_data(self, **kwargs):
@@ -475,8 +475,8 @@ class ChartRecordView(TemplateView):
 
         if not date:
             limit = 20
-            artist_params = {'user': user, 'media_type': 'Artist'}
-            context_data['current_artist_charts'] = {
+            artist_params = {"user": user, "media_type": "Artist"}
+            context_data["current_artist_charts"] = {
                 "today": live_charts(
                     **artist_params, chart_period="today", limit=limit
                 ),
@@ -492,8 +492,8 @@ class ChartRecordView(TemplateView):
                 "all": live_charts(**artist_params, limit=limit),
             }
 
-            track_params = {'user': user, 'media_type': 'Track'}
-            context_data['current_track_charts'] = {
+            track_params = {"user": user, "media_type": "Track"}
+            context_data["current_track_charts"] = {
                 "today": live_charts(
                     **track_params, chart_period="today", limit=limit
                 ),
@@ -513,34 +513,34 @@ class ChartRecordView(TemplateView):
         # Date provided, lookup past charts, returning nothing if it's now or in the future.
         now = timezone.now()
         year = now.year
-        params = {'year': year}
+        params = {"year": year}
         name = f"Chart for {year}"
 
-        date_params = date.split('-')
+        date_params = date.split("-")
         year = int(date_params[0])
         in_progress = False
         if len(date_params) == 2:
-            if 'W' in date_params[1]:
+            if "W" in date_params[1]:
                 week = int(date_params[1].strip('W"'))
-                params['week'] = week
+                params["week"] = week
                 start = datetime.strptime(date + "-1", "%Y-W%W-%w").replace(
                     tzinfo=pytz.utc
                 )
                 end = start + timedelta(days=6)
                 in_progress = start <= now <= end
-                as_str = start.strftime('Week of %B %d, %Y')
+                as_str = start.strftime("Week of %B %d, %Y")
                 name = f"Chart for {as_str}"
             else:
                 month = int(date_params[1])
-                params['month'] = month
+                params["month"] = month
                 month_str = calendar.month_name[month]
                 name = f"Chart for {month_str} {year}"
                 in_progress = now.month == month and now.year == year
         if len(date_params) == 3:
             month = int(date_params[1])
             day = int(date_params[2])
-            params['month'] = month
-            params['day'] = day
+            params["month"] = month
+            params["day"] = day
             month_str = calendar.month_name[month]
             name = f"Chart for {month_str} {day}, {year}"
             in_progress = (
@@ -573,9 +573,9 @@ class ChartRecordView(TemplateView):
                 media_filter, user=self.request.user, **params
             ).order_by("rank")
 
-        context_data['media_type'] = media_type
-        context_data['track_charts'] = track_charts
-        context_data['artist_charts'] = artist_charts
-        context_data['name'] = " ".join(["Top", media_type, "for", name])
-        context_data['in_progress'] = in_progress
+        context_data["media_type"] = media_type
+        context_data["track_charts"] = track_charts
+        context_data["artist_charts"] = artist_charts
+        context_data["name"] = " ".join(["Top", media_type, "for", name])
+        context_data["in_progress"] = in_progress
         return context_data

+ 2 - 2
vrobbler/apps/sports/admin.py

@@ -72,6 +72,6 @@ class SportEventAdmin(admin.ModelAdmin):
 
     def comp_str(self, obj):
         if obj.home_team:
-            return f'{obj.away_team} @ {obj.home_team}'
+            return f"{obj.away_team} @ {obj.home_team}"
         if obj.player_one:
-            return f'{obj.player_one} v {obj.player_two}'
+            return f"{obj.player_one} v {obj.player_two}"

+ 7 - 7
vrobbler/apps/sports/api/views.py

@@ -20,42 +20,42 @@ from sports.models import (
 
 
 class SportEventViewSet(viewsets.ModelViewSet):
-    queryset = SportEvent.objects.all().order_by('-created')
+    queryset = SportEvent.objects.all().order_by("-created")
     serializer_class = SportEventSerializer
     permission_classes = [permissions.IsAuthenticated]
 
 
 class LeagueViewSet(viewsets.ModelViewSet):
-    queryset = League.objects.all().order_by('-created')
+    queryset = League.objects.all().order_by("-created")
     serializer_class = LeagueSerializer
     permission_classes = [permissions.IsAuthenticated]
 
 
 class RoundViewSet(viewsets.ModelViewSet):
-    queryset = Round.objects.all().order_by('-created')
+    queryset = Round.objects.all().order_by("-created")
     serializer_class = RoundSerializer
     permission_classes = [permissions.IsAuthenticated]
 
 
 class SportViewSet(viewsets.ModelViewSet):
-    queryset = Sport.objects.all().order_by('-created')
+    queryset = Sport.objects.all().order_by("-created")
     serializer_class = SportSerializer
     permission_classes = [permissions.IsAuthenticated]
 
 
 class PlayerViewSet(viewsets.ModelViewSet):
-    queryset = Player.objects.all().order_by('-created')
+    queryset = Player.objects.all().order_by("-created")
     serializer_class = PlayerSerializer
     permission_classes = [permissions.IsAuthenticated]
 
 
 class TeamViewSet(viewsets.ModelViewSet):
-    queryset = Team.objects.all().order_by('-created')
+    queryset = Team.objects.all().order_by("-created")
     serializer_class = TeamSerializer
     permission_classes = [permissions.IsAuthenticated]
 
 
 class SeasonViewSet(viewsets.ModelViewSet):
-    queryset = Season.objects.all().order_by('-created')
+    queryset = Season.objects.all().order_by("-created")
     serializer_class = SeasonSerializer
     permission_classes = [permissions.IsAuthenticated]

+ 23 - 23
vrobbler/apps/sports/models.py

@@ -15,10 +15,10 @@ BNULL = {"blank": True, "null": True}
 
 
 class SportEventType(models.TextChoices):
-    UNKNOWN = 'UK', _('Event')
-    GAME = 'GA', _('Game')
-    RACE = 'RA', _('Race')
-    MATCH = 'MA', _('Match')
+    UNKNOWN = "UK", _("Event")
+    GAME = "GA", _("Game")
+    RACE = "RA", _("Race")
+    MATCH = "MA", _("Match")
 
 
 class TheSportsDbMixin(TimeStampedModel):
@@ -47,7 +47,7 @@ class Sport(TheSportsDbMixin):
     @property
     def default_event_run_time_ticks(self):
         default_run_time = getattr(
-            settings, 'DEFAULT_EVENT_RUNTIME_SECONDS', 14400
+            settings, "DEFAULT_EVENT_RUNTIME_SECONDS", 14400
         )
         if self.default_event_run_time:
             default_run_time = self.default_event_run_time
@@ -68,7 +68,7 @@ class Season(TheSportsDbMixin):
     league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
 
     def __str__(self):
-        return f'{self.name} season of {self.league}'
+        return f"{self.name} season of {self.league}"
 
 
 class Team(TheSportsDbMixin):
@@ -84,11 +84,11 @@ class Round(TheSportsDbMixin):
     season = models.ForeignKey(Season, on_delete=models.DO_NOTHING, **BNULL)
 
     def __str__(self):
-        return f'{self.name} of {self.season}'
+        return f"{self.name} of {self.season}"
 
 
 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(
@@ -101,25 +101,25 @@ class SportEvent(ScrobblableMixin):
     home_team = models.ForeignKey(
         Team,
         on_delete=models.DO_NOTHING,
-        related_name='home_event_set',
+        related_name="home_event_set",
         **BNULL,
     )
     away_team = models.ForeignKey(
         Team,
         on_delete=models.DO_NOTHING,
-        related_name='away_event_set',
+        related_name="away_event_set",
         **BNULL,
     )
     player_one = models.ForeignKey(
         Player,
         on_delete=models.DO_NOTHING,
-        related_name='player_one_set',
+        related_name="player_one_set",
         **BNULL,
     )
     player_two = models.ForeignKey(
         Player,
         on_delete=models.DO_NOTHING,
-        related_name='player_two_set',
+        related_name="player_two_set",
         **BNULL,
     )
 
@@ -127,7 +127,7 @@ class SportEvent(ScrobblableMixin):
         return f"{self.start.date()} - {self.round} - {self.home_team} v {self.away_team}"
 
     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):
@@ -153,7 +153,7 @@ class SportEvent(ScrobblableMixin):
         sport, s_created = Sport.objects.get_or_create(thesportsdb_id=sid)
         if s_created:
             sport.name = sid
-            sport.save(update_fields=['name'])
+            sport.save(update_fields=["name"])
 
         # Find or create our League
         lid = data_dict.get("LeagueId")
@@ -163,34 +163,34 @@ class SportEvent(ScrobblableMixin):
         if l_created:
             league.sport = sport
             league.name = data_dict.get("LeagueName", "")
-            league.save(update_fields=['sport', 'name'])
+            league.save(update_fields=["sport", "name"])
 
         # Find or create our Season
-        seid = data_dict.get('Season')
+        seid = data_dict.get("Season")
         season, se_created = Season.objects.get_or_create(
             thesportsdb_id=seid, league=league
         )
         if se_created:
             season.name = seid
-            season.save(update_fields=['name'])
+            season.save(update_fields=["name"])
 
         # Find or create our Round
-        rid = data_dict.get('RoundId')
+        rid = data_dict.get("RoundId")
         round, r_created = Round.objects.get_or_create(
             thesportsdb_id=rid, season=season
         )
         if r_created:
             round.season = season
-            round.save(update_fields=['season'])
+            round.save(update_fields=["season"])
 
         # Set some special data for Tennis
         player_one = None
         player_two = None
-        if data_dict.get('Sport') == 'Tennis':
-            event_name = data_dict.get('Name', '')
+        if data_dict.get("Sport") == "Tennis":
+            event_name = data_dict.get("Name", "")
             if not round.name:
                 round.name = get_round_name_from_event(event_name)
-                round.save(update_fields=['name'])
+                round.save(update_fields=["name"])
 
             players_list = get_players_from_event(event_name)
             player_one = Player.objects.filter(
@@ -229,7 +229,7 @@ class SportEvent(ScrobblableMixin):
             "away_team": away_team,
             "player_one": player_one,
             "player_two": player_two,
-            "start": data_dict['Start'],
+            "start": data_dict["Start"],
             "round": round,
             "run_time_ticks": data_dict.get("RunTimeTicks"),
             "run_time": data_dict.get("RunTime", ""),

+ 16 - 16
vrobbler/apps/sports/thesportsdb.py

@@ -14,36 +14,36 @@ client = TheSportsDbClient(api_key=API_KEY)
 
 def lookup_event_from_thesportsdb(event_id: str) -> dict:
 
-    event = client.lookup_event(event_id)['events'][0]
+    event = client.lookup_event(event_id)["events"][0]
     if not event or type(event) != dict:
         return {}
     league = {}  # client.lookup_league(league_id=event.get('idLeague'))
     event_type = "Game"
     sport, _created = Sport.objects.get_or_create(
-        thesportsdb_id=event.get('strSport')
+        thesportsdb_id=event.get("strSport")
     )
 
     data_dict = {
         "EventId": event_id,
         "ItemType": sport.default_event_type,
-        "Name": event.get('strEvent'),
-        "AltName": event.get('strEventAlternate'),
-        "Start": parse(event.get('strTimestamp')),
-        "Provider_thesportsdb": event.get('idEvent'),
+        "Name": event.get("strEvent"),
+        "AltName": event.get("strEventAlternate"),
+        "Start": parse(event.get("strTimestamp")),
+        "Provider_thesportsdb": event.get("idEvent"),
         "RunTime": sport.default_event_run_time,
         "RunTimeTicks": sport.default_event_run_time_ticks,
-        "Sport": event.get('strSport'),
-        "Season": event.get('strSeason'),
-        "LeagueId": event.get('idLeague'),
-        "LeagueName": event.get('strLeague'),
-        "HomeTeamId": event.get('idHomeTeam'),
-        "HomeTeamName": event.get('strHomeTeam'),
-        "AwayTeamId": event.get('idAwayTeam'),
-        "AwayTeamName": event.get('strAwayTeam'),
-        "RoundId": event.get('intRound'),
+        "Sport": event.get("strSport"),
+        "Season": event.get("strSeason"),
+        "LeagueId": event.get("idLeague"),
+        "LeagueName": event.get("strLeague"),
+        "HomeTeamId": event.get("idHomeTeam"),
+        "HomeTeamName": event.get("strHomeTeam"),
+        "AwayTeamId": event.get("idAwayTeam"),
+        "AwayTeamName": event.get("strAwayTeam"),
+        "RoundId": event.get("intRound"),
         "PlaybackPositionTicks": None,
         "PlaybackPosition": None,
-        "UtcTimestamp": timezone.now().strftime('%Y-%m-%d %H:%M:%S.%f%z'),
+        "UtcTimestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f%z"),
         "IsPaused": False,
         "PlayedToCompletion": False,
         "Source": "Vrobbler",

+ 5 - 5
vrobbler/apps/sports/urls.py

@@ -1,18 +1,18 @@
 from django.urls import path
 from sports import views
 
-app_name = 'sports'
+app_name = "sports"
 
 
 urlpatterns = [
     path(
-        'sport-events/',
+        "sport-events/",
         views.SportEventListView.as_view(),
-        name='event_list',
+        name="event_list",
     ),
     path(
-        'sport-events/<slug:slug>/',
+        "sport-events/<slug:slug>/",
         views.SportEventDetailView.as_view(),
-        name='event_detail',
+        name="event_detail",
     ),
 ]

+ 2 - 2
vrobbler/apps/sports/utils.py

@@ -1,11 +1,11 @@
 def get_round_name_from_event(event: str) -> str:
-    return ' '.join(event.split(' ')[:2])
+    return " ".join(event.split(" ")[:2])
 
 
 def get_players_from_event(event: str) -> list[str]:
     players = []
     event_name = get_round_name_from_event(event)
-    players_list = event.split(event_name)[1:][0].split('vs')
+    players_list = event.split(event_name)[1:][0].split("vs")
     players.append(players_list[0].strip())
     players.append(players_list[1].strip())
     return players

+ 1 - 1
vrobbler/apps/sports/views.py

@@ -9,4 +9,4 @@ class SportEventListView(generic.ListView):
 
 class SportEventDetailView(generic.DetailView):
     model = SportEvent
-    slug_field = 'uuid'
+    slug_field = "uuid"

+ 1 - 1
vrobbler/apps/videos/admin.py

@@ -14,7 +14,7 @@ class SeriesAdmin(admin.ModelAdmin):
 @admin.register(Video)
 class VideoAdmin(admin.ModelAdmin):
     date_hierarchy = "created"
-    raw_id_fields = ('tv_series',)
+    raw_id_fields = ("tv_series",)
     list_display = (
         "title",
         "video_type",

+ 2 - 2
vrobbler/apps/videos/api/views.py

@@ -8,12 +8,12 @@ from videos.models import Series, Video
 
 
 class SeriesViewSet(viewsets.ModelViewSet):
-    queryset = Series.objects.all().order_by('-created')
+    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')
+    queryset = Video.objects.all().order_by("-created")
     serializer_class = VideoSerializer
     permission_classes = [permissions.IsAuthenticated]

+ 1 - 1
vrobbler/apps/videos/apps.py

@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class VideosConfig(AppConfig):
-    name = 'videos'
+    name = "videos"

+ 11 - 11
vrobbler/apps/videos/imdb.py

@@ -10,11 +10,11 @@ logger = logging.getLogger(__name__)
 
 def lookup_video_from_imdb(imdb_id: str) -> dict:
 
-    if 'tt' not in imdb_id:
+    if "tt" not in imdb_id:
         logger.warning(f"IMDB ID should begin with 'tt' {imdb_id}")
         return
 
-    lookup_id = imdb_id.strip('tt')
+    lookup_id = imdb_id.strip("tt")
     media = imdb_client.get_movie(lookup_id)
 
     run_time_seconds = 60 * 60
@@ -26,11 +26,11 @@ def lookup_video_from_imdb(imdb_id: str) -> dict:
     run_time_ticks = run_time_seconds * 1000 * 1000
 
     item_type = "Movie"
-    if media.get('series title'):
+    if media.get("series title"):
         item_type = "Episode"
 
     try:
-        plot = media.get('plot')[0]
+        plot = media.get("plot")[0]
     except TypeError:
         plot = ""
     except IndexError:
@@ -40,19 +40,19 @@ def lookup_video_from_imdb(imdb_id: str) -> dict:
     # Build a rough approximation of a Jellyfin data response
     data_dict = {
         "ItemType": item_type,
-        "Name": media.get('title'),
+        "Name": media.get("title"),
         "Overview": plot,
-        "Tagline": media.get('tagline'),
-        "Year": media.get('year'),
+        "Tagline": media.get("tagline"),
+        "Year": media.get("year"),
         "Provider_imdb": imdb_id,
         "RunTime": run_time_seconds,
         "RunTimeTicks": run_time_ticks,
-        "SeriesName": media.get('series title'),
-        "EpisodeNumber": media.get('episode'),
-        "SeasonNumber": media.get('season'),
+        "SeriesName": media.get("series title"),
+        "EpisodeNumber": media.get("episode"),
+        "SeasonNumber": media.get("season"),
         "PlaybackPositionTicks": 1,
         "PlaybackPosition": 1,
-        "UtcTimestamp": timezone.now().strftime('%Y-%m-%d %H:%M:%S.%f%z'),
+        "UtcTimestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f%z"),
         "IsPaused": False,
         "PlayedToCompletion": False,
     }

+ 8 - 8
vrobbler/apps/videos/models.py

@@ -33,13 +33,13 @@ class Series(TimeStampedModel):
 
 
 class Video(ScrobblableMixin):
-    COMPLETION_PERCENT = getattr(settings, 'VIDEO_COMPLETION_PERCENT', 90)
-    SECONDS_TO_STALE = getattr(settings, 'VIDEO_SECONDS_TO_STALE', 14400)
+    COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
+    SECONDS_TO_STALE = getattr(settings, "VIDEO_SECONDS_TO_STALE", 14400)
 
     class VideoType(models.TextChoices):
-        UNKNOWN = 'U', _('Unknown')
-        TV_EPISODE = 'E', _('TV Episode')
-        MOVIE = 'M', _('Movie')
+        UNKNOWN = "U", _("Unknown")
+        TV_EPISODE = "E", _("TV Episode")
+        MOVIE = "M", _("Movie")
 
     video_type = models.CharField(
         max_length=1,
@@ -59,7 +59,7 @@ class Video(ScrobblableMixin):
     tvrage_id = models.CharField(max_length=20, **BNULL)
 
     class Meta:
-        unique_together = [['title', 'imdb_id']]
+        unique_together = [["title", "imdb_id"]]
 
     def __str__(self):
         if self.video_type == self.VideoType.TV_EPISODE:
@@ -67,7 +67,7 @@ class Video(ScrobblableMixin):
         return self.title
 
     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):
@@ -106,7 +106,7 @@ class Video(ScrobblableMixin):
             series, series_created = Series.objects.get_or_create(
                 name=series_name
             )
-            video_dict['video_type'] = Video.VideoType.TV_EPISODE
+            video_dict["video_type"] = Video.VideoType.TV_EPISODE
 
         video, created = cls.objects.get_or_create(**video_dict)
 

+ 7 - 7
vrobbler/apps/videos/urls.py

@@ -1,21 +1,21 @@
 from django.urls import path
 from videos import views
 
-app_name = 'videos'
+app_name = "videos"
 
 
 urlpatterns = [
     # path('', views.scrobble_endpoint, name='scrobble-list'),
-    path("movies/", views.MovieListView.as_view(), name='movie_list'),
-    path('series/', views.SeriesListView.as_view(), name='series_list'),
+    path("movies/", views.MovieListView.as_view(), name="movie_list"),
+    path("series/", views.SeriesListView.as_view(), name="series_list"),
     path(
-        'series/<slug:slug>/',
+        "series/<slug:slug>/",
         views.SeriesDetailView.as_view(),
-        name='series_detail',
+        name="series_detail",
     ),
     path(
-        'video/<slug:slug>/',
+        "video/<slug:slug>/",
         views.VideoDetailView.as_view(),
-        name='video_detail',
+        name="video_detail",
     ),
 ]

+ 2 - 2
vrobbler/apps/videos/views.py

@@ -18,9 +18,9 @@ class SeriesListView(generic.ListView):
 
 class SeriesDetailView(generic.DetailView):
     model = Series
-    slug_field = 'uuid'
+    slug_field = "uuid"
 
 
 class VideoDetailView(generic.DetailView):
     model = Video
-    slug_field = 'uuid'
+    slug_field = "uuid"

+ 1 - 1
vrobbler/asgi.py

@@ -11,6 +11,6 @@ import os
 
 from django.core.asgi import get_asgi_application
 
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrobbler.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vrobbler.settings")
 
 application = get_asgi_application()

+ 4 - 4
vrobbler/cli.py

@@ -4,10 +4,10 @@ import sys
 from os import environ as env
 
 
-if not 'DJANGO_SETTINGS_MODULE' in env:
+if not "DJANGO_SETTINGS_MODULE" in env:
     from vrobbler import settings
 
-    env.setdefault('DJANGO_SETTINGS_MODULE', settings.__name__)
+    env.setdefault("DJANGO_SETTINGS_MODULE", settings.__name__)
 
 
 import django
@@ -15,7 +15,7 @@ import django
 django.setup()
 
 # this line must be after django.setup() for logging configure
-logger = logging.getLogger('vrobbler')
+logger = logging.getLogger("vrobbler")
 
 
 def main():
@@ -33,5 +33,5 @@ def main():
     execute_from_command_line(sys.argv)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()

+ 20 - 20
vrobbler/settings.py

@@ -9,7 +9,7 @@ from dotenv import load_dotenv
 
 PROJECT_ROOT = Path(__file__).resolve().parent
 BASE_DIR = Path(__file__).resolve().parent.parent
-sys.path.insert(0, os.path.join(PROJECT_ROOT, 'apps'))
+sys.path.insert(0, os.path.join(PROJECT_ROOT, "apps"))
 
 # Tap vrobbler.conf if it's available
 if os.path.exists("vrobbler.conf"):
@@ -57,7 +57,7 @@ TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
 LASTFM_API_KEY = os.getenv("VROBBLER_LASTFM_API_KEY")
 LASTFM_SECRET_KEY = os.getenv("VROBBLER_LASTFM_SECRET_KEY")
 
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 
 TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "US/Eastern")
 
@@ -159,9 +159,9 @@ if TESTING:
     }
 
 db_str = ""
-if 'sqlite' in DATABASES['default']['ENGINE']:
+if "sqlite" in DATABASES["default"]["ENGINE"]:
     db_str = f"Connected to sqlite@{DATABASES['default']['NAME']}"
-if 'postgresql' in DATABASES['default']['ENGINE']:
+if "postgresql" in DATABASES["default"]["ENGINE"]:
     db_str = f"Connected to postgres@{DATABASES['default']['HOST']}/{DATABASES['default']['NAME']}"
 if db_str:
     print(db_str)
@@ -187,11 +187,11 @@ AUTHENTICATION_BACKENDS = [
 # We have to ignore content negotiation because Jellyfin is a bad actor
 REST_FRAMEWORK = {
     "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
-    'DEFAULT_AUTHENTICATION_CLASSES': [
-        'rest_framework.authentication.TokenAuthentication',
-        'rest_framework.authentication.SessionAuthentication',
+    "DEFAULT_AUTHENTICATION_CLASSES": [
+        "rest_framework.authentication.TokenAuthentication",
+        "rest_framework.authentication.SessionAuthentication",
     ],
-    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'vrobbler.negotiation.IgnoreClientContentNegotiation',
+    "DEFAULT_CONTENT_NEGOTIATION_CLASS": "vrobbler.negotiation.IgnoreClientContentNegotiation",
     "DEFAULT_FILTER_BACKENDS": [
         "django_filters.rest_framework.DjangoFilterBackend"
     ],
@@ -289,17 +289,17 @@ LOGGING = {
             "class": "logging.NullHandler",
             "level": LOG_LEVEL,
         },
-        'sql': {
-            'class': 'logging.handlers.RotatingFileHandler',
-            'filename': ''.join([LOG_FILE_PATH, 'vrobbler_sql.', LOG_TYPE]),
-            'formatter': LOG_TYPE,
-            'level': LOG_LEVEL,
+        "sql": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "".join([LOG_FILE_PATH, "vrobbler_sql.", LOG_TYPE]),
+            "formatter": LOG_TYPE,
+            "level": LOG_LEVEL,
         },
-        'file': {
-            'class': 'logging.handlers.RotatingFileHandler',
-            'filename': ''.join([LOG_FILE_PATH, 'vrobbler.', LOG_TYPE]),
-            'formatter': LOG_TYPE,
-            'level': LOG_LEVEL,
+        "file": {
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": "".join([LOG_FILE_PATH, "vrobbler.", LOG_TYPE]),
+            "formatter": LOG_TYPE,
+            "level": LOG_LEVEL,
         },
     },
     "loggers": {
@@ -322,5 +322,5 @@ LOGGING = {
 
 LOG_TO_CONSOLE = os.getenv("VROBBLER_LOG_TO_CONSOLE", False)
 if LOG_TO_CONSOLE:
-    LOGGING['loggers']['django']['handlers'] = ["console"]
-    LOGGING['loggers']['vrobbler']['handlers'] = ["console"]
+    LOGGING["loggers"]["django"]["handlers"] = ["console"]
+    LOGGING["loggers"]["vrobbler"]["handlers"] = ["console"]

+ 21 - 21
vrobbler/urls.py

@@ -31,29 +31,29 @@ 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'leagues', LeagueViewSet)
-router.register(r'sports', SportViewSet)
-router.register(r'seasons', SeasonViewSet)
-router.register(r'players', PlayerViewSet)
-router.register(r'sport-events', SportEventViewSet)
-router.register(r'teams', TeamViewSet)
-router.register(r'users', UserViewSet)
-router.register(r'user_profiles', UserProfileViewSet)
+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"leagues", LeagueViewSet)
+router.register(r"sports", SportViewSet)
+router.register(r"seasons", SeasonViewSet)
+router.register(r"players", PlayerViewSet)
+router.register(r"sport-events", SportEventViewSet)
+router.register(r"teams", TeamViewSet)
+router.register(r"users", UserViewSet)
+router.register(r"user_profiles", UserProfileViewSet)
 
 urlpatterns = [
-    path('api/v1/', include(router.urls)),
-    path('api/v1/auth', include("rest_framework.urls")),
+    path("api/v1/", include(router.urls)),
+    path("api/v1/auth", include("rest_framework.urls")),
     path("admin/", admin.site.urls),
     path("accounts/", include("allauth.urls")),
     path("", include(music_urls, namespace="music")),

+ 1 - 1
vrobbler/wsgi.py

@@ -11,6 +11,6 @@ import os
 
 from django.core.wsgi import get_wsgi_application
 
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vrobbler.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vrobbler.settings")
 
 application = get_wsgi_application()