[black] Reformat to use 88 line lengths

This commit is contained in:
2026-03-23 16:17:15 -04:00
parent ab4b5470b7
commit d576467db8
83 changed files with 310 additions and 786 deletions

View File

@ -88,7 +88,7 @@ testpaths = ["tests"]
DJANGO_SETTINGS_MODULE='vrobbler.settings-testing' DJANGO_SETTINGS_MODULE='vrobbler.settings-testing'
[tool.black] [tool.black]
line-length = 79 line-length = 88
target-version = ["py39", "py310"] target-version = ["py39", "py310"]
include = ".py$" include = ".py$"
exclude = "migrations" exclude = "migrations"

View File

@ -93,41 +93,29 @@ class TestRecipeScraperService:
assert result is True assert result is True
def test_scrape_returns_title(self, scraper): def test_scrape_returns_title(self, scraper):
result = scraper.scrape( result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
)
assert result["title"] == "Test Recipe" assert result["title"] == "Test Recipe"
def test_scrape_returns_ingredients(self, scraper): def test_scrape_returns_ingredients(self, scraper):
result = scraper.scrape( result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
)
assert len(result["ingredients"]) == 3 assert len(result["ingredients"]) == 3
assert "1 cup flour" in result["ingredients"] assert "1 cup flour" in result["ingredients"]
def test_scrape_returns_instructions(self, scraper): def test_scrape_returns_instructions(self, scraper):
result = scraper.scrape( result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
)
assert len(result["instructions"]) > 0 assert len(result["instructions"]) > 0
assert "Mix ingredients together" in result["instructions"] assert "Mix ingredients together" in result["instructions"]
def test_scrape_returns_yields(self, scraper): def test_scrape_returns_yields(self, scraper):
result = scraper.scrape( result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
)
assert result["yields"] == "4 servings" assert result["yields"] == "4 servings"
def test_scrape_returns_total_time(self, scraper): def test_scrape_returns_total_time(self, scraper):
result = scraper.scrape( result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
)
assert result["total_time"] == 30 assert result["total_time"] == 30
def test_scrape_returns_url(self, scraper): def test_scrape_returns_url(self, scraper):
result = scraper.scrape( result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
)
assert result["url"] == "https://example.com/recipe" assert result["url"] == "https://example.com/recipe"
def test_scrape_raises_on_invalid_html(self, scraper): def test_scrape_raises_on_invalid_html(self, scraper):

View File

