Blacken quotes
This commit is contained in:
@ -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()
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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",)
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class AlbumAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
filter_horizontal = [
|
filter_horizontal = [
|
||||||
'artists',
|
"artists",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class MusicConfig(AppConfig):
|
class MusicConfig(AppConfig):
|
||||||
name = 'music'
|
name = "music"
|
||||||
|
|||||||
@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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?
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class PodcastsConfig(AppConfig):
|
class PodcastsConfig(AppConfig):
|
||||||
name = 'podcasts'
|
name = "podcasts"
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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",)
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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",)
|
||||||
|
|||||||
@ -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]
|
||||||
|
|
||||||
|
|||||||
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class ScrobblesConfig(AppConfig):
|
class ScrobblesConfig(AppConfig):
|
||||||
name = 'scrobbles'
|
name = "scrobbles"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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"]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 = (
|
||||||
|
|||||||
@ -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()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}"
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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", ""),
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class VideosConfig(AppConfig):
|
class VideosConfig(AppConfig):
|
||||||
name = 'videos'
|
name = "videos"
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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")),
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user