Blacken quotes

This commit is contained in:
2023-03-04 17:29:25 -05:00
parent 3d7528030a
commit 94f1396f2e
60 changed files with 593 additions and 594 deletions

View File

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

View File

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

View File

@ -24,7 +24,7 @@ class MopidyRequest:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.request_data = { self.request_data = {
"name": kwargs.get('name', self.name), "name": kwargs.get("name", self.name),
"artist": kwargs.get("artist", self.artist), "artist": kwargs.get("artist", self.artist),
"album": kwargs.get("album", self.album), "album": kwargs.get("album", self.album),
"track_number": int(kwargs.get("track_number", self.track_number)), "track_number": int(kwargs.get("track_number", self.track_number)),
@ -61,7 +61,7 @@ class MopidyRequest:
@pytest.fixture @pytest.fixture
def valid_auth_token(): 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 return Token.objects.create(user=user).key

View File

@ -11,11 +11,11 @@ from scrobbles.models import Scrobble
def build_scrobbles(client, request_data, num=7, spacing=2): def build_scrobbles(client, request_data, num=7, spacing=2):
url = reverse('scrobbles:mopidy-webhook') url = reverse("scrobbles:mopidy-webhook")
user = get_user_model().objects.create(username='Test User') user = get_user_model().objects.create(username="Test User")
UserProfile.objects.create(user=user, timezone='US/Eastern') UserProfile.objects.create(user=user, timezone="US/Eastern")
for i in range(num): for i in range(num):
client.post(url, request_data, content_type='application/json') client.post(url, request_data, content_type="application/json")
s = Scrobble.objects.last() s = Scrobble.objects.last()
s.user = user s.user = user
s.timestamp = timezone.now() - timedelta(days=i * spacing) 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() user = get_user_model().objects.first()
count_dict = scrobble_counts(user) count_dict = scrobble_counts(user)
assert count_dict == { assert count_dict == {
'alltime': 7, "alltime": 7,
'month': 2, "month": 2,
'today': 1, "today": 1,
'week': 3, "week": 3,
'year': 7, "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): def test_top_tracks_by_week(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1) build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first() 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" 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): def test_top_tracks_by_month(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1) build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first() 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" 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): def test_top_tracks_by_year(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1) build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first() 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" 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): def test_top__artists_by_week(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1) build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first() 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" 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): def test_top__artists_by_month(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1) build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first() 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" 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): def test_top__artists_by_year(client, mopidy_track_request_data):
build_scrobbles(client, mopidy_track_request_data, 7, 1) build_scrobbles(client, mopidy_track_request_data, 7, 1)
user = get_user_model().objects.first() 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" assert tops[0].name == "Sublime"

View File

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

View File

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

View File

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

View File

@ -8,12 +8,12 @@ from books.models import Author, Book
class AuthorViewSet(viewsets.ModelViewSet): class AuthorViewSet(viewsets.ModelViewSet):
queryset = Author.objects.all().order_by('-created') queryset = Author.objects.all().order_by("-created")
serializer_class = AuthorSerializer serializer_class = AuthorSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class BookViewSet(viewsets.ModelViewSet): class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all().order_by('-created') queryset = Book.objects.all().order_by("-created")
serializer_class = BookSerializer serializer_class = BookSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,18 +9,18 @@ from music.models import Artist, Album, Track
class ArtistViewSet(viewsets.ModelViewSet): class ArtistViewSet(viewsets.ModelViewSet):
queryset = Artist.objects.all().order_by('-created') queryset = Artist.objects.all().order_by("-created")
serializer_class = ArtistSerializer serializer_class = ArtistSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class AlbumViewSet(viewsets.ModelViewSet): class AlbumViewSet(viewsets.ModelViewSet):
queryset = Album.objects.all().order_by('-created') queryset = Album.objects.all().order_by("-created")
serializer_class = AlbumSerializer serializer_class = AlbumSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class TrackViewSet(viewsets.ModelViewSet): class TrackViewSet(viewsets.ModelViewSet):
queryset = Track.objects.all().order_by('-created') queryset = Track.objects.all().order_by("-created")
serializer_class = TrackSerializer serializer_class = TrackSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,26 @@
from django.urls import path from django.urls import path
from music import views from music import views
app_name = 'music' app_name = "music"
urlpatterns = [ urlpatterns = [
path('albums/', views.AlbumListView.as_view(), name='albums_list'), path("albums/", views.AlbumListView.as_view(), name="albums_list"),
path( path(
'album/<slug:slug>/', "album/<slug:slug>/",
views.AlbumDetailView.as_view(), 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( path(
'tracks/<slug:slug>/', "tracks/<slug:slug>/",
views.TrackDetailView.as_view(), 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( path(
'artists/<slug:slug>/', "artists/<slug:slug>/",
views.ArtistDetailView.as_view(), views.ArtistDetailView.as_view(),
name='artist_detail', name="artist_detail",
), ),
] ]

View File

@ -17,19 +17,19 @@ from music.models import Album, Artist, Track
def get_or_create_artist(name: str, mbid: str = None) -> Artist: def get_or_create_artist(name: str, mbid: str = None) -> Artist:
artist = None 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() 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() 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() name = re.split("&", name, flags=re.IGNORECASE)[0].strip()
artist_dict = lookup_artist_from_mb(name) 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() artist = Artist.objects.filter(musicbrainz_id=mbid).first()
if not artist: if not artist:
artist = Artist.objects.create(name=name, musicbrainz_id=mbid) 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: def get_or_create_album(name: str, artist: Artist, mbid: str = None) -> Album:
album = None album = None
album_dict = lookup_album_dict_from_mb(name, artist_name=artist.name) 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() album = Album.objects.filter(musicbrainz_id=mbid).first()
if not album: if not album:
@ -81,7 +81,7 @@ def get_or_create_track(
title, title,
artist.musicbrainz_id, artist.musicbrainz_id,
album.musicbrainz_id, album.musicbrainz_id,
)['id'] )["id"]
track = Track.objects.filter(musicbrainz_id=mbid).first() track = Track.objects.filter(musicbrainz_id=mbid).first()

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ class UserViewSet(viewsets.ModelViewSet):
API endpoint that allows users to be viewed or edited. 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 serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
@ -23,6 +23,6 @@ class UserProfileViewSet(viewsets.ModelViewSet):
API endpoint that allows users to be viewed or edited. 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 serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,36 +14,36 @@ client = TheSportsDbClient(api_key=API_KEY)
def lookup_event_from_thesportsdb(event_id: str) -> dict: 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: if not event or type(event) != dict:
return {} return {}
league = {} # client.lookup_league(league_id=event.get('idLeague')) league = {} # client.lookup_league(league_id=event.get('idLeague'))
event_type = "Game" event_type = "Game"
sport, _created = Sport.objects.get_or_create( sport, _created = Sport.objects.get_or_create(
thesportsdb_id=event.get('strSport') thesportsdb_id=event.get("strSport")
) )
data_dict = { data_dict = {
"EventId": event_id, "EventId": event_id,
"ItemType": sport.default_event_type, "ItemType": sport.default_event_type,
"Name": event.get('strEvent'), "Name": event.get("strEvent"),
"AltName": event.get('strEventAlternate'), "AltName": event.get("strEventAlternate"),
"Start": parse(event.get('strTimestamp')), "Start": parse(event.get("strTimestamp")),
"Provider_thesportsdb": event.get('idEvent'), "Provider_thesportsdb": event.get("idEvent"),
"RunTime": sport.default_event_run_time, "RunTime": sport.default_event_run_time,
"RunTimeTicks": sport.default_event_run_time_ticks, "RunTimeTicks": sport.default_event_run_time_ticks,
"Sport": event.get('strSport'), "Sport": event.get("strSport"),
"Season": event.get('strSeason'), "Season": event.get("strSeason"),
"LeagueId": event.get('idLeague'), "LeagueId": event.get("idLeague"),
"LeagueName": event.get('strLeague'), "LeagueName": event.get("strLeague"),
"HomeTeamId": event.get('idHomeTeam'), "HomeTeamId": event.get("idHomeTeam"),
"HomeTeamName": event.get('strHomeTeam'), "HomeTeamName": event.get("strHomeTeam"),
"AwayTeamId": event.get('idAwayTeam'), "AwayTeamId": event.get("idAwayTeam"),
"AwayTeamName": event.get('strAwayTeam'), "AwayTeamName": event.get("strAwayTeam"),
"RoundId": event.get('intRound'), "RoundId": event.get("intRound"),
"PlaybackPositionTicks": None, "PlaybackPositionTicks": None,
"PlaybackPosition": 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, "IsPaused": False,
"PlayedToCompletion": False, "PlayedToCompletion": False,
"Source": "Vrobbler", "Source": "Vrobbler",

View File

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

View File

@ -1,11 +1,11 @@
def get_round_name_from_event(event: str) -> str: 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]: def get_players_from_event(event: str) -> list[str]:
players = [] players = []
event_name = get_round_name_from_event(event) 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[0].strip())
players.append(players_list[1].strip()) players.append(players_list[1].strip())
return players return players

View File

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

View File

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

View File

@ -8,12 +8,12 @@ from videos.models import Series, Video
class SeriesViewSet(viewsets.ModelViewSet): class SeriesViewSet(viewsets.ModelViewSet):
queryset = Series.objects.all().order_by('-created') queryset = Series.objects.all().order_by("-created")
serializer_class = SeriesSerializer serializer_class = SeriesSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
class VideoViewSet(viewsets.ModelViewSet): class VideoViewSet(viewsets.ModelViewSet):
queryset = Video.objects.all().order_by('-created') queryset = Video.objects.all().order_by("-created")
serializer_class = VideoSerializer serializer_class = VideoSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application 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() application = get_asgi_application()

View File

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

View File

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

View File

@ -31,29 +31,29 @@ from vrobbler.apps.videos import urls as video_urls
from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'scrobbles', ScrobbleViewSet) router.register(r"scrobbles", ScrobbleViewSet)
router.register(r'lastfm-imports', LastFmImportViewSet) router.register(r"lastfm-imports", LastFmImportViewSet)
router.register(r'tsv-imports', AudioScrobblerTSVImportViewSet) router.register(r"tsv-imports", AudioScrobblerTSVImportViewSet)
router.register(r'koreader-imports', KoReaderImportViewSet) router.register(r"koreader-imports", KoReaderImportViewSet)
router.register(r'artist', ArtistViewSet) router.register(r"artist", ArtistViewSet)
router.register(r'album', AlbumViewSet) router.register(r"album", AlbumViewSet)
router.register(r'tracks', TrackViewSet) router.register(r"tracks", TrackViewSet)
router.register(r'series', SeriesViewSet) router.register(r"series", SeriesViewSet)
router.register(r'videos', VideoViewSet) router.register(r"videos", VideoViewSet)
router.register(r'authors', AuthorViewSet) router.register(r"authors", AuthorViewSet)
router.register(r'books', BookViewSet) router.register(r"books", BookViewSet)
router.register(r'leagues', LeagueViewSet) router.register(r"leagues", LeagueViewSet)
router.register(r'sports', SportViewSet) router.register(r"sports", SportViewSet)
router.register(r'seasons', SeasonViewSet) router.register(r"seasons", SeasonViewSet)
router.register(r'players', PlayerViewSet) router.register(r"players", PlayerViewSet)
router.register(r'sport-events', SportEventViewSet) router.register(r"sport-events", SportEventViewSet)
router.register(r'teams', TeamViewSet) router.register(r"teams", TeamViewSet)
router.register(r'users', UserViewSet) router.register(r"users", UserViewSet)
router.register(r'user_profiles', UserProfileViewSet) router.register(r"user_profiles", UserProfileViewSet)
urlpatterns = [ urlpatterns = [
path('api/v1/', include(router.urls)), path("api/v1/", include(router.urls)),
path('api/v1/auth', include("rest_framework.urls")), path("api/v1/auth", include("rest_framework.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("", include(music_urls, namespace="music")), path("", include(music_urls, namespace="music")),

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application 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() application = get_wsgi_application()