@ -9,9 +9,7 @@ from foods.sources.usda import (
class TestUSDAFoodAPI: class TestUSDAFoodAPI:
@pytest.fixture @pytest.fixture
def usda_api(self): def usda_api(self):
with patch( with patch("vrobbler.apps.foods.sources.usda.settings") as mock_settings:
"vrobbler.apps.foods.sources.usda.settings"
) as mock_settings:
mock_settings.USDA_API_KEY = "test_api_key" mock_settings.USDA_API_KEY = "test_api_key"
return USDAFoodAPI(api_key="test_api_key") return USDAFoodAPI(api_key="test_api_key")

View File

@ -1,9 +1,7 @@
import pytest import pytest
from vrobbler.apps.podcasts.scrapers import scrape_data_from_google_podcasts from vrobbler.apps.podcasts.scrapers import scrape_data_from_google_podcasts
expected_desc_snippet = ( expected_desc_snippet = "NPR's Up First is the news you need to start your day. "
"NPR's Up First is the news you need to start your day. "
)
expected_img_url = "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcR1F0CfR24RR6sme531yIkCrnK4zzmo97jeualO5drVPKG6oCk" expected_img_url = "https://encrypted-tbn2.gstatic.com/images?q=tbn:ANd9GcR1F0CfR24RR6sme531yIkCrnK4zzmo97jeualO5drVPKG6oCk"
expected_google_url = "https://podcasts.google.com/feed/aHR0cHM6Ly9mZWVkcy5ucHIub3JnLzUxMDMxOC9wb2RjYXN0LnhtbA" expected_google_url = "https://podcasts.google.com/feed/aHR0cHM6Ly9mZWVkcy5ucHIub3JnLzUxMDMxOC9wb2RjYXN0LnhtbA"

View File

@ -68,9 +68,7 @@ class MopidyRequest:
"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)),
"run_time_ticks": int( "run_time_ticks": int(kwargs.get("run_time_ticks", self.run_time_ticks)),
kwargs.get("run_time_ticks", self.run_time_ticks)
),
"run_time": int(kwargs.get("run_time", self.run_time)), "run_time": int(kwargs.get("run_time", self.run_time)),
"playback_time_ticks": int( "playback_time_ticks": int(
kwargs.get("playback_time_ticks", self.playback_time_ticks) kwargs.get("playback_time_ticks", self.playback_time_ticks)
@ -113,9 +111,7 @@ def mopidy_track():
@pytest.fixture @pytest.fixture
def mopidy_track_diff_album_request_data(**kwargs): def mopidy_track_diff_album_request_data(**kwargs):
mb_album_id = "0c56c457-afe1-4679-baab-759ba8dd2a58" mb_album_id = "0c56c457-afe1-4679-baab-759ba8dd2a58"
return MopidyRequest( return MopidyRequest(album="Gold", musicbrainz_album_id=mb_album_id).request_json
album="Gold", musicbrainz_album_id=mb_album_id
).request_json
@pytest.fixture @pytest.fixture

View File

@ -6,7 +6,5 @@ from vrobbler.apps.scrobbles.utils import timestamp_user_tz_to_utc
def test_timestamp_user_tz_to_utc(): def test_timestamp_user_tz_to_utc():
timestamp = timestamp_user_tz_to_utc( timestamp = timestamp_user_tz_to_utc(1685561082, pytz.timezone("US/Eastern"))
1685561082, pytz.timezone("US/Eastern")
)
assert timestamp == datetime(2023, 5, 31, 23, 24, 42, tzinfo=pytz.utc) assert timestamp == datetime(2023, 5, 31, 23, 24, 42, tzinfo=pytz.utc)

View File

@ -498,9 +498,7 @@ def test_scrobble_detail_view_with_notes_as_flat_list(client):
user = get_user_model().objects.create_user( user = get_user_model().objects.create_user(
username="testuser", email="test@example.com", password="testpass" username="testuser", email="test@example.com", password="testpass"
) )
task = Task.objects.create( task = Task.objects.create(title="Test Task", description="Test description")
title="Test Task", description="Test description"
)
scrobble = Scrobble.objects.create( scrobble = Scrobble.objects.create(
task=task, task=task,
media_type="Task", media_type="Task",
@ -522,9 +520,7 @@ def test_scrobble_detail_view_with_notes_as_dict_timestamps(client):
user = get_user_model().objects.create_user( user = get_user_model().objects.create_user(
username="testuser", email="test@example.com", password="testpass" username="testuser", email="test@example.com", password="testpass"
) )
task = Task.objects.create( task = Task.objects.create(title="Test Task", description="Test description")
title="Test Task", description="Test description"
)
scrobble = Scrobble.objects.create( scrobble = Scrobble.objects.create(
task=task, task=task,
media_type="Task", media_type="Task",
@ -552,9 +548,7 @@ def test_scrobble_detail_view_with_notes_and_labels(client):
user = get_user_model().objects.create_user( user = get_user_model().objects.create_user(
username="testuser", email="test@example.com", password="testpass" username="testuser", email="test@example.com", password="testpass"
) )
task = Task.objects.create( task = Task.objects.create(title="Test Task", description="Test description")
title="Test Task", description="Test description"
)
scrobble = Scrobble.objects.create( scrobble = Scrobble.objects.create(
task=task, task=task,
media_type="Task", media_type="Task",
@ -580,9 +574,7 @@ def test_scrobble_detail_view_post_updates_log(client):
user = get_user_model().objects.create_user( user = get_user_model().objects.create_user(
username="testuser", email="test@example.com", password="testpass" username="testuser", email="test@example.com", password="testpass"
) )
task = Task.objects.create( task = Task.objects.create(title="Test Task", description="Test description")
title="Test Task", description="Test description"
)
scrobble = Scrobble.objects.create( scrobble = Scrobble.objects.create(
task=task, task=task,
media_type="Task", media_type="Task",

View File

@ -14,9 +14,7 @@ def mock_request():
class TestVersionInfo: class TestVersionInfo:
def test_returns_version_and_commit(self, mock_request): def test_returns_version_and_commit(self, mock_request):
with ( with (
patch( patch("vrobbler.context_processors.get_version") as mock_get_version,
"vrobbler.context_processors.get_version"
) as mock_get_version,
patch( patch(
"vrobbler.context_processors.subprocess.check_output" "vrobbler.context_processors.subprocess.check_output"
) as mock_check_output, ) as mock_check_output,
@ -32,9 +30,7 @@ class TestVersionInfo:
def test_uses_env_commit_if_set(self, mock_request): def test_uses_env_commit_if_set(self, mock_request):
with ( with (
patch.dict(os.environ, {"VROBBLER_COMMIT": "env_commit_hash"}), patch.dict(os.environ, {"VROBBLER_COMMIT": "env_commit_hash"}),
patch( patch("vrobbler.context_processors.get_version") as mock_get_version,
"vrobbler.context_processors.get_version"
) as mock_get_version,
): ):
mock_get_version.return_value = "1.0.0" mock_get_version.return_value = "1.0.0"
@ -44,9 +40,7 @@ class TestVersionInfo:
def test_returns_unknown_when_version_fails(self, mock_request): def test_returns_unknown_when_version_fails(self, mock_request):
with ( with (
patch( patch("vrobbler.context_processors.get_version") as mock_get_version,
"vrobbler.context_processors.get_version"
) as mock_get_version,
patch( patch(
"vrobbler.context_processors.subprocess.check_output" "vrobbler.context_processors.subprocess.check_output"
) as mock_check_output, ) as mock_check_output,
@ -62,9 +56,7 @@ class TestVersionInfo:
import subprocess import subprocess
with ( with (
patch( patch("vrobbler.context_processors.get_version") as mock_get_version,
"vrobbler.context_processors.get_version"
) as mock_get_version,
patch( patch(
"vrobbler.context_processors.subprocess.check_output" "vrobbler.context_processors.subprocess.check_output"
) as mock_check_output, ) as mock_check_output,
@ -80,9 +72,7 @@ class TestVersionInfo:
import subprocess import subprocess
with ( with (
patch( patch("vrobbler.context_processors.get_version") as mock_get_version,
"vrobbler.context_processors.get_version"
) as mock_get_version,
patch( patch(
"vrobbler.context_processors.subprocess.check_output" "vrobbler.context_processors.subprocess.check_output"
) as mock_check_output, ) as mock_check_output,

View File

@ -48,12 +48,8 @@ class TestVideoAPI:
assert response.status_code == 200 assert response.status_code == 200
assert response.data["title"] == "Test Video" assert response.data["title"] == "Test Video"
def test_filter_videos_by_channel( def test_filter_videos_by_channel(self, client, auth_headers, channel, video):
self, client, auth_headers, channel, video response = client.get(f"/api/v1/videos/?channel={channel.id}", **auth_headers)
):
response = client.get(
f"/api/v1/videos/?channel={channel.id}", **auth_headers
)
assert response.status_code == 200 assert response.status_code == 200
assert len(response.data["results"]) == 1 assert len(response.data["results"]) == 1
assert response.data["results"][0]["channel"] == channel.id assert response.data["results"][0]["channel"] == channel.id
@ -68,9 +64,7 @@ class TestChannelAPI:
assert response.data["results"][0]["name"] == "Test Channel" assert response.data["results"][0]["name"] == "Test Channel"
def test_get_channel(self, client, auth_headers, channel): def test_get_channel(self, client, auth_headers, channel):
response = client.get( response = client.get(f"/api/v1/channels/{channel.id}/", **auth_headers)
f"/api/v1/channels/{channel.id}/", **auth_headers
)
assert response.status_code == 200 assert response.status_code == 200
assert response.data["name"] == "Test Channel" assert response.data["name"] == "Test Channel"

View File

@ -67,9 +67,7 @@ class Beer(ScrobblableMixin):
) )
untappd_id = models.CharField(max_length=255, **BNULL) untappd_id = models.CharField(max_length=255, **BNULL)
untappd_rating = models.FloatField(**BNULL) untappd_rating = models.FloatField(**BNULL)
producer = models.ForeignKey( producer = models.ForeignKey(BeerProducer, on_delete=models.DO_NOTHING, **BNULL)
BeerProducer, on_delete=models.DO_NOTHING, **BNULL
)
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
return reverse("beers:beer_detail", kwargs={"slug": self.uuid}) return reverse("beers:beer_detail", kwargs={"slug": self.uuid})
@ -130,9 +128,7 @@ class Beer(ScrobblableMixin):
) )
style_ids.append(style_inst.id) style_ids.append(style_inst.id)
producer, _created = BeerProducer.objects.get_or_create( producer, _created = BeerProducer.objects.get_or_create(**producer_dict)
**producer_dict
)
beer_dict["producer_id"] = producer.id beer_dict["producer_id"] = producer.id
beer = Beer.objects.create(**beer_dict) beer = Beer.objects.create(**beer_dict)
for style_id in style_ids: for style_id in style_ids:

View File

@ -85,9 +85,7 @@ def get_ibu_from_soup(soup) -> Optional[int]:
def get_rating_from_soup(soup) -> str: def get_rating_from_soup(soup) -> str:
rating = "" rating = ""
try: try:
rating = float( rating = float(soup.find(class_="num").get_text().strip("(").strip(")"))
soup.find(class_="num").get_text().strip("(").strip(")")
)
except AttributeError: except AttributeError:
rating = None rating = None
except ValueError: except ValueError:
@ -124,9 +122,7 @@ def get_beer_from_untappd_id(untappd_id: str) -> dict:
beer_dict = {"untappd_id": untappd_id} beer_dict = {"untappd_id": untappd_id}
if response.status_code != 200: if response.status_code != 200:
logger.warn( logger.warn("Bad response from untappd.com", extra={"response": response})
"Bad response from untappd.com", extra={"response": response}
)
return beer_dict return beer_dict
soup = BeautifulSoup(response.text, "html.parser") soup = BeautifulSoup(response.text, "html.parser")

View File

@ -14,9 +14,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SEARCH_ID_URL = ( SEARCH_ID_URL = "https://boardgamegeek.com/xmlapi/search?search={query}&exact=1"
"https://boardgamegeek.com/xmlapi/search?search={query}&exact=1"
)
GAME_ID_URL = "https://boardgamegeek.com/xmlapi/boardgame/{id}" GAME_ID_URL = "https://boardgamegeek.com/xmlapi/boardgame/{id}"
BGG_ACCESS_TOKEN = getattr(settings, "BGG_ACCESS_TOKEN", "") BGG_ACCESS_TOKEN = getattr(settings, "BGG_ACCESS_TOKEN", "")
BASE_HEADERS = { BASE_HEADERS = {

View File

@ -99,10 +99,7 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
def player_log(self) -> str: def player_log(self) -> str:
if self.players: if self.players:
return ", ".join( return ", ".join(
[ [BoardGameScoreLogData(**player).__str__() for player in self.players]
BoardGameScoreLogData(**player).__str__()
for player in self.players
]
) )
return "" return ""
@ -134,9 +131,7 @@ class BoardGamePublisher(TimeStampedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("boardgames:publisher_detail", kwargs={"slug": self.uuid})
"boardgames:publisher_detail", kwargs={"slug": self.uuid}
)
class BoardGameDesigner(TimeStampedModel): class BoardGameDesigner(TimeStampedModel):
@ -149,9 +144,7 @@ class BoardGameDesigner(TimeStampedModel):
return str(self.name) return str(self.name)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("boardgames:designer_detail", kwargs={"slug": self.uuid})
"boardgames:designer_detail", kwargs={"slug": self.uuid}
)
class BoardGameLocation(TimeStampedModel): class BoardGameLocation(TimeStampedModel):
@ -159,23 +152,17 @@ class BoardGameLocation(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
bgstats_id = models.UUIDField(**BNULL) bgstats_id = models.UUIDField(**BNULL)
description = models.TextField(**BNULL) description = models.TextField(**BNULL)
geo_location = models.ForeignKey( geo_location = models.ForeignKey(GeoLocation, **BNULL, on_delete=models.DO_NOTHING)
GeoLocation, **BNULL, on_delete=models.DO_NOTHING
)
def __str__(self) -> str: def __str__(self) -> str:
return str(self.name) return str(self.name)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("boardgames:location_detail", kwargs={"slug": self.uuid})
"boardgames:location_detail", kwargs={"slug": self.uuid}
)
class BoardGame(ScrobblableMixin): class BoardGame(ScrobblableMixin):
COMPLETION_PERCENT = getattr( COMPLETION_PERCENT = getattr(settings, "BOARD_GAME_COMPLETION_PERCENT", 100)
settings, "BOARD_GAME_COMPLETION_PERCENT", 100
)
FIELDS_FROM_BGGEEK = [ FIELDS_FROM_BGGEEK = [
"igdb_id", "igdb_id",
@ -250,9 +237,7 @@ class BoardGame(ScrobblableMixin):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("boardgames:boardgame_detail", kwargs={"slug": self.uuid})
"boardgames:boardgame_detail", kwargs={"slug": self.uuid}
)
@property @property
def logdata_cls(self): def logdata_cls(self):
@ -316,9 +301,7 @@ class BoardGame(ScrobblableMixin):
self.cover.save(fname, ContentFile(r.content), save=True) self.cover.save(fname, ContentFile(r.content), save=True)
@classmethod @classmethod
def find_or_create( def find_or_create(cls, lookup_id: str, data: dict[str, Any] = {}) -> "BoardGame":
cls, lookup_id: str, data: dict[str, Any] = {}
) -> "BoardGame":
"""Given a Lookup ID (either BGG or BGA ID), return a board game object""" """Given a Lookup ID (either BGG or BGA ID), return a board game object"""
game = cls.objects.filter(bggeek_id=lookup_id).first() game = cls.objects.filter(bggeek_id=lookup_id).first()
if not game: if not game:
@ -354,9 +337,7 @@ class BoardGame(ScrobblableMixin):
game.uses_teams = data.get("useTeams", False) game.uses_teams = data.get("useTeams", False)
game.bgstats_id = data.get("uuid", None) game.bgstats_id = data.get("uuid", None)
if publisher: if publisher:
publisher, _ = BoardGamePublisher.objects.get_or_create( publisher, _ = BoardGamePublisher.objects.get_or_create(name=publisher)
name=publisher
)
game.publisher = publisher game.publisher = publisher
game.save() game.save()
@ -369,9 +350,7 @@ class BoardGame(ScrobblableMixin):
if publishers: if publishers:
for name in publishers: for name in publishers:
publisher, _ = BoardGamePublisher.objects.get_or_create( publisher, _ = BoardGamePublisher.objects.get_or_create(name=name)
name=name
)
game.publishers.add(publisher) game.publishers.add(publisher)
return game return game

View File

@ -11,9 +11,7 @@ User = get_user_model()
def import_chess_games_for_user_id(user_id: int, commit: bool = False) -> dict: def import_chess_games_for_user_id(user_id: int, commit: bool = False) -> dict:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
client = berserk.Client( client = berserk.Client(session=berserk.TokenSession(settings.LICHESS_API_KEY))
session=berserk.TokenSession(settings.LICHESS_API_KEY)
)
games = client.games.export_by_player(user.profile.lichess_username) games = client.games.export_by_player(user.profile.lichess_username)
for game_dict in games: for game_dict in games:
chess, created = BoardGame.objects.get_or_create(title="Chess") chess, created = BoardGame.objects.get_or_create(title="Chess")
@ -62,9 +60,7 @@ def import_chess_games_for_user_id(user_id: int, commit: bool = False) -> dict:
white_player.get("aiLevel", "") white_player.get("aiLevel", "")
) )
else: else:
other_player["name_str"] = white_player.get("user", {}).get( other_player["name_str"] = white_player.get("user", {}).get("name", "")
"name", ""
)
other_player["lichess_username"] = other_player["name_str"] other_player["lichess_username"] = other_player["name_str"]
other_player["color"] = "white" other_player["color"] = "white"
@ -82,9 +78,7 @@ def import_chess_games_for_user_id(user_id: int, commit: bool = False) -> dict:
black_player.get("aiLevel", "") black_player.get("aiLevel", "")
) )
else: else:
other_player["name_str"] = black_player.get("user", {}).get( other_player["name_str"] = black_player.get("user", {}).get("name", "")
"name", ""
)
other_player["lichess_username"] = other_player["name_str"] other_player["lichess_username"] = other_player["name_str"]
other_player["color"] = "black" other_player["color"] = "black"
if winner == "white": if winner == "white":

View File

@ -37,9 +37,7 @@ class BoardGameListView(ScrobbleableListView):
for designer in scrobble.board_game.designers.all(): for designer in scrobble.board_game.designers.all():
designers_this_week[designer.id] = { designers_this_week[designer.id] = {
"designer": designer, "designer": designer,
"count": designers_this_week.get(designer.id, {}).get( "count": designers_this_week.get(designer.id, {}).get("count", 0)
"count", 0
)
+ 1, + 1,
} }
@ -48,9 +46,7 @@ class BoardGameListView(ScrobbleableListView):
for designer in scrobble.board_game.designers.all(): for designer in scrobble.board_game.designers.all():
designers_this_month[designer.id] = { designers_this_month[designer.id] = {
"designer": designer, "designer": designer,
"count": designers_this_month.get(designer.id, {}).get( "count": designers_this_month.get(designer.id, {}).get("count", 0)
"count", 0
)
+ 1, + 1,
} }

View File

@ -6,9 +6,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
USER_AGENT = ( USER_AGENT = "Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
"Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
)
AMAZON_SEARCH_URL = "https://www.amazon.com/s?k={amazon_id}" AMAZON_SEARCH_URL = "https://www.amazon.com/s?k={amazon_id}"
@ -95,32 +93,20 @@ def get_amazon_product_dict(amazon_id: str) -> dict:
soup = BeautifulSoup(response.text, "html.parser") soup = BeautifulSoup(response.text, "html.parser")
try: try:
data_dict["title"] = soup.findAll("span", class_="celwidget")[ data_dict["title"] = soup.findAll("span", class_="celwidget")[1].text.strip()
1 data_dict["cover_url"] = soup.find("img", class_="frontImage").get("src")
].text.strip() data_dict["summary"] = soup.findAll("div", class_="a-expander-content")[1].text
data_dict["cover_url"] = soup.find("img", class_="frontImage").get(
"src"
)
data_dict["summary"] = soup.findAll(
"div", class_="a-expander-content"
)[1].text
meta = soup.findAll("div", class_="rpi-attribute-value") meta = soup.findAll("div", class_="rpi-attribute-value")
data_dict["isbn"] = meta[AmazonAttribute.ISBN_10.value].text.strip() data_dict["isbn"] = meta[AmazonAttribute.ISBN_10.value].text.strip()
pages = meta[AmazonAttribute.PAGES.value].text pages = meta[AmazonAttribute.PAGES.value].text
if "pages" in pages: if "pages" in pages:
data_dict["pages"] = ( data_dict["pages"] = (
meta[AmazonAttribute.PAGES.value] meta[AmazonAttribute.PAGES.value].text.split("pages")[0].strip()
.text.split("pages")[0]
.strip()
) )
except IndexError as e: except IndexError as e:
logger.error( logger.error(f"Amazon lookup is failing for this product {amazon_id}: {e}")
f"Amazon lookup is failing for this product {amazon_id}: {e}"
)
except AttributeError as e: except AttributeError as e:
logger.error( logger.error(f"Amazon lookup is failing for this product {amazon_id}: {e}")
f"Amazon lookup is failing for this product {amazon_id}: {e}"
)
return data_dict return data_dict

View File

@ -146,9 +146,7 @@ def build_book_map(rows) -> dict:
) )
continue continue
book = Book.objects.filter( book = Book.objects.filter(
koreader_data_by_hash__icontains=book_row[ koreader_data_by_hash__icontains=book_row[KoReaderBookColumn.MD5.value]
KoReaderBookColumn.MD5.value
]
).first() ).first()
if not book: if not book:
@ -202,15 +200,11 @@ def build_page_data(page_rows: list, book_map: dict, user_tz=None) -> dict:
"end_ts": start_ts + duration, "end_ts": start_ts + duration,
} }
if book_ids_not_found: if book_ids_not_found:
logger.info( logger.info(f"Found pages for books not in file: {set(book_ids_not_found)}")
f"Found pages for books not in file: {set(book_ids_not_found)}"
)
return book_map return book_map
def build_scrobbles_from_book_map( def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobble"]:
book_map: dict, user: "User"
) -> list["Scrobble"]:
Scrobble = apps.get_model("scrobbles", "Scrobble") Scrobble = apps.get_model("scrobbles", "Scrobble")
scrobbles_to_create = [] scrobbles_to_create = []
@ -240,9 +234,9 @@ def build_scrobbles_from_book_map(
seconds_from_last_page = 0 seconds_from_last_page = 0
if prev_page_stats: if prev_page_stats:
seconds_from_last_page = stats.get( seconds_from_last_page = stats.get("end_ts") - prev_page_stats.get(
"end_ts" "start_ts"
) - prev_page_stats.get("start_ts") )
playback_position_seconds = playback_position_seconds + stats.get( playback_position_seconds = playback_position_seconds + stats.get(
"duration" "duration"
@ -251,9 +245,7 @@ def build_scrobbles_from_book_map(
end_of_reading = pages_processed == total_pages_read end_of_reading = pages_processed == total_pages_read
big_jump_to_this_page = (cur_page_number - last_page_number) > 10 big_jump_to_this_page = (cur_page_number - last_page_number) > 10
is_session_gap = seconds_from_last_page > SESSION_GAP_SECONDS is_session_gap = seconds_from_last_page > SESSION_GAP_SECONDS
if ( if (is_session_gap and not big_jump_to_this_page) or end_of_reading:
is_session_gap and not big_jump_to_this_page
) or end_of_reading:
should_create_scrobble = True should_create_scrobble = True
if should_create_scrobble: if should_create_scrobble:
@ -381,9 +373,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
return new_scrobbles return new_scrobbles
book_map = build_page_data( book_map = build_page_data(
cur.execute( cur.execute("SELECT * from page_stat_data ORDER BY id_book, start_time"),
"SELECT * from page_stat_data ORDER BY id_book, start_time"
),
book_map, book_map,
tz, tz,
) )

View File

@ -13,9 +13,7 @@ HEADERS = {
} }
LOCG_WRTIER_URL = "" LOCG_WRTIER_URL = ""
LOCG_WRITER_DETAIL_URL = "https://leagueofcomicgeeks.com/people/{slug}" LOCG_WRITER_DETAIL_URL = "https://leagueofcomicgeeks.com/people/{slug}"
LOCG_SEARCH_URL = ( LOCG_SEARCH_URL = "https://leagueofcomicgeeks.com/search/ajax_issues?query={query}"
"https://leagueofcomicgeeks.com/search/ajax_issues?query={query}"
)
LOCG_DETAIL_URL = "https://leagueofcomicgeeks.com/comic/{locg_slug}" LOCG_DETAIL_URL = "https://leagueofcomicgeeks.com/comic/{locg_slug}"
@ -72,26 +70,19 @@ def lookup_comic_by_locg_slug(slug: str) -> dict:
attrs = soup.findAll("div", class_="details-addtl-block") attrs = soup.findAll("div", class_="details-addtl-block")
try: try:
data_dict["pages"] = ( data_dict["pages"] = (
attrs[1] attrs[1].find("div", class_="value").text.split("pages")[0].strip()
.find("div", class_="value")
.text.split("pages")[0]
.strip()
) )
except IndexError: except IndexError:
logger.warn(f"No ISBN field") logger.warn(f"No ISBN field")
try: try:
data_dict["isbn"] = ( data_dict["isbn"] = attrs[3].find("div", class_="value").text.strip()
attrs[3].find("div", class_="value").text.strip()
)
except IndexError: except IndexError:
logger.warn(f"No ISBN field") logger.warn(f"No ISBN field")
writer_slug = None writer_slug = None
try: try:
writer_slug = ( writer_slug = (
soup.findAll("div", class_="name")[5] soup.findAll("div", class_="name")[5].a.get("href").split("people/")[1]
.a.get("href")
.split("people/")[1]
) )
except IndexError: except IndexError:
logger.warn(f"No wrtier found") logger.warn(f"No wrtier found")

View File

@ -37,7 +37,5 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
scrobbles_to_create = [] scrobbles_to_create = []
for scrobble in Scrobble.objects.filter( for scrobble in Scrobble.objects.filter(media_type="Book", source="KOReader"):
media_type="Book", source="KOReader"
):
update_scrobble_from_page_data(scrobble) update_scrobble_from_page_data(scrobble)

View File

@ -52,8 +52,7 @@ class Command(BaseCommand):
seconds_from_last_page = 0 seconds_from_last_page = 0
if prev_page: if prev_page:
seconds_from_last_page = ( seconds_from_last_page = (
page.end_time.timestamp() page.end_time.timestamp() - prev_page.start_time.timestamp()
- prev_page.start_time.timestamp()
) )
playback_position_seconds = ( playback_position_seconds = (
playback_position_seconds + page.duration_seconds playback_position_seconds + page.duration_seconds
@ -62,9 +61,7 @@ class Command(BaseCommand):
end_of_reading = pages_processed == total_pages end_of_reading = pages_processed == total_pages
big_jump_to_this_page = False big_jump_to_this_page = False
if prev_page: if prev_page:
big_jump_to_this_page = ( big_jump_to_this_page = (page.number - prev_page.number) > 10
page.number - prev_page.number
) > 10
if ( if (
seconds_from_last_page > SESSION_GAP_SECONDS seconds_from_last_page > SESSION_GAP_SECONDS
and not big_jump_to_this_page and not big_jump_to_this_page
@ -113,9 +110,7 @@ class Command(BaseCommand):
user_id=user.id, user_id=user.id,
).first() ).first()
if scrobble: if scrobble:
logger.info( logger.info(f"Found existing scrobble {scrobble}, updating")
f"Found existing scrobble {scrobble}, updating"
)
scrobble.book_page_data = scrobble_page_data scrobble.book_page_data = scrobble_page_data
scrobble.playback_position_seconds = ( scrobble.playback_position_seconds = (
scrobble.calc_reading_duration() scrobble.calc_reading_duration()

View File

@ -133,9 +133,7 @@ class Author(TimeStampedModel):
class Book(LongPlayScrobblableMixin): class Book(LongPlayScrobblableMixin):
COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95) COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
AVG_PAGE_READING_SECONDS = getattr( AVG_PAGE_READING_SECONDS = getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60)
settings, "AVERAGE_PAGE_READING_SECONDS", 60
)
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
original_title = models.CharField(max_length=255, **BNULL) original_title = models.CharField(max_length=255, **BNULL)
@ -260,9 +258,7 @@ class Book(LongPlayScrobblableMixin):
# TODO use either a Google Books id identifier or author name like for tracks # TODO use either a Google Books id identifier or author name like for tracks
book, created = cls.objects.get_or_create(original_title=title) book, created = cls.objects.get_or_create(original_title=title)
if not created: if not created:
logger.info( logger.info("Found exact match for book by title", extra={"title": title})
"Found exact match for book by title", extra={"title": title}
)
if not enrich: if not enrich:
logger.info( logger.info(
@ -296,9 +292,7 @@ class Book(LongPlayScrobblableMixin):
if authors: if authors:
for author_str in authors: for author_str in authors:
if author_str: if author_str:
author, a_created = Author.objects.get_or_create( author, a_created = Author.objects.get_or_create(name=author_str)
name=author_str
)
author_list.append(author) author_list.append(author)
if a_created: if a_created:
# TODO enrich author # TODO enrich author
@ -332,13 +326,9 @@ class Book(LongPlayScrobblableMixin):
if not data: if not data:
logger.warn(f"Checking openlibrary for {self.title}") logger.warn(f"Checking openlibrary for {self.title}")
if self.openlibrary_id and force_update: if self.openlibrary_id and force_update:
data = lookup_book_from_openlibrary( data = lookup_book_from_openlibrary(str(self.openlibrary_id))
str(self.openlibrary_id)
)
else: else:
data = lookup_book_from_openlibrary( data = lookup_book_from_openlibrary(str(self.title), author_name)
str(self.title), author_name
)
if not data: if not data:
if self.locg_slug: if self.locg_slug:
@ -385,10 +375,7 @@ class Book(LongPlayScrobblableMixin):
if "pages" in data.keys() and data.get("pages") == None: if "pages" in data.keys() and data.get("pages") == None:
data.pop("pages") data.pop("pages")
if ( if not isinstance(data.get("pages"), int) and "pages" in data.keys():
not isinstance(data.get("pages"), int)
and "pages" in data.keys()
):
logger.info( logger.info(
f"Pages for {self} from OL expected to be int, but got {data.get('pages')}" f"Pages for {self} from OL expected to be int, but got {data.get('pages')}"
) )
@ -420,9 +407,7 @@ class Book(LongPlayScrobblableMixin):
self.save() self.save()
def fix_authors_metadata(self, openlibrary_author_id): def fix_authors_metadata(self, openlibrary_author_id):
author = Author.objects.filter( author = Author.objects.filter(openlibrary_id=openlibrary_author_id).first()
openlibrary_id=openlibrary_author_id
).first()
if not author: if not author:
data = lookup_author_from_openlibrary(openlibrary_author_id) data = lookup_author_from_openlibrary(openlibrary_author_id)
author_image_url = data.pop("author_headshot_url", None) author_image_url = data.pop("author_headshot_url", None)
@ -433,9 +418,7 @@ class Book(LongPlayScrobblableMixin):
r = requests.get(author_image_url) r = requests.get(author_image_url)
if r.status_code == 200: if r.status_code == 200:
fname = f"{author.name}_{author.uuid}.jpg" fname = f"{author.name}_{author.uuid}.jpg"
author.headshot.save( author.headshot.save(fname, ContentFile(r.content), save=True)
fname, ContentFile(r.content), save=True
)
self.authors.add(author) self.authors.add(author)
def get_author_from_locg(self, locg_slug): def get_author_from_locg(self, locg_slug):
@ -451,9 +434,7 @@ class Book(LongPlayScrobblableMixin):
author.headshot.save(fname, ContentFile(r.content), save=True) author.headshot.save(fname, ContentFile(r.content), save=True)
self.authors.add(author) self.authors.add(author)
def page_data_for_user( def page_data_for_user(self, user_id: int, convert_timestamps: bool = True) -> dict:
self, user_id: int, convert_timestamps: bool = True
) -> dict:
scrobbles = self.scrobble_set.filter(user=user_id) scrobbles = self.scrobble_set.filter(user=user_id)
pages = {} pages = {}
@ -461,9 +442,7 @@ class Book(LongPlayScrobblableMixin):
if scrobble.logdata.page_data: if scrobble.logdata.page_data:
for page, data in scrobble.logdata.page_data.items(): for page, data in scrobble.logdata.page_data.items():
if convert_timestamps: if convert_timestamps:
data["start_ts"] = datetime.fromtimestamp( data["start_ts"] = datetime.fromtimestamp(data["start_ts"])
data["start_ts"]
)
data["end_ts"] = datetime.fromtimestamp(data["end_ts"]) data["end_ts"] = datetime.fromtimestamp(data["end_ts"])
pages[page] = data pages[page] = data
sorted_pages = OrderedDict( sorted_pages = OrderedDict(
@ -502,9 +481,7 @@ class Paper(LongPlayScrobblableMixin):
"""Keeps track of Academic Papers""" """Keeps track of Academic Papers"""
COMPLETION_PERCENT = getattr(settings, "PAPER_COMPLETION_PERCENT", 60) COMPLETION_PERCENT = getattr(settings, "PAPER_COMPLETION_PERCENT", 60)
AVG_PAGE_READING_SECONDS = getattr( AVG_PAGE_READING_SECONDS = getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60)
settings, "AVERAGE_PAGE_READING_SECONDS", 60
)
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
semantic_title = models.CharField(max_length=255, **BNULL) semantic_title = models.CharField(max_length=255, **BNULL)

View File

@ -10,7 +10,9 @@ from thefuzz import fuzz
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ISBN_URL = "https://openlibrary.org/isbn/{isbn}.json" ISBN_URL = "https://openlibrary.org/isbn/{isbn}.json"
SEARCH_URL = "https://openlibrary.org/search.json?q={query}&sort=editions&mode=everything" SEARCH_URL = (
"https://openlibrary.org/search.json?q={query}&sort=editions&mode=everything"
)
AUTHOR_SEARCH_URL = "https://openlibrary.org/search/authors.json?q={query}" AUTHOR_SEARCH_URL = "https://openlibrary.org/search/authors.json?q={query}"
COVER_URL = "https://covers.openlibrary.org/b/olid/{id}-L.jpg" COVER_URL = "https://covers.openlibrary.org/b/olid/{id}-L.jpg"
AUTHOR_URL = "https://openlibrary.org/authors/{id}.json" AUTHOR_URL = "https://openlibrary.org/authors/{id}.json"
@ -80,9 +82,7 @@ def lookup_author_from_openlibrary(olid: str) -> dict:
} }
def lookup_book_from_openlibrary( def lookup_book_from_openlibrary(title: str, author: Optional[str] = None) -> dict:
title: str, author: Optional[str] = None
) -> dict:
title_quoted = urllib.parse.quote(title) title_quoted = urllib.parse.quote(title)
author_quoted = "" author_quoted = ""
if author: if author:

View File

@ -160,9 +160,7 @@ class ComicVineClient(object):
# or not the request cache is to be used, store the procedure in a # or not the request cache is to be used, store the procedure in a
# local function to avoid repetition. # local function to avoid repetition.
def __httpget(): def __httpget():
response = requests.get( response = requests.get(self.API_URL, headers=self.HEADERS, params=params)
self.API_URL, headers=self.HEADERS, params=params
)
if not response.ok: if not response.ok:
self._handle_http_error(response) self._handle_http_error(response)
@ -214,19 +212,13 @@ def lookup_comic_from_comicvine(title: str) -> dict:
api_key = getattr(settings, "COMICVINE_API_KEY", "") api_key = getattr(settings, "COMICVINE_API_KEY", "")
if not api_key: if not api_key:
logger.warning( logger.warning("No ComicVine API key configured, not looking anything up")
"No ComicVine API key configured, not looking anything up"
)
return {} return {}
client = ComicVineClient( client = ComicVineClient(api_key=getattr(settings, "COMICVINE_API_KEY", None))
api_key=getattr(settings, "COMICVINE_API_KEY", None)
)
raw_results = client.search(title).get("results") raw_results = client.search(title).get("results")
results = [ results = [r for r in raw_results if r.get("resource_type") == resource_type]
r for r in raw_results if r.get("resource_type") == resource_type
]
if not results: if not results:
logger.warning("No comic found on ComicVine") logger.warning("No comic found on ComicVine")
return {} return {}

View File

@ -6,9 +6,7 @@ import requests
from django.conf import settings from django.conf import settings
API_KEY = settings.GOOGLE_API_KEY API_KEY = settings.GOOGLE_API_KEY
GOOGLE_BOOKS_URL = ( GOOGLE_BOOKS_URL = 'https://www.googleapis.com/books/v1/volumes?q="{title}"&key={key}'
'https://www.googleapis.com/books/v1/volumes?q="{title}"&key={key}'
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,14 +19,10 @@ def lookup_book_from_google(title: str) -> dict:
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
if response.status_code != 200: if response.status_code != 200:
logger.warning( logger.warning("Bad response from Google", extra={"response": response})
"Bad response from Google", extra={"response": response}
)
return book_dict return book_dict
google_result = ( google_result = json.loads(response.content).get("items", [{}])[0].get("volumeInfo")
json.loads(response.content).get("items", [{}])[0].get("volumeInfo")
)
if not google_result: if not google_result:
return {} return {}
@ -69,8 +63,8 @@ def lookup_book_from_google(title: str) -> dict:
book_dict["base_run_time_seconds"] = 3600 book_dict["base_run_time_seconds"] = 3600
if book_dict.get("pages"): if book_dict.get("pages"):
book_dict["base_run_time_seconds"] = book_dict.get( book_dict["base_run_time_seconds"] = book_dict.get("pages", 10) * getattr(
"pages", 10 settings, "AVERAGE_PAGE_READING_SECONDS", 60
) * getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60) )
return book_dict return book_dict

View File

@ -18,9 +18,7 @@ def get_api_result(url):
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
if response.status_code != 200: if response.status_code != 200:
logger.warning( logger.warning("Bad response from Semantic", extra={"response": response})
"Bad response from Semantic", extra={"response": response}
)
return None return None
return response return response
@ -64,12 +62,10 @@ def lookup_paper_from_semantic(title: str) -> dict:
paper_dict["tldr"] = result.get("bib", {}).get("abstract") paper_dict["tldr"] = result.get("bib", {}).get("abstract")
paper_dict["journal"] = result.get("journal", {}).get("name") paper_dict["journal"] = result.get("journal", {}).get("name")
paper_dict["journal_volume"] = result.get("journal", {}).get("volume") paper_dict["journal_volume"] = result.get("journal", {}).get("volume")
paper_dict["openaccess_pdf_url"] = result.get("openAccessPdf", {}).get( paper_dict["openaccess_pdf_url"] = result.get("openAccessPdf", {}).get("url")
"url" paper_dict["base_run_time_seconds"] = paper_dict.get("pages", 10) * getattr(
settings, "AVERAGE_PAGE_READING_SECONDS", 60
) )
paper_dict["base_run_time_seconds"] = paper_dict.get(
"pages", 10
) * getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60)
paper_dict["author_dicts"] = result.get("authors") paper_dict["author_dicts"] = result.get("authors")
paper_dict["genres"] = result.get("fieldsOfStudy") paper_dict["genres"] = result.get("fieldsOfStudy")

View File

@ -99,7 +99,9 @@ class KoReaderBookRows:
] ]
) )
if end_session: if end_session:
start_time += 3600 # one second over an hour, marking a new reading session start_time += (
3600 # one second over an hour, marking a new reading session
)
end_session = False end_session = False
else: else:
start_time += AVERAGE_PAGE_READING_SECONDS start_time += AVERAGE_PAGE_READING_SECONDS

View File

@ -32,17 +32,13 @@ def test_load_page_data_to_map(get_mock, koreader_rows, valid_response):
) )
assert ( assert (
len(book_map[1]["pages"]) len(book_map[1]["pages"])
== koreader_rows.BOOK_ROWS[0][ == koreader_rows.BOOK_ROWS[0][KoReaderBookColumn.TOTAL_READ_PAGES.value]
KoReaderBookColumn.TOTAL_READ_PAGES.value
]
) )
@pytest.mark.django_db @pytest.mark.django_db
@mock.patch("requests.get") @mock.patch("requests.get")
def test_build_scrobbles_from_pages( def test_build_scrobbles_from_pages(get_mock, koreader_rows, demo_user, valid_response):
get_mock, koreader_rows, demo_user, valid_response
):
get_mock.return_value = valid_response get_mock.return_value = valid_response
book_map = build_book_map(koreader_rows.BOOK_ROWS) book_map = build_book_map(koreader_rows.BOOK_ROWS)
book_map = build_page_data(koreader_rows.PAGE_STATS_ROWS, book_map) book_map = build_page_data(koreader_rows.PAGE_STATS_ROWS, book_map)

View File

@ -26,9 +26,7 @@ class BrickSet(LongPlayScrobblableMixin):
number = models.CharField(max_length=10, **BNULL) number = models.CharField(max_length=10, **BNULL)
release_year = models.IntegerField(**BNULL) release_year = models.IntegerField(**BNULL)
piece_count = models.IntegerField(**BNULL) piece_count = models.IntegerField(**BNULL)
brickset_rating = models.DecimalField( brickset_rating = models.DecimalField(max_digits=3, decimal_places=1, **BNULL)
max_digits=3, decimal_places=1, **BNULL
)
lego_item_number = models.CharField(max_length=10, **BNULL) lego_item_number = models.CharField(max_length=10, **BNULL)
box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL) box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
box_image_small = ImageSpecField( box_image_small = ImageSpecField(

View File

@ -7,14 +7,16 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(""" cursor.execute(
"""
INSERT OR IGNORE INTO charts_chartrecord INSERT OR IGNORE INTO charts_chartrecord
(created, modified, user_id, rank, count, year, month, week, day, (created, modified, user_id, rank, count, year, month, week, day,
video_id, series_id, artist_id, track_id, period_start, period_end) video_id, series_id, artist_id, track_id, period_start, period_end)
SELECT created, modified, user_id, rank, count, year, month, week, day, SELECT created, modified, user_id, rank, count, year, month, week, day,
video_id, series_id, artist_id, track_id, period_start, period_end video_id, series_id, artist_id, track_id, period_start, period_end
FROM scrobbles_chartrecord FROM scrobbles_chartrecord
""") """
)
self.stdout.write( self.stdout.write(
self.style.SUCCESS("Copied ChartRecord data to charts app") self.style.SUCCESS("Copied ChartRecord data to charts app")
) )

View File

@ -87,9 +87,7 @@ def get_ibu_from_soup(soup) -> Optional[int]:
def get_rating_from_soup(soup) -> str: def get_rating_from_soup(soup) -> str:
rating = "" rating = ""
try: try:
rating = float( rating = float(soup.find(class_="num").get_text().strip("(").strip(")"))
soup.find(class_="num").get_text().strip("(").strip(")")
)
except AttributeError: except AttributeError:
rating = None rating = None
except ValueError: except ValueError:
@ -127,9 +125,7 @@ def get_food_from_allrecipe_id(allrecipe_id: str) -> dict:
food_dict = {"allrecipe_id": allrecipe_id} food_dict = {"allrecipe_id": allrecipe_id}
if response.status_code != 200: if response.status_code != 200:
logger.warn( logger.warn("Bad response from allrecipe", extra={"response": response})
"Bad response from allrecipe", extra={"response": response}
)
return food_dict return food_dict
import pdb import pdb

View File

@ -63,17 +63,13 @@ class Food(ScrobblableMixin):
cook_time_minutes = models.IntegerField(**BNULL) cook_time_minutes = models.IntegerField(**BNULL)
total_time_minutes = models.IntegerField(**BNULL) total_time_minutes = models.IntegerField(**BNULL)
servings = models.IntegerField(**BNULL) servings = models.IntegerField(**BNULL)
yield_text = models.CharField( yield_text = models.CharField(max_length=255, **BNULL) # e.g., "8 calzones"
max_length=255, **BNULL
) # e.g., "8 calzones"
# Nutrition (per serving) # Nutrition (per serving)
calories = models.IntegerField(**BNULL) calories = models.IntegerField(**BNULL)
protein = models.DecimalField(max_digits=10, decimal_places=2, **BNULL) protein = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
fat = models.DecimalField(max_digits=10, decimal_places=2, **BNULL) fat = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
carbohydrates = models.DecimalField( carbohydrates = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
max_digits=10, decimal_places=2, **BNULL
)
fiber = models.DecimalField(max_digits=10, decimal_places=2, **BNULL) fiber = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
sugar = models.DecimalField(max_digits=10, decimal_places=2, **BNULL) sugar = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
sodium = models.DecimalField(max_digits=10, decimal_places=2, **BNULL) sodium = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
@ -93,9 +89,7 @@ class Food(ScrobblableMixin):
) )
allrecipe_id = models.CharField(max_length=255, **BNULL) allrecipe_id = models.CharField(max_length=255, **BNULL)
allrecipe_rating = models.FloatField(**BNULL) allrecipe_rating = models.FloatField(**BNULL)
category = models.ForeignKey( category = models.ForeignKey(FoodCategory, on_delete=models.DO_NOTHING, **BNULL)
FoodCategory, on_delete=models.DO_NOTHING, **BNULL
)
class Meta: class Meta:
indexes = [ indexes = [
@ -136,9 +130,7 @@ class Food(ScrobblableMixin):
return FoodLogData return FoodLogData
@classmethod @classmethod
def find_or_create_from_recipe( def find_or_create_from_recipe(cls, url: str, category=None) -> Tuple["Food", bool]:
cls, url: str, category=None
) -> Tuple["Food", bool]:
""" """
Scrape a recipe URL and create/update Food instance. Scrape a recipe URL and create/update Food instance.
Uses django-taggit for tags and existing FoodCategory for category. Uses django-taggit for tags and existing FoodCategory for category.
@ -164,16 +156,12 @@ class Food(ScrobblableMixin):
nutrition = recipe_data.get("nutrition") nutrition = recipe_data.get("nutrition")
if not nutrition: if not nutrition:
calculator = NutritionCalculator() calculator = NutritionCalculator()
servings = ( servings = scraper.parse_servings(recipe_data.get("yields", "1")) or 1
scraper.parse_servings(recipe_data.get("yields", "1")) or 1
)
nutrition = calculator.calculate_nutrition( nutrition = calculator.calculate_nutrition(
recipe_data.get("ingredients", []), servings recipe_data.get("ingredients", []), servings
) )
else: else:
servings = ( servings = scraper.parse_servings(recipe_data.get("yields", "1")) or 1
scraper.parse_servings(recipe_data.get("yields", "1")) or 1
)
# Get or create category (if not provided) # Get or create category (if not provided)
if not category and recipe_data.get("category"): if not category and recipe_data.get("category"):
@ -198,9 +186,7 @@ class Food(ScrobblableMixin):
calories=int(nutrition.get("calories", 0)) if nutrition else None, calories=int(nutrition.get("calories", 0)) if nutrition else None,
protein=nutrition.get("protein") if nutrition else None, protein=nutrition.get("protein") if nutrition else None,
fat=nutrition.get("fat") if nutrition else None, fat=nutrition.get("fat") if nutrition else None,
carbohydrates=( carbohydrates=(nutrition.get("carbohydrates") if nutrition else None),
nutrition.get("carbohydrates") if nutrition else None
),
fiber=nutrition.get("fiber") if nutrition else None, fiber=nutrition.get("fiber") if nutrition else None,
category=category, category=category,
) )
@ -246,9 +232,7 @@ class Food(ScrobblableMixin):
calories=int(nutrition.get("calories", 0)) if nutrition else None, calories=int(nutrition.get("calories", 0)) if nutrition else None,
protein=nutrition.get("protein") if nutrition else None, protein=nutrition.get("protein") if nutrition else None,
fat=nutrition.get("fat") if nutrition else None, fat=nutrition.get("fat") if nutrition else None,
carbohydrates=( carbohydrates=(nutrition.get("carbohydrates") if nutrition else None),
nutrition.get("carbohydrates") if nutrition else None
),
fiber=nutrition.get("fiber") if nutrition else None, fiber=nutrition.get("fiber") if nutrition else None,
category=category, category=category,
) )
@ -263,9 +247,7 @@ class Food(ScrobblableMixin):
try: try:
ingredients = json.loads(self.ingredients) ingredients = json.loads(self.ingredients)
calculator = NutritionCalculator() calculator = NutritionCalculator()
nutrition = calculator.calculate_nutrition( nutrition = calculator.calculate_nutrition(ingredients, self.servings or 1)
ingredients, self.servings or 1
)
self.calories = int(nutrition.get("calories", 0)) self.calories = int(nutrition.get("calories", 0))
self.protein = nutrition.get("protein") self.protein = nutrition.get("protein")

View File

@ -87,9 +87,7 @@ class RecipeScraperService:
response = requests.get( response = requests.get(
url, url,
timeout=10, timeout=10,
headers={ headers={"User-Agent": "Mozilla/5.0 (compatible; Vrobbler/0.3)"},
"User-Agent": "Mozilla/5.0 (compatible; Vrobbler/0.3)"
},
) )
if response.status_code != 200: if response.status_code != 200:
logger.debug("Recipe website returned non 200 response") logger.debug("Recipe website returned non 200 response")
@ -181,9 +179,7 @@ class RecipeScraperService:
keywords = recipe_data.get("keywords") keywords = recipe_data.get("keywords")
if keywords: if keywords:
if isinstance(keywords, str): if isinstance(keywords, str):
tags.update( tags.update([k.strip() for k in keywords.split(",") if k.strip()])
[k.strip() for k in keywords.split(",") if k.strip()]
)
elif isinstance(keywords, list): elif isinstance(keywords, list):
tags.update([k.strip() for k in keywords if k.strip()]) tags.update([k.strip() for k in keywords if k.strip()])

View File

@ -110,22 +110,14 @@ class USDAFoodAPI:
if "nutrientNumber" in nutrient: if "nutrientNumber" in nutrient:
nutrient_id = str(nutrient.get("nutrientNumber", "")) nutrient_id = str(nutrient.get("nutrientNumber", ""))
value = nutrient.get("value") value = nutrient.get("value")
elif "nutrient" in nutrient and isinstance( elif "nutrient" in nutrient and isinstance(nutrient["nutrient"], dict):
nutrient["nutrient"], dict nutrient_id = str(nutrient.get("nutrient", {}).get("id", ""))
):
nutrient_id = str(
nutrient.get("nutrient", {}).get("id", "")
)
value = nutrient.get("value") value = nutrient.get("value")
elif "number" in nutrient: elif "number" in nutrient:
nutrient_id = str(nutrient.get("number", "")) nutrient_id = str(nutrient.get("number", ""))
value = nutrient.get("value") value = nutrient.get("value")
if ( if nutrient_id and nutrient_id in nutrient_map and value is not None:
nutrient_id
and nutrient_id in nutrient_map
and value is not None
):
key = nutrient_map[nutrient_id] key = nutrient_map[nutrient_id]
try: try:
nutrients[key] = float(value) nutrients[key] = float(value)
@ -404,11 +396,7 @@ class NutritionCalculator:
# Strategy 1: Direct search # Strategy 1: Direct search
ingredient_name, ingredient_name,
# Strategy 2: Singular form (remove trailing 's') # Strategy 2: Singular form (remove trailing 's')
( (ingredient_name.rstrip("s") if ingredient_name.endswith("s") else None),
ingredient_name.rstrip("s")
if ingredient_name.endswith("s")
else None
),
# Strategy 3: Remove common suffixes # Strategy 3: Remove common suffixes
re.sub( re.sub(
r"\s+(fresh|dried|ground|chopped|sliced)$", r"\s+(fresh|dried|ground|chopped|sliced)$",
@ -417,11 +405,7 @@ class NutritionCalculator:
flags=re.IGNORECASE, flags=re.IGNORECASE,
), ),
# Strategy 4: Just the last word (sometimes works for simple ingredients) # Strategy 4: Just the last word (sometimes works for simple ingredients)
( (ingredient_name.split()[-1] if len(ingredient_name.split()) > 1 else None),
ingredient_name.split()[-1]
if len(ingredient_name.split()) > 1
else None
),
] ]
for query in strategies: for query in strategies:
@ -431,9 +415,7 @@ class NutritionCalculator:
try: try:
results = self.usda.search_foods(query, page_size=5) results = self.usda.search_foods(query, page_size=5)
if results: if results:
logger.debug( logger.debug(f"Found {query}: {results[0].get('description')}")
f"Found {query}: {results[0].get('description')}"
)
return results[0] return results[0]
except Exception as e: except Exception as e:
logger.warning(f"USDA search failed for '{query}': {e}") logger.warning(f"USDA search failed for '{query}': {e}")
@ -471,9 +453,7 @@ class NutritionCalculator:
} }
# Get base weight in grams # Get base weight in grams
base_grams = volume_to_grams.get( base_grams = volume_to_grams.get(unit, 100) # Default to 100g if unknown
unit, 100
) # Default to 100g if unknown
# Adjust for ingredient type (very simplified) # Adjust for ingredient type (very simplified)
if any( if any(
@ -481,22 +461,16 @@ class NutritionCalculator:
for word in ["flour", "sugar", "rice", "grain"] for word in ["flour", "sugar", "rice", "grain"]
): ):
base_grams = volume_to_grams.get(unit, 125) # Dry ingredients base_grams = volume_to_grams.get(unit, 125) # Dry ingredients
elif any( elif any(word in ingredient_name.lower() for word in ["oil", "butter", "fat"]):
word in ingredient_name.lower()
for word in ["oil", "butter", "fat"]
):
base_grams = volume_to_grams.get(unit, 220) # Dense liquids base_grams = volume_to_grams.get(unit, 220) # Dense liquids
elif any( elif any(
word in ingredient_name.lower() word in ingredient_name.lower() for word in ["milk", "water", "broth"]
for word in ["milk", "water", "broth"]
): ):
base_grams = volume_to_grams.get(unit, 240) # Liquids base_grams = volume_to_grams.get(unit, 240) # Liquids
return quantity * base_grams return quantity * base_grams
def calculate_nutrition( def calculate_nutrition(self, ingredients: List[str], servings: int = 1) -> Dict:
self, ingredients: List[str], servings: int = 1
) -> Dict:
""" """
Calculate total nutrition for a recipe from ingredient list. Calculate total nutrition for a recipe from ingredient list.
Returns nutrition per serving. Returns nutrition per serving.
@ -543,12 +517,8 @@ class NutritionCalculator:
# USDA data is typically per 100g, so scale accordingly # USDA data is typically per 100g, so scale accordingly
multiplier = grams / 100 multiplier = grams / 100
totals["calories"] += ( totals["calories"] += nutrients.get("calories", 0) * multiplier
nutrients.get("calories", 0) * multiplier totals["protein"] += nutrients.get("protein", 0) * multiplier
)
totals["protein"] += (
nutrients.get("protein", 0) * multiplier
)
totals["fat"] += nutrients.get("fat", 0) * multiplier totals["fat"] += nutrients.get("fat", 0) * multiplier
totals["carbohydrates"] += ( totals["carbohydrates"] += (
nutrients.get("carbohydrates", 0) * multiplier nutrients.get("carbohydrates", 0) * multiplier

View File

@ -20,9 +20,7 @@ class LifeEvent(ScrobblableMixin):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("life-events:life-event_detail", kwargs={"slug": self.uuid})
"life-events:life-event_detail", kwargs={"slug": self.uuid}
)
@property @property
def logdata_cls(self): def logdata_cls(self):
@ -38,6 +36,6 @@ class LifeEvent(ScrobblableMixin):
def scrobbles(self, user_id): def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble") Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter( return Scrobble.objects.filter(user_id=user_id, life_event=self).order_by(
user_id=user_id, life_event=self "-timestamp"
).order_by("-timestamp") )

