from datetime import datetime, timedelta from unittest.mock import patch, MagicMock from django.utils import timezone import pytest import time_machine from django.urls import reverse from music.models import Track, Artist, Album from podcasts.models import PodcastEpisode from scrobbles.models import Scrobble @pytest.mark.django_db def test_get_not_allowed_from_mopidy(client, valid_auth_token): url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.get(url, headers=headers) assert response.status_code == 405 @pytest.mark.django_db def test_get_not_allowed_from_jellyfin(client, valid_auth_token): url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.get(url, headers=headers) assert response.status_code == 405 @pytest.mark.django_db def test_bad_mopidy_request_data(client, valid_auth_token): url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post(url, headers) assert response.status_code == 400 assert ( response.data["detail"] == "JSON parse error - Expecting value: line 1 column 1 (char 0)" ) @pytest.mark.django_db def test_bad_jellyfin_request_data(client, valid_auth_token): url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post(url, headers) assert response.status_code == 400 assert ( response.data["detail"] == "JSON parse error - Expecting value: line 1 column 1 (char 0)" ) @pytest.mark.django_db @patch("music.models.Artist.find_or_create") @patch("music.models.Track.find_or_create") def test_create_scrobble_from_mopidy_track_webhook( mock_track_fc, mock_artist_fc, client, valid_auth_token, mopidy_track, ): mock_artist = MagicMock(spec=Artist) mock_artist.id = 1 mock_artist_fc.return_value = mock_artist mock_track = MagicMock(spec=Track) mock_track.id = 1 mock_track.scrobble_for_user.return_value = Scrobble( id=1, track_id=1, user_id=1, in_progress=True ) mock_track_fc.return_value = mock_track url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post( url, mopidy_track.request_data, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} mock_track.scrobble_for_user.assert_called_once() @pytest.mark.django_db @patch("music.models.Artist.find_or_create") @patch("music.models.Track.find_or_create") def test_create_scrobble_from_jellyfin_track_webhook( mock_track_fc, mock_artist_fc, client, valid_auth_token, jellyfin_track, ): mock_artist = MagicMock(spec=Artist) mock_artist.id = 1 mock_artist_fc.return_value = mock_artist mock_track = MagicMock(spec=Track) mock_track.id = 1 mock_track.scrobble_for_user.return_value = Scrobble( id=1, track_id=1, user_id=1, in_progress=True ) mock_track_fc.return_value = mock_track url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)): jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) response = client.post( url, jellyfin_track.request_json, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} mock_track.scrobble_for_user.assert_called_once() @pytest.mark.django_db @patch("music.models.Artist.find_or_create") @patch("music.models.Track.find_or_create") def test_mopidy_track_webhook_creates_track_and_scrobble( mock_track_fc, mock_artist_fc, client, valid_auth_token, mopidy_track, ): artist = Artist.objects.create(name="Sublime") album = Album.objects.create(name="Sublime", album_artist=artist) track = Track.objects.create( title="Same in the End", artist=artist, base_run_time_seconds=60, ) track.albums.add(album) mock_artist_fc.return_value = artist mock_track_fc.return_value = track url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post( url, mopidy_track.request_data, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.get(id=1) assert scrobble.track == track assert scrobble.source == "Mopidy" @pytest.mark.django_db @patch("music.models.Artist.find_or_create") @patch("music.models.Track.find_or_create") def test_jellyfin_track_webhook_creates_track_and_scrobble( mock_track_fc, mock_artist_fc, client, valid_auth_token, jellyfin_track, ): artist = Artist.objects.create(name="Carly Rae Jepsen") album = Album.objects.create(name="Emotion", album_artist=artist) track = Track.objects.create( title="Emotion", artist=artist, base_run_time_seconds=60, ) track.albums.add(album) mock_artist_fc.return_value = artist mock_track_fc.return_value = track url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)): jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) response = client.post( url, jellyfin_track.request_json, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.get(id=1) assert scrobble.track == track assert scrobble.source == "Jellyfin" assert "raw_data" in scrobble.log @pytest.mark.django_db @patch("music.models.Artist.find_or_create") @patch("music.models.Track.find_or_create") def test_mopidy_track_webhook_stores_raw_data( mock_track_fc, mock_artist_fc, client, valid_auth_token, mopidy_track, ): artist = Artist.objects.create(name="Sublime") album = Album.objects.create(name="Sublime", album_artist=artist) track = Track.objects.create( title="Same in the End", artist=artist, base_run_time_seconds=60, ) track.albums.add(album) mock_artist_fc.return_value = artist mock_track_fc.return_value = track url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post( url, mopidy_track.request_data, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.get(id=1) assert scrobble.track == track assert scrobble.source == "Mopidy" assert "raw_data" in scrobble.log assert scrobble.log["raw_data"]["name"] == "Same in the End" @pytest.mark.django_db @patch("music.models.Artist.find_or_create") @patch("music.models.Track.find_or_create") def test_jellyfin_track_webhook_stores_raw_data( mock_track_fc, mock_artist_fc, client, valid_auth_token, jellyfin_track, ): artist = Artist.objects.create(name="Carly Rae Jepsen") album = Album.objects.create(name="Emotion", album_artist=artist) track = Track.objects.create( title="Emotion", artist=artist, base_run_time_seconds=60, ) track.albums.add(album) mock_artist_fc.return_value = artist mock_track_fc.return_value = track url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)): jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) response = client.post( url, jellyfin_track.request_json, content_type="application/json", headers=headers, ) assert response.status_code == 200 scrobble = Scrobble.objects.get(id=1) assert scrobble.track == track assert scrobble.source == "Jellyfin" assert "raw_data" in scrobble.log assert scrobble.log["raw_data"]["Name"] == "Emotion" @pytest.mark.skip("Need to refactor") @pytest.mark.django_db @patch("music.utils.lookup_artist_from_mb", return_value={}) @patch( "music.utils.lookup_album_dict_from_mb", return_value={"year": "1999", "mb_group_id": 1}, ) @patch("music.utils.lookup_track_from_mb", return_value={}) @patch("music.models.lookup_artist_from_tadb", return_value={}) @patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) @patch("music.models.Album.fetch_artwork", return_value=None) @patch("music.models.Album.scrape_allmusic", return_value=None) def test_scrobble_mopidy_same_track_different_album( mock_lookup_artist, mock_lookup_album, mock_lookup_track, mock_lookup_artist_tadb, mock_lookup_album_tadb, mock_fetch_artwork, mock_scrape_allmusic, client, mopidy_track, mopidy_track_diff_album_request_data, valid_auth_token, ): url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post( url, mopidy_track.request_data, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.last() assert scrobble.media_obj.album.name == "Sublime" response = client.post( url, mopidy_track_diff_album_request_data, content_type="application/json", ) assert response.status_code == 200 assert response.data == {"scrobble_id": 2} scrobble = Scrobble.objects.last() assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.album.name == "Sublime" assert scrobble.media_obj.title == "Same in the End" @pytest.mark.django_db @patch( "podcasts.sources.podcastindex.lookup_podcast_from_podcastindex", return_value={}, ) def test_scrobble_mopidy_podcast( mock_lookup_podcast, client, mopidy_podcast_request_data, valid_auth_token ): url = reverse("scrobbles:mopidy-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} response = client.post( url, mopidy_podcast_request_data, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.get(id=1) assert scrobble.media_obj.__class__ == PodcastEpisode assert scrobble.media_obj.title == "Up First" @pytest.mark.skip("Need to refactor") @pytest.mark.django_db @patch("music.utils.lookup_artist_from_mb", return_value={}) @patch( "music.utils.lookup_album_dict_from_mb", return_value={"year": "1999", "mb_group_id": 1}, ) @patch("music.utils.lookup_track_from_mb", return_value={}) @patch("music.models.lookup_artist_from_tadb", return_value={}) @patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) @patch("music.models.Album.fetch_artwork", return_value=None) @patch("music.models.Album.scrape_allmusic", return_value=None) def test_scrobble_jellyfin_track( mock_lookup_artist, mock_lookup_album, mock_lookup_track, mock_lookup_artist_tadb, mock_lookup_album_tadb, mock_fetch_artwork, mock_scrape_allmusic, client, jellyfin_track, valid_auth_token, ): url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} with time_machine.travel(datetime(2024, 1, 14, 12, 00, 1)): jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) response = client.post( url, jellyfin_track.request_json, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.get(id=1) assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion" @pytest.mark.skip("Need to refactor") @pytest.mark.django_db @patch("music.utils.lookup_artist_from_mb", return_value={}) @patch( "music.utils.lookup_album_dict_from_mb", return_value={"year": "1999", "mb_group_id": 1}, ) @patch("music.utils.lookup_track_from_mb", return_value={}) @patch("music.models.lookup_artist_from_tadb", return_value={}) @patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) @patch("music.models.Album.fetch_artwork", return_value=None) @patch("music.models.Album.scrape_allmusic", return_value=None) def test_scrobble_jellyfin_track_update( mock_lookup_artist, mock_lookup_album, mock_lookup_track, mock_lookup_artist_tadb, mock_lookup_album_tadb, mock_fetch_artwork, mock_scrape_allmusic, test_track, client, jellyfin_track, valid_auth_token, ): Scrobble.objects.create( timestamp=timezone.now() - timedelta(minutes=0.5), track=Track.objects.first(), user_id=1, ) url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) response = client.post( url, jellyfin_track.request_json, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 1} scrobble = Scrobble.objects.get(id=1) assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion" @pytest.mark.skip("Need to refactor") @pytest.mark.django_db @patch("music.utils.lookup_artist_from_mb", return_value={}) @patch( "music.utils.lookup_album_dict_from_mb", return_value={"year": "1999", "mb_group_id": 1}, ) @patch("music.utils.lookup_track_from_mb", return_value={}) @patch("music.models.lookup_artist_from_tadb", return_value={}) @patch("music.models.lookup_album_from_tadb", return_value={"year": "1999"}) @patch("music.models.Album.fetch_artwork", return_value=None) @patch("music.models.Album.scrape_allmusic", return_value=None) def test_scrobble_jellyfin_track_create_new( mock_lookup_artist, mock_lookup_album, mock_lookup_track, mock_lookup_artist_tadb, mock_lookup_album_tadb, mock_fetch_artwork, mock_scrape_allmusic, test_track, client, jellyfin_track, valid_auth_token, ): url = reverse("scrobbles:jellyfin-webhook") headers = {"Authorization": f"Token {valid_auth_token}"} Scrobble.objects.create( timestamp=timezone.now() - timedelta(minutes=1), track=Track.objects.first(), user_id=1, ) jellyfin_track.request_data["UtcTimestamp"] = timezone.now().strftime( "%Y-%m-%d %H:%M:%S" ) response = client.post( url, jellyfin_track.request_json, content_type="application/json", headers=headers, ) assert response.status_code == 200 assert response.data == {"scrobble_id": 2} scrobble = Scrobble.objects.get(id=1) assert scrobble.media_obj.__class__ == Track assert scrobble.media_obj.title == "Emotion"