View File

@ -5,9 +5,7 @@ app_name = "lifeevents"
urlpatterns = [ urlpatterns = [
path( path("lifeevents/", views.LifeEventListView.as_view(), name="lifeevent_list"),
"lifeevents/", views.LifeEventListView.as_view(), name="lifeevent_list"
),
path( path(
"lifeevent/<slug:slug>/", "lifeevent/<slug:slug>/",
views.LifeEventDetailView.as_view(), views.LifeEventDetailView.as_view(),

View File

@ -42,9 +42,7 @@ class GeoLocation(ScrobblableMixin):
return f"{self.lat} x {self.lon}" return f"{self.lat} x {self.lon}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("locations:geolocation_detail", kwargs={"slug": self.uuid})
"locations:geolocation_detail", kwargs={"slug": self.uuid}
)
@property @property
def logdata_cls(self): def logdata_cls(self):
@ -101,9 +99,7 @@ class GeoLocation(ScrobblableMixin):
@property @property
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:
return ScrobblableConstants( return ScrobblableConstants(verb="Going", tags="world_map", priority="low")
verb="Going", tags="world_map", priority="low"
)
def loc_diff(self, old_lat_lon: tuple) -> tuple: def loc_diff(self, old_lat_lon: tuple) -> tuple:
return ( return (
@ -114,9 +110,7 @@ class GeoLocation(ScrobblableMixin):
def has_moved(self, previous_location: "GeoLocation") -> bool: def has_moved(self, previous_location: "GeoLocation") -> bool:
has_moved = False has_moved = False
loc_diff = self.loc_diff( loc_diff = self.loc_diff((previous_location.lat, previous_location.lon))
(previous_location.lat, previous_location.lon)
)
if loc_diff[0] > GEOLOC_PROXIMITY or loc_diff[1] > GEOLOC_PROXIMITY: if loc_diff[0] > GEOLOC_PROXIMITY or loc_diff[1] > GEOLOC_PROXIMITY:
has_moved = True has_moved = True
logger.debug( logger.debug(

View File

@ -13,9 +13,7 @@ def test_find_or_create(caplog):
@pytest.mark.django_db @pytest.mark.django_db
def test_find_or_create_truncation(): def test_find_or_create_truncation():
loc = GeoLocation.find_or_create( loc = GeoLocation.find_or_create({"lat": 44.2345, "lon": -68.2345, "alt": 60.356})
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
)
assert loc.lat == 44.234 assert loc.lat == 44.234
assert loc.lon == -68.234 assert loc.lon == -68.234
assert loc.altitude == 60 assert loc.altitude == 60
@ -25,9 +23,7 @@ def test_find_or_create_truncation():
def test_find_or_create_finds_existing(): def test_find_or_create_finds_existing():
extant = GeoLocation.objects.create(lat=44.234, lon=-68.234, altitude=50) extant = GeoLocation.objects.create(lat=44.234, lon=-68.234, altitude=50)
loc = GeoLocation.find_or_create( loc = GeoLocation.find_or_create({"lat": 44.2345, "lon": -68.2345, "alt": 60.356})
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
)
assert loc.id == extant.id assert loc.id == extant.id
@ -35,9 +31,7 @@ def test_find_or_create_finds_existing():
def test_find_or_create_creates_new(): def test_find_or_create_creates_new():
extant = GeoLocation.objects.create(lat=44.234, lon=-69.234, altitude=60) extant = GeoLocation.objects.create(lat=44.234, lon=-69.234, altitude=60)
loc = GeoLocation.find_or_create( loc = GeoLocation.find_or_create({"lat": 44.2345, "lon": -68.2345, "alt": 60.356})
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
)
assert not loc.id == extant.id assert not loc.id == extant.id
@ -47,9 +41,7 @@ def test_found_in_proximity_location():
lon = -69.234 lon = -69.234
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60) loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
close = GeoLocation.objects.create( close = GeoLocation.objects.create(lat=lat + 0.0001, lon=lon - 0.0001, altitude=60)
lat=lat + 0.0001, lon=lon - 0.0001, altitude=60
)
assert close not in loc.in_proximity(named=True) assert close not in loc.in_proximity(named=True)
assert close in loc.in_proximity() assert close in loc.in_proximity()
@ -60,9 +52,7 @@ def test_not_found_in_proximity_location():
lon = -69.234 lon = -69.234
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60) loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
far = GeoLocation.objects.create( far = GeoLocation.objects.create(lat=lat + 0.0002, lon=lon - 0.0001, altitude=60)
lat=lat + 0.0002, lon=lon - 0.0001, altitude=60
)
assert far not in loc.in_proximity() assert far not in loc.in_proximity()

View File

@ -109,9 +109,9 @@ class Artist(TimeStampedModel):
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()).order_by(
track__in=self.track_set.all() "-timestamp"
).order_by("-timestamp") )
@property @property
def tracks(self): def tracks(self):
@ -130,9 +130,7 @@ class Artist(TimeStampedModel):
if not self.allmusic_id or force: if not self.allmusic_id or force:
slug = get_allmusic_slug(self.name) slug = get_allmusic_slug(self.name)
if not slug: if not slug:
logger.info( logger.info("No allmusic link found", extra={"track_id": self.id})
"No allmusic link found", extra={"track_id": self.id}
)
return return
self.allmusic_id = slug self.allmusic_id = slug
self.save(update_fields=["allmusic_id"]) self.save(update_fields=["allmusic_id"])
@ -141,9 +139,7 @@ class Artist(TimeStampedModel):
if not self.bandcamp_id or force: if not self.bandcamp_id or force:
slug = get_bandcamp_slug(self.name) slug = get_bandcamp_slug(self.name)
if not slug: if not slug:
logger.info( logger.info("No bandcamp link found", extra={"track_id": self.id})
"No bandcamp link found", extra={"track_id": self.id}
)
return return
self.bandcamp_id = slug self.bandcamp_id = slug
self.save(update_fields=["bandcamp_id"]) self.save(update_fields=["bandcamp_id"])
@ -193,13 +189,9 @@ class Artist(TimeStampedModel):
Thus, when we find or create an artist, we should always provide an optional Thus, when we find or create an artist, we should always provide an optional
album name or track name, but probably not both.""" album name or track name, but probably not both."""
if album_name: if album_name:
logger.info( logger.info(f"Looking for artist with name {name} and album {album_name}")
f"Looking for artist with name {name} and album {album_name}"
)
if track_name: if track_name:
logger.info( logger.info(f"Looking for artist with name {name} and track {track_name}")
f"Looking for artist with name {name} and track {track_name}"
)
keys = {} keys = {}
name = clean_artist_name(name) name = clean_artist_name(name)
@ -310,9 +302,9 @@ class Album(TimeStampedModel):
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()).order_by(
track__in=self.track_set.all() "-timestamp"
).order_by("-timestamp") )
@property @property
def primary_image_url(self) -> str: def primary_image_url(self) -> str:
@ -349,9 +341,7 @@ class Album(TimeStampedModel):
if self.album_artist and (not self.allmusic_id or force): if self.album_artist and (not self.allmusic_id or force):
slug = get_allmusic_slug(self.album_artist.name, self.name) slug = get_allmusic_slug(self.album_artist.name, self.name)
if not slug: if not slug:
logger.info( logger.info(f"No allmsuic link for {self} by {self.album_artist}")
f"No allmsuic link for {self} by {self.album_artist}"
)
return return
self.allmusic_id = slug self.allmusic_id = slug
self.save(update_fields=["allmusic_id"]) self.save(update_fields=["allmusic_id"])
@ -380,9 +370,7 @@ class Album(TimeStampedModel):
try: try:
Album.objects.filter(pk=self.pk).update(**album_data) Album.objects.filter(pk=self.pk).update(**album_data)
except: except:
logger.info( logger.info(f"Could not save info for album {self} with data {album_data}")
f"Could not save info for album {self} with data {album_data}"
)
def scrape_bandcamp(self, force=False) -> None: def scrape_bandcamp(self, force=False) -> None:
if not self.bandcamp_id or force: if not self.bandcamp_id or force:
@ -404,13 +392,13 @@ class Album(TimeStampedModel):
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
][0]["artist"]["id"] ]["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]
@ -435,10 +423,7 @@ class Album(TimeStampedModel):
if not new_artist: if not new_artist:
for t in self.track_set.all(): for t in self.track_set.all():
self.artists.add(t.artist) self.artists.add(t.artist)
if ( if not self.cover_image or self.cover_image == "default-image-replace-me":
not self.cover_image
or self.cover_image == "default-image-replace-me"
):
self.fetch_artwork() self.fetch_artwork()
self.fix_album_artist() self.fix_album_artist()
self.scrape_theaudiodb() self.scrape_theaudiodb()
@ -448,20 +433,15 @@ class Album(TimeStampedModel):
if not self.cover_image and not force: if not self.cover_image and not force:
if self.musicbrainz_id: if self.musicbrainz_id:
try: try:
img_data = musicbrainzngs.get_image_front( img_data = musicbrainzngs.get_image_front(self.musicbrainz_id)
self.musicbrainz_id
)
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 (
not self.cover_image not self.cover_image or self.cover_image == "default-image-replace-me"
or self.cover_image == "default-image-replace-me"
) and self.musicbrainz_releasegroup_id: ) and self.musicbrainz_releasegroup_id:
try: try:
img_data = musicbrainzngs.get_release_group_image_front( img_data = musicbrainzngs.get_release_group_image_front(
@ -522,9 +502,7 @@ class Album(TimeStampedModel):
@classmethod @classmethod
def find_or_create(cls, name: str, artist_name: str) -> "Album": def find_or_create(cls, name: str, artist_name: str) -> "Album":
logger.info( logger.info(f"Looking for album with name {name} and artist_name {artist_name}")
f"Looking for album with name {name} and artist_name {artist_name}"
)
artist = Artist.find_or_create(artist_name, album_name=name) artist = Artist.find_or_create(artist_name, album_name=name)
album_dict = get_album_metadata_with_artist(name, artist.name) album_dict = get_album_metadata_with_artist(name, artist.name)
@ -572,9 +550,7 @@ class Album(TimeStampedModel):
if found_name and name != found_name: if found_name and name != found_name:
alt_name = name alt_name = name
album = Album.objects.filter( album = Album.objects.filter(musicbrainz_id=album_dict.get("mbid")).first()
musicbrainz_id=album_dict.get("mbid")
).first()
if not album: if not album:
year = None year = None
@ -583,9 +559,7 @@ class Album(TimeStampedModel):
album = Album.objects.create( album = Album.objects.create(
name=found_name, name=found_name,
musicbrainz_id=album_dict.get("mbid"), musicbrainz_id=album_dict.get("mbid"),
musicbrainz_releasegroup_id=album_dict.get( musicbrainz_releasegroup_id=album_dict.get("release_group_mbid"),
"release_group_mbid"
),
year=year, year=year,
album_artist=artist, album_artist=artist,
alt_names=alt_name, alt_names=alt_name,
@ -666,9 +640,7 @@ class Track(ScrobblableMixin):
album = None album = None
if album_name: if album_name:
logger.info("Looking up album for: {album_name}") logger.info("Looking up album for: {album_name}")
album = Album.find_or_create( album = Album.find_or_create(name=album_name, artist_name=artist_name)
name=album_name, artist_name=artist_name
)
artist = album.album_artist artist = album.album_artist
else: else:
artist = Artist.find_or_create(artist_name, track_name=title) artist = Artist.find_or_create(artist_name, track_name=title)
@ -709,15 +681,11 @@ class Track(ScrobblableMixin):
}, },
) )
try: try:
mbid, length = get_recording_mbid_exact( mbid, length = get_recording_mbid_exact(title, artist_name, album_name)
title, artist_name, album_name
)
except Exception: except Exception:
print("No musicbrainz result found, cannot enrich") print("No musicbrainz result found, cannot enrich")
return track return track
track.base_run_time_seconds = run_time_seconds or int( track.base_run_time_seconds = run_time_seconds or int(length / 1000)
length / 1000
)
track.musicbrainz_id = mbid track.musicbrainz_id = mbid
if commit: if commit:
track.save() track.save()

View File

@ -29,9 +29,7 @@ def lookup_album_from_mb(musicbrainz_id: str) -> dict:
"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").get("id"),
"release-group"
).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],
}, },
@ -56,13 +54,11 @@ def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
top_result = {} top_result = {}
try: try:
top_result = musicbrainzngs.search_releases( top_result = musicbrainzngs.search_releases(release_name, artist=artist_name)[
release_name, artist=artist_name "release-list"
)["release-list"][0] ][0]
except IndexError: except IndexError:
logger.info( logger.info(f"No release found on MB for {artist_name} and {release_name}")
f"No release found on MB for {artist_name} and {release_name}"
)
score = int(top_result.get("ext:score", 0)) score = int(top_result.get("ext:score", 0))
if score < 85: if score < 85:
@ -86,9 +82,7 @@ def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
def lookup_artist_from_mb(artist_name: str) -> dict: def lookup_artist_from_mb(artist_name: str) -> dict:
try: try:
top_result = musicbrainzngs.search_artists(artist=artist_name)[ top_result = musicbrainzngs.search_artists(artist=artist_name)["artist-list"][0]
"artist-list"
][0]
except IndexError: except IndexError:
return {} return {}
score = int(top_result.get("ext:score")) score = int(top_result.get("ext:score"))
@ -204,9 +198,7 @@ def get_recording_mbid_exact(
for track in tracks: for track in tracks:
if track["recording"]["title"].lower() == track_title.lower(): if track["recording"]["title"].lower() == track_title.lower():
return track["recording"]["id"], int( return track["recording"]["id"], int(track["recording"]["length"])
track["recording"]["length"]
)
raise Exception("No recording found") raise Exception("No recording found")
except musicbrainzngs.WebServiceError as e: except musicbrainzngs.WebServiceError as e:
@ -224,9 +216,7 @@ def get_artist_metadata_extended(artist_name, strict=True):
""" """
try: try:
# Step 1: Search for artist # Step 1: Search for artist
search_results = musicbrainzngs.search_artists( search_results = musicbrainzngs.search_artists(artist=artist_name, limit=5)
artist=artist_name, limit=5
)
for artist in search_results.get("artist-list", []): for artist in search_results.get("artist-list", []):
if not strict or artist["name"].lower() == artist_name.lower(): if not strict or artist["name"].lower() == artist_name.lower():
mbid = artist["id"] mbid = artist["id"]
@ -354,9 +344,7 @@ def get_album_metadata_with_artist(album_name, artist_name, strict=True):
primary_artist = release["artist-credit"][0]["artist"] primary_artist = release["artist-credit"][0]["artist"]
all_artists = [ all_artists = [
ac["artist"]["name"] ac["artist"]["name"] for ac in release["artist-credit"] if "artist" in ac
for ac in release["artist-credit"]
if "artist" in ac
] ]
artist_metadata = get_artist_metadata_brief(primary_artist["id"]) artist_metadata = get_artist_metadata_brief(primary_artist["id"])
@ -446,9 +434,7 @@ def get_track_metadata_with_artist(track_title, artist_name, strict=True):
for release in recording["release-list"]: for release in recording["release-list"]:
release_date = parse_date(release.get("date")) release_date = parse_date(release.get("date"))
if release_date: if release_date:
valid_candidates.append( valid_candidates.append((recording["id"], release, release_date))
(recording["id"], release, release_date)
)
if not valid_candidates: if not valid_candidates:
return None return None

View File

@ -6,9 +6,15 @@ import requests
from django.conf import settings from django.conf import settings
THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY") THEAUDIODB_API_KEY = getattr(settings, "THEAUDIODB_API_KEY")
ARTIST_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s=" ARTIST_SEARCH_URL = (
ARTIST_FETCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/artist.php?i=" f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.php?s="
ALBUM_SEARCH_URL = f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/searchalbum.php?s=" )
ARTIST_FETCH_URL = (
f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/artist.php?i="
)
ALBUM_SEARCH_URL = (
f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/searchalbum.php?s="
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -94,14 +100,10 @@ def lookup_album_from_tadb(name: str, artist: str) -> dict:
album_info["rateyourmusic_id"] = album.get("strRateYourMusicID") album_info["rateyourmusic_id"] = album.get("strRateYourMusicID")
if album.get("intYearReleased"): if album.get("intYearReleased"):
album_info["theaudiodb_year_released"] = float( album_info["theaudiodb_year_released"] = float(album.get("intYearReleased"))
album.get("intYearReleased")
)
if album.get("intScore"): if album.get("intScore"):
album_info["theaudiodb_score"] = float(album.get("intScore")) album_info["theaudiodb_score"] = float(album.get("intScore"))
if album.get("intScoreVotes"): if album.get("intScoreVotes"):
album_info["theaudiodb_score_votes"] = int( album_info["theaudiodb_score_votes"] = int(album.get("intScoreVotes"))
album.get("intScoreVotes")
)
return album_info return album_info

View File

@ -88,9 +88,7 @@ def condense_albums(commit: bool = False):
track.albums.add(dup_track.album) track.albums.add(dup_track.album)
# Find out if this track appears more than once # Find out if this track appears more than once
duplicates = Track.objects.filter( duplicates = Track.objects.filter(title=track.title, artist=track.artist)
title=track.title, artist=track.artist
)
if duplicates.count() > 1: if duplicates.count() > 1:
logger.info(f"Track appears more than once, condensing: {track}") logger.info(f"Track appears more than once, condensing: {track}")
@ -98,9 +96,7 @@ def condense_albums(commit: bool = False):
# Find all scrobbles # Find all scrobbles
duplicate_ids = duplicates.values_list("id", flat=True) duplicate_ids = duplicates.values_list("id", flat=True)
scrobbles = Scrobble.objects.filter(track_id__in=duplicate_ids) scrobbles = Scrobble.objects.filter(track_id__in=duplicate_ids)
logger.info( logger.info(f"Found {scrobbles.count()} scrobbles to merge onto {track}")
f"Found {scrobbles.count()} scrobbles to merge onto {track}"
)
if commit: if commit:
scrobbles.update(track=track) scrobbles.update(track=track)
track.albums.add(*list(set(albums_to_add))) track.albums.add(*list(set(albums_to_add)))

View File

@ -5,9 +5,7 @@ app_name = "people"
urlpatterns = [ urlpatterns = [
path( path("people/", views.PersonCreateUpdateView.as_view(), name="person_form"),
"people/", views.PersonCreateUpdateView.as_view(), name="person_form"
),
path( path(
"people/<int:pk>/edit/", "people/<int:pk>/edit/",
views.PersonUpdateView.as_view(), views.PersonUpdateView.as_view(),

View File

@ -39,9 +39,7 @@ class Producer(TimeStampedModel):
class Podcast(TimeStampedModel): class Podcast(TimeStampedModel):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
producer = models.ForeignKey( producer = models.ForeignKey(Producer, on_delete=models.DO_NOTHING, **BNULL)
Producer, on_delete=models.DO_NOTHING, **BNULL
)
podcastindex_id = models.CharField(max_length=100, **BNULL) podcastindex_id = models.CharField(max_length=100, **BNULL)
owner = models.CharField(max_length=150, *BNULL) owner = models.CharField(max_length=150, *BNULL)
description = models.TextField(**BNULL) description = models.TextField(**BNULL)
@ -123,9 +121,7 @@ class PodcastEpisode(ScrobblableMixin):
mopidy_uri = models.CharField(max_length=255, **BNULL) mopidy_uri = models.CharField(max_length=255, **BNULL)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid})
"podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid}
)
def __str__(self): def __str__(self):
return f"{self.title}" return f"{self.title}"

View File

@ -14,9 +14,7 @@ PODCASTINDEX_API_SECRET = getattr(settings, "PODCASTINDEX_API_SECRET")
def get_auth_headers(): def get_auth_headers():
now = int(time.time()) now = int(time.time())
hash_data = hashlib.sha1( hash_data = hashlib.sha1(
(PODCASTINDEX_API_KEY + PODCASTINDEX_API_SECRET + str(now)).encode( (PODCASTINDEX_API_KEY + PODCASTINDEX_API_SECRET + str(now)).encode("utf-8")
"utf-8"
)
).hexdigest() ).hexdigest()
return { return {

View File

@ -53,9 +53,7 @@ def fetch_metadata_from_rss(uri: str) -> dict[str, Any]:
if isinstance(feed.feed.itunes_owner, dict) if isinstance(feed.feed.itunes_owner, dict)
else feed.feed.itunes_owner else feed.feed.itunes_owner
) )
podcast_other = feed.feed.get("managingeditor") or feed.feed.get( podcast_other = feed.feed.get("managingeditor") or feed.feed.get("copyright")
"copyright"
)
except AttributeError: except AttributeError:
podcast_owner = None podcast_owner = None
podcast_other = None podcast_other = None
@ -64,9 +62,7 @@ def fetch_metadata_from_rss(uri: str) -> dict[str, Any]:
"podcast_name": getattr(feed.feed, "title", ""), "podcast_name": getattr(feed.feed, "title", ""),
# "podcast_description": getattr(feed.feed, "description", ""), # "podcast_description": getattr(feed.feed, "description", ""),
# "podcast_link": getattr(feed.feed, "link", ""), # "podcast_link": getattr(feed.feed, "link", ""),
"podcast_producer": podcast_publisher "podcast_producer": podcast_publisher or podcast_owner or podcast_other,
or podcast_owner
or podcast_other,
} }
for entry in feed.entries: for entry in feed.entries:
@ -116,8 +112,7 @@ def parse_mopidy_uri(uri: str) -> dict[str, Any]:
# Beacuse we have epsiode numbers on # Beacuse we have epsiode numbers on
podcast_data["pub_date"] = parse( podcast_data["pub_date"] = parse(
episode_str[ episode_str[
episode_num_pad : len(PODCAST_DATE_FORMAT) episode_num_pad : len(PODCAST_DATE_FORMAT) + episode_num_pad
+ episode_num_pad
] ]
) )
except ParserError: except ParserError:
@ -129,9 +124,7 @@ def parse_mopidy_uri(uri: str) -> dict[str, Any]:
if podcast_data["episode_num"]: if podcast_data["episode_num"]:
gap_to_strip += episode_num_pad gap_to_strip += episode_num_pad
podcast_data["title"] = ( podcast_data["title"] = episode_str[gap_to_strip:].replace("-", " ").strip()
episode_str[gap_to_strip:].replace("-", " ").strip()
)
return podcast_data return podcast_data

View File

@ -3,9 +3,7 @@ from datetime import datetime
import pytz import pytz
ALL_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) ALL_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
COMMON_TIMEZONE_CHOICES = tuple( COMMON_TIMEZONE_CHOICES = tuple(zip(pytz.common_timezones, pytz.common_timezones))
zip(pytz.common_timezones, pytz.common_timezones)
)
PRETTY_TIMEZONE_CHOICES = [] PRETTY_TIMEZONE_CHOICES = []
for tz in pytz.common_timezones: for tz in pytz.common_timezones:

View File

@ -5,7 +5,5 @@ app_name = "profiles"
urlpatterns = [ urlpatterns = [
path( path("settings/", views.ProfileFormView.as_view(), name="profile_settings"),
"settings/", views.ProfileFormView.as_view(), name="profile_settings"
),
] ]

View File

@ -82,15 +82,9 @@ def fix_profile_historic_timezones(profile):
profile.timezone_change_log = "" profile.timezone_change_log = ""
profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n" profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n"
profile.timezone_change_log += ( profile.timezone_change_log += f"{home_tz} - {pendulum.parse(europe_end)}\n"
f"{home_tz} - {pendulum.parse(europe_end)}\n" profile.timezone_change_log += f"{washington_tz} - {pendulum.parse(washington)}\n"
) profile.timezone_change_log += f"{home_tz} - {pendulum.parse(washington_end)}\n"
profile.timezone_change_log += (
f"{washington_tz} - {pendulum.parse(washington)}\n"
)
profile.timezone_change_log += (
f"{home_tz} - {pendulum.parse(washington_end)}\n"
)
profile.timezone_change_log += f"{camp_tz} - {pendulum.parse(camp)}\n" profile.timezone_change_log += f"{camp_tz} - {pendulum.parse(camp)}\n"
profile.timezone_change_log += f"{home_tz} - {pendulum.parse(camp_end)}\n" profile.timezone_change_log += f"{home_tz} - {pendulum.parse(camp_end)}\n"
profile.timezone_change_log += f"{summer_tz} - {pendulum.parse(summer)}\n" profile.timezone_change_log += f"{summer_tz} - {pendulum.parse(summer)}\n"

View File

@ -110,9 +110,7 @@ class Puzzle(ScrobblableMixin):
( (
manufacturer, manufacturer,
_created, _created,
) = PuzzleManufacturer.objects.get_or_create( ) = PuzzleManufacturer.objects.get_or_create(name=manufacturer_name)
name=manufacturer_name
)
puzzle_dict["manufacturer_id"] = manufacturer.id puzzle_dict["manufacturer_id"] = manufacturer.id
genres = puzzle_dict.pop("genres", None) genres = puzzle_dict.pop("genres", None)
@ -126,9 +124,7 @@ class Puzzle(ScrobblableMixin):
r = requests.get(cover_url) r = requests.get(cover_url)
if r.status_code == 200: if r.status_code == 200:
fname = f"{puzzle.title}_{puzzle.uuid}.jpg" fname = f"{puzzle.title}_{puzzle.uuid}.jpg"
puzzle.ipdb_image.save( puzzle.ipdb_image.save(fname, ContentFile(r.content), save=True)
fname, ContentFile(r.content), save=True
)
return puzzle return puzzle

View File

@ -30,9 +30,7 @@ def get_title_from_soup(soup) -> str:
def get_manufacturer_from_soup(soup) -> str: def get_manufacturer_from_soup(soup) -> str:
manufacturer = "" manufacturer = ""
try: try:
manufacturer = ( manufacturer = soup.find(class_="infobox").div.contents[0].split("|")[0].strip()
soup.find(class_="infobox").div.contents[0].split("|")[0].strip()
)
except AttributeError: except AttributeError:
pass pass
except ValueError: except ValueError:
@ -46,10 +44,7 @@ def get_pieces_count_from_soup(soup) -> int:
pieces = 0 pieces = 0
try: try:
pieces = int( pieces = int(
soup.find(class_="infobox") soup.find(class_="infobox").div.contents[0].split("|")[1].split(" ")[1]
.div.contents[0]
.split("|")[1]
.split(" ")[1]
) )
except AttributeError: except AttributeError:
pass pass
@ -63,9 +58,7 @@ def get_pieces_count_from_soup(soup) -> int:
def get_publish_year_from_soup(soup) -> int: def get_publish_year_from_soup(soup) -> int:
year = 1900 year = 1900
try: try:
year = int( year = int(soup.find(class_="infobox").div.contents[0].split("|")[2].strip())
soup.find(class_="infobox").div.contents[0].split("|")[2].strip()
)
except AttributeError: except AttributeError:
pass pass
except ValueError: except ValueError:
@ -152,9 +145,7 @@ def get_barcode_from_soup(soup) -> str:
def get_image_url_from_soup(soup) -> str: def get_image_url_from_soup(soup) -> str:
url = "" url = ""
try: try:
url = ( url = soup.find(class_="image-container").contents[0].contents[0]["src"]
soup.find(class_="image-container").contents[0].contents[0]["src"]
)
except AttributeError: except AttributeError:
pass pass
except ValueError: except ValueError:
@ -210,9 +201,7 @@ def get_dimensions_from_soup(soup) -> str:
def get_rating_from_soup(soup) -> str: def get_rating_from_soup(soup) -> str:
rating = "" rating = ""
try: try:
rating = float( rating = float(soup.find(class_="num").get_text().strip("(").strip(")"))
soup.find(class_="num").get_text().strip("(").strip(")")
)
except AttributeError: except AttributeError:
rating = None rating = None
except ValueError: except ValueError:
@ -227,9 +216,7 @@ def get_puzzle_from_ipdb_id(ipdb_id: str) -> dict:
puzzle_dict = {"ipdb_id": ipdb_id} puzzle_dict = {"ipdb_id": ipdb_id}
if response.status_code != 200: if response.status_code != 200:
logger.warn( logger.warn("Bad response from untappd.com", extra={"response": response})
"Bad response from untappd.com", extra={"response": response}
)
return puzzle_dict return puzzle_dict
soup = BeautifulSoup(response.text, "html.parser") soup = BeautifulSoup(response.text, "html.parser")

View File

@ -70,7 +70,10 @@ class KoReaderImportAdmin(ImportBaseAdmin):
@admin.register(RetroarchImport) @admin.register(RetroarchImport)
class RetroarchImportAdmin(ImportBaseAdmin): class RetroarchImportAdmin(ImportBaseAdmin):
... ...
class RetroarchImportAdmin(ImportBaseAdmin): ...
class RetroarchImportAdmin(ImportBaseAdmin):
...
@admin.register(Genre) @admin.register(Genre)

View File

@ -63,9 +63,7 @@ class KoReaderImportSerializer(serializers.HyperlinkedModelSerializer):
fields = "__all__" fields = "__all__"
class AudioScrobblerTSVImportSerializer( class AudioScrobblerTSVImportSerializer(serializers.HyperlinkedModelSerializer):
serializers.HyperlinkedModelSerializer
):
class Meta: class Meta:
model = AudioScrobblerTSVImport model = AudioScrobblerTSVImport
fields = "__all__" fields = "__all__"

View File

@ -13,9 +13,7 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"):
if start_date: if start_date:
end_query = Q(timestamp__lte=end_date) end_query = Q(timestamp__lte=end_date)
scrobble_qs = Scrobble.objects.filter( scrobble_qs = Scrobble.objects.filter(start_query, end_query, track__isnull=False)
start_query, end_query, track__isnull=False
)
headers = [] headers = []
extension = "tsv" extension = "tsv"
delimiter = "\t" delimiter = "\t"

View File

@ -56,9 +56,7 @@ def django_form_field_from_type(field_type, required=True):
if type(None) in args: if type(None) in args:
required = False required = False
non_none_type = [arg for arg in args if arg is not type(None)][0] non_none_type = [arg for arg in args if arg is not type(None)][0]
return django_form_field_from_type( return django_form_field_from_type(non_none_type, required=required)
non_none_type, required=required
)
# Determine actual type # Determine actual type
base_type = origin if origin else field_type base_type = origin if origin else field_type
@ -84,9 +82,7 @@ def form_from_dataclass(dataclass):
continue continue
required = f.default is None and f.default_factory is None required = f.default is None and f.default_factory is None
form_fields[f.name] = django_form_field_from_type( form_fields[f.name] = django_form_field_from_type(f.type, required=required)
f.type, required=required
)
if f.name in dataclass._excluded_fields: if f.name in dataclass._excluded_fields:
form_fields[f.name].disabled = True form_fields[f.name].disabled = True
@ -101,9 +97,7 @@ def form_from_dataclass(dataclass):
def clean_notes(self): def clean_notes(self):
notes_str = self.cleaned_data.get("notes", "") notes_str = self.cleaned_data.get("notes", "")
return [ return [line.strip() for line in notes_str.splitlines() if line.strip()]
line.strip() for line in notes_str.splitlines() if line.strip()
]
form_cls.clean_notes = clean_notes form_cls.clean_notes = clean_notes
return form_cls return form_cls

View File

@ -43,9 +43,7 @@ def import_scrobbles_from_imap() -> list[Scrobble]:
try: try:
message = email.message_from_bytes(msg_data[0][1]) message = email.message_from_bytes(msg_data[0][1])
logger.info( logger.info("Processing email message", extra={"email_msg": message})
"Processing email message", extra={"email_msg": message}
)
except IndexError: except IndexError:
logger.info("No email message data found") logger.info("No email message data found")
return return
@ -75,9 +73,7 @@ def import_scrobbles_from_imap() -> list[Scrobble]:
if filename.lower().endswith(".bgsplay"): if filename.lower().endswith(".bgsplay"):
# TODO Pull this out into a parse_pgsplay function # TODO Pull this out into a parse_pgsplay function
try: try:
parsed_json = json.loads( parsed_json = json.loads(file_data.decode("utf-8"))
file_data.decode("utf-8")
)
except Exception as e: except Exception as e:
logger.error( logger.error(
"Failed to parse JSON file", "Failed to parse JSON file",

View File

@ -54,9 +54,7 @@ class LastFM:
lfm_scrobble.get("timestamp") lfm_scrobble.get("timestamp")
) )
timestamp = lfm_scrobble.get("timestamp") timestamp = lfm_scrobble.get("timestamp")
stop_timestamp = timestamp + timedelta( stop_timestamp = timestamp + timedelta(seconds=track.run_time_seconds)
seconds=track.run_time_seconds
)
new_scrobble = Scrobble( new_scrobble = Scrobble(
user=self.vrobbler_user, user=self.vrobbler_user,
timestamp=timestamp, timestamp=timestamp,
@ -149,13 +147,11 @@ class LastFM:
# TODO figure out if this will actually work # TODO figure out if this will actually work
# timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC) # timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC)
timestamp = datetime.utcfromtimestamp( timestamp = datetime.utcfromtimestamp(int(scrobble.timestamp)).replace(
int(scrobble.timestamp) tzinfo=pytz.utc
).replace(tzinfo=pytz.utc)
logger.info(
f"Scrobble appended to list for bulk create", extra=log_dict
) )
logger.info(f"Scrobble appended to list for bulk create", extra=log_dict)
scrobbles.append( scrobbles.append(
{ {
"artist": artist, "artist": artist,

View File

@ -20,9 +20,7 @@ def import_from_webdav_for_all_users(restart=False):
webdav_pass__isnull=False, webdav_pass__isnull=False,
webdav_auto_import=True, webdav_auto_import=True,
).values_list("user_id", flat=True) ).values_list("user_id", flat=True)
logger.info( logger.info(f"Start import of {webdav_enabled_user_ids.count()} webdav accounts")
f"Start import of {webdav_enabled_user_ids.count()} webdav accounts"
)
koreader_import_count = 0 koreader_import_count = 0

View File

@ -24,6 +24,4 @@ class Command(BaseCommand):
print(f"Deleted {scrobbles_found} zombie scrobbles") print(f"Deleted {scrobbles_found} zombie scrobbles")
return return
print( print(f"Found {scrobbles_found} zombie scrobbles, use --delete to remove them")
f"Found {scrobbles_found} zombie scrobbles, use --delete to remove them"
)

View File

@ -41,8 +41,6 @@ class Command(BaseCommand):
if not dry_run: if not dry_run:
scrobble.save(update_fields=["scrobble_log"]) scrobble.save(update_fields=["scrobble_log"])
else: else:
print( print(f"Scrobble {scrobble} scrobble_log updated to {old_data}")
f"Scrobble {scrobble} scrobble_log updated to {old_data}"
)
print(f"Migrated scrobble logs for {updated_scrobble_count} scrobbles") print(f"Migrated scrobble logs for {updated_scrobble_count} scrobbles")

View File

@ -42,9 +42,7 @@ class ScrobbleNtfyNotification(ScrobbleNotification):
def __init__(self, scrobble, **kwargs): def __init__(self, scrobble, **kwargs):
super().__init__(scrobble) super().__init__(scrobble)
self.ntfy_str: str = f"{self.scrobble.media_obj}" self.ntfy_str: str = f"{self.scrobble.media_obj}"
self.click_url = self.url_tmpl.format( self.click_url = self.url_tmpl.format(path=self.media_obj.get_absolute_url())
path=self.media_obj.get_absolute_url()
)
self.title = self.media_obj.strings.verb self.title = self.media_obj.strings.verb
self.actions = "" self.actions = ""
if kwargs.get("end", False): if kwargs.get("end", False):
@ -89,11 +87,7 @@ class MoodNtfyNotification(BasicNtfyNotification):
self.title = "Mood Check-in!" self.title = "Mood Check-in!"
def send(self): def send(self):
if ( if self.profile and self.profile.ntfy_enabled and self.profile.ntfy_url:
self.profile
and self.profile.ntfy_enabled
and self.profile.ntfy_url
):
requests.post( requests.post(
self.profile.ntfy_url, self.profile.ntfy_url,
data=self.ntfy_str.encode(encoding="utf-8"), data=self.ntfy_str.encode(encoding="utf-8"),

View File

@ -37,9 +37,7 @@ User = get_user_model()
def timestamp_user_tz_to_utc(timestamp: int, user_tz: ZoneInfo) -> datetime: def timestamp_user_tz_to_utc(timestamp: int, user_tz: ZoneInfo) -> datetime:
return user_tz.localize(datetime.utcfromtimestamp(timestamp)).astimezone( return user_tz.localize(datetime.utcfromtimestamp(timestamp)).astimezone(pytz.utc)
pytz.utc
)
def convert_to_seconds(run_time: str) -> int: def convert_to_seconds(run_time: str) -> int:
@ -150,9 +148,7 @@ def import_lastfm_for_all_users(restart=False):
) )
continue continue
lfm_client = LastFM( lfm_client = LastFM(user=get_user_model().objects.filter(id=user_id).first())
user=get_user_model().objects.filter(id=user_id).first()
)
has_scrobbles = lfm_client.get_last_scrobbles( has_scrobbles = lfm_client.get_last_scrobbles(
time_from=last_processed, check=True time_from=last_processed, check=True
@ -237,9 +233,7 @@ def import_from_webdav_for_all_users(restart=False):
webdav_pass__isnull=False, webdav_pass__isnull=False,
webdav_auto_import=True, webdav_auto_import=True,
).values_list("user_id", flat=True) ).values_list("user_id", flat=True)
logger.info( logger.info(f"start import of {webdav_enabled_user_ids.count()} webdav accounts")
f"start import of {webdav_enabled_user_ids.count()} webdav accounts"
)
koreader_import_count = 0 koreader_import_count = 0
@ -348,11 +342,7 @@ def send_mood_checkin_reminders() -> int:
def extract_domain(url): def extract_domain(url):
parsed_url = urlparse(url) parsed_url = urlparse(url)
domain = ( domain = parsed_url.netloc.split(".")[-2] + "." + parsed_url.netloc.split(".")[-1]
parsed_url.netloc.split(".")[-2]
+ "."
+ parsed_url.netloc.split(".")[-1]
)
return domain return domain
@ -367,20 +357,14 @@ def fix_playback_position_seconds(
) )
continue continue
if ( if scrobble.media_type == "Track" and scrobble.media_obj.run_time_seconds:
scrobble.media_type == "Track"
and scrobble.media_obj.run_time_seconds
):
too_long = ( too_long = (
scrobble.playback_position_seconds scrobble.playback_position_seconds > scrobble.media_obj.run_time_seconds
> scrobble.media_obj.run_time_seconds
) )
zero = scrobble.playback_position_seconds == 0 zero = scrobble.playback_position_seconds == 0
null = not scrobble.playback_position_seconds null = not scrobble.playback_position_seconds
if too_long or zero or null: if too_long or zero or null:
scrobble.playback_position_seconds = ( scrobble.playback_position_seconds = scrobble.media_obj.run_time_seconds
scrobble.media_obj.run_time_seconds
)
updated_scrobbles.append(scrobble) updated_scrobbles.append(scrobble)
if commit: if commit:
scrobble.save(update_fields=["playback_position_seconds"]) scrobble.save(update_fields=["playback_position_seconds"])

View File

@ -43,9 +43,7 @@ class Sport(TheSportsDbMixin):
@property @property
def default_event_run_time(self): def default_event_run_time(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_seconds: if self.default_event_run_time_seconds:
default_run_time = self.default_event_run_time_seconds default_run_time = self.default_event_run_time_seconds
return default_run_time return default_run_time
@ -124,7 +122,9 @@ class SportEvent(ScrobblableMixin):
) )
def __str__(self): def __str__(self):
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})
@ -206,14 +206,10 @@ class SportEvent(ScrobblableMixin):
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(name__icontains=players_list[0]).first()
name__icontains=players_list[0]
).first()
if not player_one: if not player_one:
player_one = Player.objects.create(name=players_list[0]) player_one = Player.objects.create(name=players_list[0])
player_two = Player.objects.filter( player_two = Player.objects.filter(name__icontains=players_list[1]).first()
name__icontains=players_list[1]
).first()
if not player_two: if not player_two:
player_two = Player.objects.create(name=players_list[1]) player_two = Player.objects.create(name=players_list[1])

View File

@ -21,9 +21,7 @@ def lookup_event_from_thesportsdb(event_id: str) -> dict:
if not event or type(event) != dict: if not event or type(event) != dict:
return {} return {}
sport, _created = Sport.objects.get_or_create( sport, _created = Sport.objects.get_or_create(thesportsdb_id=event.get("strSport"))
thesportsdb_id=event.get("strSport")
)
try: try:
start = parse(event.get("strTimestamp")) start = parse(event.get("strTimestamp"))

View File

@ -29,9 +29,7 @@ def get_title_from_labels(
def convert_old_orgmode_log_to_new(commit=False): def convert_old_orgmode_log_to_new(commit=False):
scrobbles = Scrobble.objects.filter( scrobbles = Scrobble.objects.filter(source="Org-mode", log__has_key="drawers")
source="Org-mode", log__has_key="drawers"
)
for scrobble in scrobbles: for scrobble in scrobbles:
scrobble.log["title"] = scrobble.log.pop("description") scrobble.log["title"] = scrobble.log.pop("description")
scrobble.log["description"] = scrobble.log.pop("details") scrobble.log["description"] = scrobble.log.pop("details")
@ -49,9 +47,7 @@ def convert_old_orgmode_log_to_new(commit=False):
def convert_old_todoist_log_to_new(commit=False): def convert_old_todoist_log_to_new(commit=False):
scrobbles = Scrobble.objects.filter( scrobbles = Scrobble.objects.filter(source="Todoist", log__has_key="todoist_type")
source="Todoist", log__has_key="todoist_type"
)
for scrobble in scrobbles: for scrobble in scrobbles:
scrobble.log["title"] = scrobble.log.pop("description") scrobble.log["title"] = scrobble.log.pop("description")
scrobble.log["description"] = scrobble.log.pop("details") scrobble.log["description"] = scrobble.log.pop("details")
@ -94,9 +90,7 @@ def convert_notes_to_dict(commit=False):
def convert_old_boardgame_log_to_new(commit=False): def convert_old_boardgame_log_to_new(commit=False):
scrobbles = Scrobble.objects.filter( scrobbles = Scrobble.objects.filter(board_game__isnull=False, log__has_key="notes")
board_game__isnull=False, log__has_key="notes"
)
for scrobble in scrobbles: for scrobble in scrobbles:
if isinstance(scrobble.log.get("notes"), str): if isinstance(scrobble.log.get("notes"), str):
scrobble.log["notes"] = [scrobble.log.pop("notes")] scrobble.log["notes"] = [scrobble.log.pop("notes")]

View File

@ -34,20 +34,14 @@ def todoist_webhook(request):
is_note_type = todoist_tyllll = "note" is_note_type = todoist_tyllll = "note"
new_labels = event_data.get("labels", []) new_labels = event_data.get("labels", [])
old_labels = ( old_labels = (
post_data.get("event_data_extra", {}) post_data.get("event_data_extra", {}).get("old_item", {}).get("labels", [])
.get("old_item", {})
.get("labels", [])
) )
# TODO Don't hard code status strings in here # TODO Don't hard code status strings in here
is_updated = todoist_event in ["updated"] is_updated = todoist_event in ["updated"]
is_added = todoist_event in ["added"] is_added = todoist_event in ["added"]
task_started = ( task_started = "inprogress" in new_labels and "inprogress" not in old_labels
"inprogress" in new_labels and "inprogress" not in old_labels task_stopped = "inprogress" not in new_labels and "inprogress" in old_labels
)
task_stopped = (
"inprogress" not in new_labels and "inprogress" in old_labels
)
if is_item_type and is_updated and (task_started or task_stopped): if is_item_type and is_updated and (task_started or task_stopped):
todoist_task = { todoist_task = {
@ -71,9 +65,7 @@ def todoist_webhook(request):
"updated_at": task_data.get("updated_at"), "updated_at": task_data.get("updated_at"),
"details": task_data.get("description"), "details": task_data.get("description"),
"notes": event_data.get("content"), "notes": event_data.get("content"),
"is_deleted": ( "is_deleted": (True if event_data.get("is_deleted") == "true" else False),
True if event_data.get("is_deleted") == "true" else False
),
} }
if (is_added and not todoist_note) or (is_updated and not todoist_task): if (is_added and not todoist_note) or (is_updated and not todoist_task):
@ -104,9 +96,7 @@ def todoist_webhook(request):
) )
if todoist_note: if todoist_note:
scrobble = todoist_scrobble_update_task( scrobble = todoist_scrobble_update_task(todoist_note, user_profile.user_id)
todoist_note, user_profile.user_id
)
if not scrobble: if not scrobble:
logger.info( logger.info(

View File

@ -72,6 +72,6 @@ class Trail(ScrobblableMixin):
def scrobbles(self, user_id): def scrobbles(self, user_id):
Scrobble = apps.get_model("scrobbles", "Scrobble") Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter( return Scrobble.objects.filter(user_id=user_id, life_event=self).order_by(
user_id=user_id, life_event=self "-timestamp"
).order_by("-timestamp") )

View File

@ -37,9 +37,7 @@ def lookup_game_from_hltb(name_or_id: str) -> Optional[dict]:
found_games = [] found_games = []
for g in results: for g in results:
found_games.append(f"{g.game_name} ({g.game_id})") found_games.append(f"{g.game_name} ({g.game_id})")
logger.info( logger.info(f"Found more than one match {found_games}, taking {hltb_game}")
f"Found more than one match {found_games}, taking {hltb_game}"
)
game_dict = { game_dict = {
"title": hltb_game.game_name, "title": hltb_game.game_name,

View File

@ -29,9 +29,7 @@ User = get_user_model()
def get_igdb_token() -> str: def get_igdb_token() -> str:
token_url = REFRESH_TOKEN_URL.format( token_url = REFRESH_TOKEN_URL.format(id=IGDB_CLIENT_ID, secret=IGDB_CLIENT_SECRET)
id=IGDB_CLIENT_ID, secret=IGDB_CLIENT_SECRET
)
response = requests.post(token_url) response = requests.post(token_url)
results = json.loads(response.content) results = json.loads(response.content)
return results.get("access_token") return results.get("access_token")
@ -100,9 +98,9 @@ def lookup_game_from_igdb(name_or_igdb_id: str) -> Dict:
alt_name = game.get("alternative_names")[0].get("name") alt_name = game.get("alternative_names")[0].get("name")
screenshot_url = None screenshot_url = None
if "screenshots" in game.keys(): if "screenshots" in game.keys():
screenshot_url = "https:" + game.get("screenshots")[0].get( screenshot_url = "https:" + game.get("screenshots")[0].get("url").replace(
"url" "t_thumb", "t_screenshot_big_2x"
).replace("t_thumb", "t_screenshot_big_2x") )
cover_url = None cover_url = None
if "cover" in game.keys(): if "cover" in game.keys():
cover_url = "https:" + game.get("cover").get("url").replace( cover_url = "https:" + game.get("cover").get("url").replace(

View File

@ -58,9 +58,7 @@ class VideoGamePlatform(TimeStampedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("videogames:platform_detail", kwargs={"slug": self.uuid})
"videogames:platform_detail", kwargs={"slug": self.uuid}
)
class VideoGameCollection(TimeStampedModel): class VideoGameCollection(TimeStampedModel):
@ -85,9 +83,7 @@ class VideoGameCollection(TimeStampedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("videogames:collection_detail", kwargs={"slug": self.uuid})
"videogames:collection_detail", kwargs={"slug": self.uuid}
)
class VideoGame(LongPlayScrobblableMixin): class VideoGame(LongPlayScrobblableMixin):
@ -188,9 +184,7 @@ class VideoGame(LongPlayScrobblableMixin):
return url return url
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("videogames:videogame_detail", kwargs={"slug": self.uuid})
"videogames:videogame_detail", kwargs={"slug": self.uuid}
)
def hltb_link(self): def hltb_link(self):
return f"https://howlongtobeat.com/game/{self.hltb_id}" return f"https://howlongtobeat.com/game/{self.hltb_id}"

View File

@ -98,9 +98,7 @@ def import_retroarch_lrtl_files(playlog_path: str, user_id: int) -> List[dict]:
logger.warning(f"User ID {user_id} is not valid, cannot scrobble") logger.warning(f"User ID {user_id} is not valid, cannot scrobble")
raise UserNotFound raise UserNotFound
game_logs = load_game_data( game_logs = load_game_data(playlog_path, pytz.timezone(user.profile.timezone))
playlog_path, pytz.timezone(user.profile.timezone)
)
found_game = None found_game = None
new_scrobbles = [] new_scrobbles = []
@ -130,9 +128,7 @@ def import_retroarch_lrtl_files(playlog_path: str, user_id: int) -> List[dict]:
# Found a game, check if scrobble exists # Found a game, check if scrobble exists
end_datetime = game_data.get("last_played") end_datetime = game_data.get("last_played")
found_scrobble = found_game.scrobble_set.filter( found_scrobble = found_game.scrobble_set.filter(stop_timestamp=end_datetime)
stop_timestamp=end_datetime
)
if found_scrobble: if found_scrobble:
logger.info(f"Skipping scrobble for game {found_game.id}") logger.info(f"Skipping scrobble for game {found_game.id}")
continue continue

View File

@ -8,7 +8,9 @@ from vrobbler.apps.videogames.exceptions import GameNotFound
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAME_LOOKUP_URL = "http://adb.arcadeitalia.net/dettaglio_mame.php?game_name={query}&search_id=2" MAME_LOOKUP_URL = (
"http://adb.arcadeitalia.net/dettaglio_mame.php?game_name={query}&search_id=2"
)
def _strip_and_clean(text): def _strip_and_clean(text):

View File

@ -32,9 +32,7 @@ def get_or_create_videogame(
platforms = game_dict.get("platforms", []) platforms = game_dict.get("platforms", [])
if platforms: if platforms:
for platform in game_dict.get("platforms", []): for platform in game_dict.get("platforms", []):
p, _created = VideoGamePlatform.objects.get_or_create( p, _created = VideoGamePlatform.objects.get_or_create(name=platform)
name=platform
)
platform_ids.append(p.id) platform_ids.append(p.id)
game_dict.pop("platforms") game_dict.pop("platforms")
@ -98,9 +96,7 @@ def get_or_create_videogame(
return game return game
def load_game_data_from_igdb( def load_game_data_from_igdb(game_id: int, igdb_id: str = "") -> Optional[VideoGame]:
game_id: int, igdb_id: str = ""
) -> Optional[VideoGame]:
"""Look up game, if it doesn't exist, lookup data from igdb""" """Look up game, if it doesn't exist, lookup data from igdb"""
game = VideoGame.objects.filter(id=game_id).first() game = VideoGame.objects.filter(id=game_id).first()
if not game: if not game:

View File

@ -15,9 +15,7 @@ class SeriesSerializer(serializers.HyperlinkedModelSerializer):
class VideoSerializer(serializers.HyperlinkedModelSerializer): class VideoSerializer(serializers.HyperlinkedModelSerializer):
channel = serializers.PrimaryKeyRelatedField( channel = serializers.PrimaryKeyRelatedField(queryset=Channel.objects.all())
queryset=Channel.objects.all()
)
tv_series = serializers.PrimaryKeyRelatedField( tv_series = serializers.PrimaryKeyRelatedField(
queryset=Series.objects.all(), required=False, allow_null=True queryset=Series.objects.all(), required=False, allow_null=True
) )

View File

@ -8,22 +8,18 @@ class Command(BaseCommand):
help = "Find or create a Video by ID and output it as JSON" help = "Find or create a Video by ID and output it as JSON"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument("video_id", type=str, help="The video ID to find or create")
"video_id", type=str, help="The video ID to find or create"
)
def handle(self, *args, **options): def handle(self, *args, **options):
instance = Video.find_or_create( instance = Video.find_or_create(options.get("video_id", ""), overwrite=True)
options.get("video_id", ""), overwrite=True
)
data = json.loads(serializers.serialize("json", [instance]))[0] data = json.loads(serializers.serialize("json", [instance]))[0]
# --- Enrich with series model --- # --- Enrich with series model ---
if instance.tv_series_id: if instance.tv_series_id:
series_instance = instance.tv_series series_instance = instance.tv_series
series_json = json.loads( series_json = json.loads(serializers.serialize("json", [series_instance]))[
serializers.serialize("json", [series_instance]) 0
)[0] ]
data["series"] = series_json # new nested field data["series"] = series_json # new nested field
if instance.channel_id: if instance.channel_id:

View File

@ -7,9 +7,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
USER_AGENT = ( USER_AGENT = "Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
"Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
)
SKATEVIDEOSITE_URL = "https://www.skatevideosite.com" SKATEVIDEOSITE_URL = "https://www.skatevideosite.com"
SKATEVIDEOSITE_SEARCH_URL = SKATEVIDEOSITE_URL + "/search/?q={title}" SKATEVIDEOSITE_SEARCH_URL = SKATEVIDEOSITE_URL + "/search/?q={title}"

View File

@ -18,9 +18,7 @@ TMDB_IMAGE_URL = "https://image.tmdb.org/t/p/original"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def lookup_video_from_tmdb( def lookup_video_from_tmdb(name_or_id: str, kind: str = "movie") -> VideoMetadata:
name_or_id: str, kind: str = "movie"
) -> VideoMetadata:
from videos.models import Series from videos.models import Series
imdb_id = name_or_id imdb_id = name_or_id

View File

@ -26,14 +26,10 @@ def lookup_video_from_youtube(youtube_id: str) -> VideoMetadata:
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
if response.status_code != 200: if response.status_code != 200:
logger.warning( logger.warning("Bad response from Google", extra={"response": response})
"Bad response from Google", extra={"response": response}
)
return video_metadata return video_metadata
yt_metadata = ( yt_metadata = json.loads(response.content).get("items", [None])[0].get("snippet")
json.loads(response.content).get("items", [None])[0].get("snippet")
)
duration_iso8601 = ( duration_iso8601 = (
json.loads(response.content) json.loads(response.content)
.get("items", [None])[0] .get("items", [None])[0]

View File

@ -19,9 +19,7 @@ def clean_up_videos():
try: try:
video.save(update_fields=["imdb_id"]) video.save(update_fields=["imdb_id"])
except IntegrityError: except IntegrityError:
new_video = Video.objects.filter( new_video = Video.objects.filter(imdb_id="tt" + video.imdb_id).first()
imdb_id="tt" + video.imdb_id
).first()
video.scrobble_set.all().update(video=new_video) video.scrobble_set.all().update(video=new_video)
video.delete() video.delete()

View File

@ -32,9 +32,7 @@ class SeriesDetailView(LoginRequiredMixin, ChartContextMixin, generic.DetailView
context_data = super().get_context_data(**kwargs) context_data = super().get_context_data(**kwargs)
context_data["scrobbles"] = self.object.scrobbles_for_user(user_id) context_data["scrobbles"] = self.object.scrobbles_for_user(user_id)
next_episode_id = ( next_episode_id = self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
)
if self.object.is_episode_playing(user_id): if self.object.is_episode_playing(user_id):
next_episode_id = "" next_episode_id = ""
if next_episode_id: if next_episode_id:
@ -96,10 +94,7 @@ class VideoListView(ScrobbleableListView):
if channel: if channel:
channels_this_week[channel.id] = { channels_this_week[channel.id] = {
"channel": channel, "channel": channel,
"count": channels_this_week.get(channel.id, {}).get( "count": channels_this_week.get(channel.id, {}).get("count", 0) + 1,
"count", 0
)
+ 1,
} }
channels_this_month = {} channels_this_month = {}
@ -108,9 +103,7 @@ class VideoListView(ScrobbleableListView):
if channel: if channel:
channels_this_month[channel.id] = { channels_this_month[channel.id] = {
"channel": channel, "channel": channel,
"count": channels_this_month.get(channel.id, {}).get( "count": channels_this_month.get(channel.id, {}).get("count", 0)
"count", 0
)
+ 1, + 1,
} }

View File

@ -39,9 +39,9 @@ class Domain(TimeStampedModel):
def scrobbles_for_user(self, user_id): def scrobbles_for_user(self, user_id):
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
return Scrobble.objects.filter( return Scrobble.objects.filter(web_page__domain=self, user_id=user_id).order_by(
web_page__domain=self, user_id=user_id "-timestamp"
).order_by("-timestamp") )
class WebPage(ScrobblableMixin): class WebPage(ScrobblableMixin):
@ -136,9 +136,7 @@ class WebPage(ScrobblableMixin):
def scrobbles(self, user): def scrobbles(self, user):
Scrobble = apps.get_model("scrobbles", "Scrobble") Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(user=user, web_page=self).order_by( return Scrobble.objects.filter(user=user, web_page=self).order_by("-timestamp")
"-timestamp"
)
def clean_title(self, title: str, save=True): def clean_title(self, title: str, save=True):
if len(title.split("|")) > 1: if len(title.split("|")) > 1:
@ -213,9 +211,7 @@ class WebPage(ScrobblableMixin):
return return
if response.status_code == 200: if response.status_code == 200:
logger.info( logger.info("Website already exists in archive", extra={"url": self.url})
"Website already exists in archive", extra={"url": self.url}
)
else: else:
raise Exception( raise Exception(
f"Failed to push URL to archivebox (Response {response.status_code})" f"Failed to push URL to archivebox (Response {response.status_code})"

View File

@ -40,10 +40,7 @@ class WebPageListView(ScrobbleableListView):
if domain: if domain:
domains_this_week[domain.id] = { domains_this_week[domain.id] = {
"domain": domain, "domain": domain,
"count": domains_this_week.get(domain.id, {}).get( "count": domains_this_week.get(domain.id, {}).get("count", 0) + 1,
"count", 0
)
+ 1,
} }
domains_this_month = {} domains_this_month = {}
@ -52,10 +49,7 @@ class WebPageListView(ScrobbleableListView):
if domain: if domain:
domains_this_month[domain.id] = { domains_this_month[domain.id] = {
"domain": domain, "domain": domain,
"count": domains_this_month.get(domain.id, {}).get( "count": domains_this_month.get(domain.id, {}).get("count", 0) + 1,
"count", 0
)
+ 1,
} }
context_data["domains_this_week"] = sorted( context_data["domains_this_week"] = sorted(
@ -82,9 +76,7 @@ class WebPageDetailView(ScrobbleableDetailView):
return context return context
class WebPageReadView( class WebPageReadView(LoginRequiredMixin, generic.edit.FormView, generic.DetailView):
LoginRequiredMixin, generic.edit.FormView, generic.DetailView
):
model = WebPage model = WebPage
slug_field = "uuid" slug_field = "uuid"
template_name = "webpages/webpage_read.html" template_name = "webpages/webpage_read.html"
@ -95,11 +87,7 @@ class WebPageReadView(
webpage = WebPage.objects.get(uuid=kwargs.get("slug")) webpage = WebPage.objects.get(uuid=kwargs.get("slug"))
latest_scrobble = webpage.scrobbles(user).last() latest_scrobble = webpage.scrobbles(user).last()
if latest_scrobble.played_to_completion: if latest_scrobble.played_to_completion:
redirect( redirect(reverse("webpages:webpage_detail", kwargs={"slug": webpage.uuid}))
reverse(
"webpages:webpage_detail", kwargs={"slug": webpage.uuid}
)
)
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
def form_valid(self, *args): def form_valid(self, *args):

View File

@ -19,9 +19,7 @@ class HealthCheckMiddleware:
is_db_connected = False is_db_connected = False
else: else:
is_db_connected = True is_db_connected = True
logger.info( logger.info("[health-check]", extra={"is_db_connected": is_db_connected})
"[health-check]", extra={"is_db_connected": is_db_connected}
)
if is_db_connected: if is_db_connected:
return HttpResponse("ok") return HttpResponse("ok")
return self.get_response(request) return self.get_response(request)

View File

@ -37,9 +37,7 @@ TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
TAGGIT_CASE_INSENSITIVE = True TAGGIT_CASE_INSENSITIVE = True
KEEP_DETAILED_SCROBBLE_LOGS = os.getenv( KEEP_DETAILED_SCROBBLE_LOGS = os.getenv("VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False)
"VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False
)
# Key must be 16, 24 or 32 bytes long and will be converted to a byte stream # Key must be 16, 24 or 32 bytes long and will be converted to a byte stream
ENCRYPTED_FIELD_KEY = os.getenv( ENCRYPTED_FIELD_KEY = os.getenv(
@ -54,9 +52,7 @@ DELETE_STALE_SCROBBLES = (
) )
# Used to dump data coming from srobbling sources, helpful for building new inputs # Used to dump data coming from srobbling sources, helpful for building new inputs
DUMP_REQUEST_DATA = ( DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", "false").lower() in TRUTHY
os.getenv("VROBBLER_DUMP_REQUEST_DATA", "false").lower() in TRUTHY
)
USDA_API_KEY = os.getenv("VROBBLER_USDA_API_KEY") USDA_API_KEY = os.getenv("VROBBLER_USDA_API_KEY")
THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2") THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2")
@ -72,9 +68,7 @@ COMICVINE_API_KEY = os.getenv("VROBBLER_COMICVINE_API_KEY")
BGG_ACCESS_TOKEN = os.getenv("VROBBLER_BGG_ACCESS_TOKEN", "") BGG_ACCESS_TOKEN = os.getenv("VROBBLER_BGG_ACCESS_TOKEN", "")
GEOLOC_ACCURACY = os.getenv("VROBBLER_GEOLOC_ACCURACY", 3) GEOLOC_ACCURACY = os.getenv("VROBBLER_GEOLOC_ACCURACY", 3)
GEOLOC_PROXIMITY = os.getenv("VROBBLER_GEOLOC_PROXIMITY", "0.0001") GEOLOC_PROXIMITY = os.getenv("VROBBLER_GEOLOC_PROXIMITY", "0.0001")
POINTS_FOR_MOVEMENT_HISTORY = os.getenv( POINTS_FOR_MOVEMENT_HISTORY = os.getenv("VROBBLER_POINTS_FOR_MOVEMENT_HISTORY", 3)
"VROBBLER_POINTS_FOR_MOVEMENT_HISTORY", 3
)
TODOIST_CLIENT_ID = os.getenv("VROBBLER_TODOIST_CLIENT_ID", "") TODOIST_CLIENT_ID = os.getenv("VROBBLER_TODOIST_CLIENT_ID", "")
TODOIST_CLIENT_SECRET = os.getenv("VROBBLER_TODOIST_CLIENT_SECRET", "") TODOIST_CLIENT_SECRET = os.getenv("VROBBLER_TODOIST_CLIENT_SECRET", "")
@ -97,9 +91,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "America/New_York") TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "America/New_York")
ALLOWED_HOSTS = ["*"] ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [os.getenv("VROBBLER_TRUSTED_ORIGINS", "http://localhost:8000")]
os.getenv("VROBBLER_TRUSTED_ORIGINS", "http://localhost:8000")
]
X_FRAME_OPTIONS = "SAMEORIGIN" X_FRAME_OPTIONS = "SAMEORIGIN"
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None) REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
@ -108,9 +100,7 @@ if REDIS_URL:
else: else:
print("Eagerly running all tasks") print("Eagerly running all tasks")
CELERY_TASK_ALWAYS_EAGER = ( CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", "false").lower() in TRUTHY
os.getenv("VROBBLER_SKIP_CELERY", "false").lower() in TRUTHY
)
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/" CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
CELERY_RESULT_BACKEND = "django-db" CELERY_RESULT_BACKEND = "django-db"
CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "America/New_York") CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "America/New_York")
@ -231,9 +221,7 @@ DATABASES = {
} }
if TESTING: if TESTING:
DATABASES = { DATABASES = {"default": dj_database_url.config(default="sqlite:///testdb.sqlite3")}
"default": dj_database_url.config(default="sqlite:///testdb.sqlite3")
}
db_str = "" db_str = ""
if "sqlite" in DATABASES["default"]["ENGINE"]: if "sqlite" in DATABASES["default"]["ENGINE"]:
@ -269,9 +257,7 @@ REST_FRAMEWORK = {
"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"
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 200, "PAGE_SIZE": 200,
} }
@ -331,9 +317,7 @@ else:
STATIC_ROOT = os.getenv( STATIC_ROOT = os.getenv(
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static") "VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
) )
MEDIA_ROOT = os.getenv( MEDIA_ROOT = os.getenv("VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media"))
"VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media")
)
STATIC_URL = os.getenv("VROBBLER_STATIC_URL", "/static/") STATIC_URL = os.getenv("VROBBLER_STATIC_URL", "/static/")
MEDIA_URL = os.getenv("VROBBLER_MEDIA_URL", "/media/") MEDIA_URL = os.getenv("VROBBLER_MEDIA_URL", "/media/")
@ -417,9 +401,7 @@ LOGGING = {
}, },
} }
LOG_TO_CONSOLE = ( LOG_TO_CONSOLE = os.getenv("VROBBLER_LOG_TO_CONSOLE", "false").lower() in TRUTHY
os.getenv("VROBBLER_LOG_TO_CONSOLE", "false").lower() in TRUTHY
)
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"]