[black] Reformat to use 88 line lengths
This commit is contained in:
@ -88,7 +88,7 @@ testpaths = ["tests"]
|
||||
DJANGO_SETTINGS_MODULE='vrobbler.settings-testing'
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
line-length = 88
|
||||
target-version = ["py39", "py310"]
|
||||
include = ".py$"
|
||||
exclude = "migrations"
|
||||
|
||||
@ -93,41 +93,29 @@ class TestRecipeScraperService:
|
||||
assert result is True
|
||||
|
||||
def test_scrape_returns_title(self, scraper):
|
||||
result = scraper.scrape(
|
||||
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
|
||||
)
|
||||
result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
|
||||
assert result["title"] == "Test Recipe"
|
||||
|
||||
def test_scrape_returns_ingredients(self, scraper):
|
||||
result = scraper.scrape(
|
||||
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
|
||||
)
|
||||
result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
|
||||
assert len(result["ingredients"]) == 3
|
||||
assert "1 cup flour" in result["ingredients"]
|
||||
|
||||
def test_scrape_returns_instructions(self, scraper):
|
||||
result = scraper.scrape(
|
||||
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
|
||||
)
|
||||
result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
|
||||
assert len(result["instructions"]) > 0
|
||||
assert "Mix ingredients together" in result["instructions"]
|
||||
|
||||
def test_scrape_returns_yields(self, scraper):
|
||||
result = scraper.scrape(
|
||||
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
|
||||
)
|
||||
result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
|
||||
assert result["yields"] == "4 servings"
|
||||
|
||||
def test_scrape_returns_total_time(self, scraper):
|
||||
result = scraper.scrape(
|
||||
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
|
||||
)
|
||||
result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
|
||||
assert result["total_time"] == 30
|
||||
|
||||
def test_scrape_returns_url(self, scraper):
|
||||
result = scraper.scrape(
|
||||
RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe"
|
||||
)
|
||||
result = scraper.scrape(RECIPE_HTML_WITH_SCHEMA, "https://example.com/recipe")
|
||||
assert result["url"] == "https://example.com/recipe"
|
||||
|
||||
def test_scrape_raises_on_invalid_html(self, scraper):
|
||||
|
||||
@ -9,9 +9,7 @@ from foods.sources.usda import (
|
||||
class TestUSDAFoodAPI:
|
||||
@pytest.fixture
|
||||
def usda_api(self):
|
||||
with patch(
|
||||
"vrobbler.apps.foods.sources.usda.settings"
|
||||
) as mock_settings:
|
||||
with patch("vrobbler.apps.foods.sources.usda.settings") as mock_settings:
|
||||
mock_settings.USDA_API_KEY = "test_api_key"
|
||||
return USDAFoodAPI(api_key="test_api_key")
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import pytest
|
||||
from vrobbler.apps.podcasts.scrapers import scrape_data_from_google_podcasts
|
||||
|
||||
expected_desc_snippet = (
|
||||
"NPR's Up First is the news you need to start your day. "
|
||||
)
|
||||
expected_desc_snippet = "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_google_url = "https://podcasts.google.com/feed/aHR0cHM6Ly9mZWVkcy5ucHIub3JnLzUxMDMxOC9wb2RjYXN0LnhtbA"
|
||||
|
||||
@ -68,9 +68,7 @@ class MopidyRequest:
|
||||
"artist": kwargs.get("artist", self.artist),
|
||||
"album": kwargs.get("album", self.album),
|
||||
"track_number": int(kwargs.get("track_number", self.track_number)),
|
||||
"run_time_ticks": int(
|
||||
kwargs.get("run_time_ticks", self.run_time_ticks)
|
||||
),
|
||||
"run_time_ticks": int(kwargs.get("run_time_ticks", self.run_time_ticks)),
|
||||
"run_time": int(kwargs.get("run_time", self.run_time)),
|
||||
"playback_time_ticks": int(
|
||||
kwargs.get("playback_time_ticks", self.playback_time_ticks)
|
||||
@ -113,9 +111,7 @@ def mopidy_track():
|
||||
@pytest.fixture
|
||||
def mopidy_track_diff_album_request_data(**kwargs):
|
||||
mb_album_id = "0c56c457-afe1-4679-baab-759ba8dd2a58"
|
||||
return MopidyRequest(
|
||||
album="Gold", musicbrainz_album_id=mb_album_id
|
||||
).request_json
|
||||
return MopidyRequest(album="Gold", musicbrainz_album_id=mb_album_id).request_json
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@ -6,7 +6,5 @@ from vrobbler.apps.scrobbles.utils import timestamp_user_tz_to_utc
|
||||
|
||||
|
||||
def test_timestamp_user_tz_to_utc():
|
||||
timestamp = timestamp_user_tz_to_utc(
|
||||
1685561082, pytz.timezone("US/Eastern")
|
||||
)
|
||||
timestamp = timestamp_user_tz_to_utc(1685561082, pytz.timezone("US/Eastern"))
|
||||
assert timestamp == datetime(2023, 5, 31, 23, 24, 42, tzinfo=pytz.utc)
|
||||
|
||||
@ -498,9 +498,7 @@ def test_scrobble_detail_view_with_notes_as_flat_list(client):
|
||||
user = get_user_model().objects.create_user(
|
||||
username="testuser", email="test@example.com", password="testpass"
|
||||
)
|
||||
task = Task.objects.create(
|
||||
title="Test Task", description="Test description"
|
||||
)
|
||||
task = Task.objects.create(title="Test Task", description="Test description")
|
||||
scrobble = Scrobble.objects.create(
|
||||
task=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(
|
||||
username="testuser", email="test@example.com", password="testpass"
|
||||
)
|
||||
task = Task.objects.create(
|
||||
title="Test Task", description="Test description"
|
||||
)
|
||||
task = Task.objects.create(title="Test Task", description="Test description")
|
||||
scrobble = Scrobble.objects.create(
|
||||
task=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(
|
||||
username="testuser", email="test@example.com", password="testpass"
|
||||
)
|
||||
task = Task.objects.create(
|
||||
title="Test Task", description="Test description"
|
||||
)
|
||||
task = Task.objects.create(title="Test Task", description="Test description")
|
||||
scrobble = Scrobble.objects.create(
|
||||
task=task,
|
||||
media_type="Task",
|
||||
@ -580,9 +574,7 @@ def test_scrobble_detail_view_post_updates_log(client):
|
||||
user = get_user_model().objects.create_user(
|
||||
username="testuser", email="test@example.com", password="testpass"
|
||||
)
|
||||
task = Task.objects.create(
|
||||
title="Test Task", description="Test description"
|
||||
)
|
||||
task = Task.objects.create(title="Test Task", description="Test description")
|
||||
scrobble = Scrobble.objects.create(
|
||||
task=task,
|
||||
media_type="Task",
|
||||
|
||||
@ -14,9 +14,7 @@ def mock_request():
|
||||
class TestVersionInfo:
|
||||
def test_returns_version_and_commit(self, mock_request):
|
||||
with (
|
||||
patch(
|
||||
"vrobbler.context_processors.get_version"
|
||||
) as mock_get_version,
|
||||
patch("vrobbler.context_processors.get_version") as mock_get_version,
|
||||
patch(
|
||||
"vrobbler.context_processors.subprocess.check_output"
|
||||
) as mock_check_output,
|
||||
@ -32,9 +30,7 @@ class TestVersionInfo:
|
||||
def test_uses_env_commit_if_set(self, mock_request):
|
||||
with (
|
||||
patch.dict(os.environ, {"VROBBLER_COMMIT": "env_commit_hash"}),
|
||||
patch(
|
||||
"vrobbler.context_processors.get_version"
|
||||
) as mock_get_version,
|
||||
patch("vrobbler.context_processors.get_version") as mock_get_version,
|
||||
):
|
||||
mock_get_version.return_value = "1.0.0"
|
||||
|
||||
@ -44,9 +40,7 @@ class TestVersionInfo:
|
||||
|
||||
def test_returns_unknown_when_version_fails(self, mock_request):
|
||||
with (
|
||||
patch(
|
||||
"vrobbler.context_processors.get_version"
|
||||
) as mock_get_version,
|
||||
patch("vrobbler.context_processors.get_version") as mock_get_version,
|
||||
patch(
|
||||
"vrobbler.context_processors.subprocess.check_output"
|
||||
) as mock_check_output,
|
||||
@ -62,9 +56,7 @@ class TestVersionInfo:
|
||||
import subprocess
|
||||
|
||||
with (
|
||||
patch(
|
||||
"vrobbler.context_processors.get_version"
|
||||
) as mock_get_version,
|
||||
patch("vrobbler.context_processors.get_version") as mock_get_version,
|
||||
patch(
|
||||
"vrobbler.context_processors.subprocess.check_output"
|
||||
) as mock_check_output,
|
||||
@ -80,9 +72,7 @@ class TestVersionInfo:
|
||||
import subprocess
|
||||
|
||||
with (
|
||||
patch(
|
||||
"vrobbler.context_processors.get_version"
|
||||
) as mock_get_version,
|
||||
patch("vrobbler.context_processors.get_version") as mock_get_version,
|
||||
patch(
|
||||
"vrobbler.context_processors.subprocess.check_output"
|
||||
) as mock_check_output,
|
||||
|
||||
@ -48,12 +48,8 @@ class TestVideoAPI:
|
||||
assert response.status_code == 200
|
||||
assert response.data["title"] == "Test Video"
|
||||
|
||||
def test_filter_videos_by_channel(
|
||||
self, client, auth_headers, channel, video
|
||||
):
|
||||
response = client.get(
|
||||
f"/api/v1/videos/?channel={channel.id}", **auth_headers
|
||||
)
|
||||
def test_filter_videos_by_channel(self, client, auth_headers, channel, video):
|
||||
response = client.get(f"/api/v1/videos/?channel={channel.id}", **auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["channel"] == channel.id
|
||||
@ -68,9 +64,7 @@ class TestChannelAPI:
|
||||
assert response.data["results"][0]["name"] == "Test Channel"
|
||||
|
||||
def test_get_channel(self, client, auth_headers, channel):
|
||||
response = client.get(
|
||||
f"/api/v1/channels/{channel.id}/", **auth_headers
|
||||
)
|
||||
response = client.get(f"/api/v1/channels/{channel.id}/", **auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.data["name"] == "Test Channel"
|
||||
|
||||
|
||||
@ -67,9 +67,7 @@ class Beer(ScrobblableMixin):
|
||||
)
|
||||
untappd_id = models.CharField(max_length=255, **BNULL)
|
||||
untappd_rating = models.FloatField(**BNULL)
|
||||
producer = models.ForeignKey(
|
||||
BeerProducer, on_delete=models.DO_NOTHING, **BNULL
|
||||
)
|
||||
producer = models.ForeignKey(BeerProducer, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
def get_absolute_url(self) -> str:
|
||||
return reverse("beers:beer_detail", kwargs={"slug": self.uuid})
|
||||
@ -130,9 +128,7 @@ class Beer(ScrobblableMixin):
|
||||
)
|
||||
style_ids.append(style_inst.id)
|
||||
|
||||
producer, _created = BeerProducer.objects.get_or_create(
|
||||
**producer_dict
|
||||
)
|
||||
producer, _created = BeerProducer.objects.get_or_create(**producer_dict)
|
||||
beer_dict["producer_id"] = producer.id
|
||||
beer = Beer.objects.create(**beer_dict)
|
||||
for style_id in style_ids:
|
||||
|
||||
@ -85,9 +85,7 @@ def get_ibu_from_soup(soup) -> Optional[int]:
|
||||
def get_rating_from_soup(soup) -> str:
|
||||
rating = ""
|
||||
try:
|
||||
rating = float(
|
||||
soup.find(class_="num").get_text().strip("(").strip(")")
|
||||
)
|
||||
rating = float(soup.find(class_="num").get_text().strip("(").strip(")"))
|
||||
except AttributeError:
|
||||
rating = None
|
||||
except ValueError:
|
||||
@ -124,9 +122,7 @@ def get_beer_from_untappd_id(untappd_id: str) -> dict:
|
||||
beer_dict = {"untappd_id": untappd_id}
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warn(
|
||||
"Bad response from untappd.com", extra={"response": response}
|
||||
)
|
||||
logger.warn("Bad response from untappd.com", extra={"response": response})
|
||||
return beer_dict
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
|
||||
@ -14,9 +14,7 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SEARCH_ID_URL = (
|
||||
"https://boardgamegeek.com/xmlapi/search?search={query}&exact=1"
|
||||
)
|
||||
SEARCH_ID_URL = "https://boardgamegeek.com/xmlapi/search?search={query}&exact=1"
|
||||
GAME_ID_URL = "https://boardgamegeek.com/xmlapi/boardgame/{id}"
|
||||
BGG_ACCESS_TOKEN = getattr(settings, "BGG_ACCESS_TOKEN", "")
|
||||
BASE_HEADERS = {
|
||||
|
||||
@ -99,10 +99,7 @@ class BoardGameLogData(BaseLogData, LongPlayLogData):
|
||||
def player_log(self) -> str:
|
||||
if self.players:
|
||||
return ", ".join(
|
||||
[
|
||||
BoardGameScoreLogData(**player).__str__()
|
||||
for player in self.players
|
||||
]
|
||||
[BoardGameScoreLogData(**player).__str__() for player in self.players]
|
||||
)
|
||||
return ""
|
||||
|
||||
@ -134,9 +131,7 @@ class BoardGamePublisher(TimeStampedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"boardgames:publisher_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("boardgames:publisher_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
|
||||
class BoardGameDesigner(TimeStampedModel):
|
||||
@ -149,9 +144,7 @@ class BoardGameDesigner(TimeStampedModel):
|
||||
return str(self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"boardgames:designer_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("boardgames:designer_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
|
||||
class BoardGameLocation(TimeStampedModel):
|
||||
@ -159,23 +152,17 @@ class BoardGameLocation(TimeStampedModel):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
bgstats_id = models.UUIDField(**BNULL)
|
||||
description = models.TextField(**BNULL)
|
||||
geo_location = models.ForeignKey(
|
||||
GeoLocation, **BNULL, on_delete=models.DO_NOTHING
|
||||
)
|
||||
geo_location = models.ForeignKey(GeoLocation, **BNULL, on_delete=models.DO_NOTHING)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"boardgames:location_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("boardgames:location_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
|
||||
class BoardGame(ScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(
|
||||
settings, "BOARD_GAME_COMPLETION_PERCENT", 100
|
||||
)
|
||||
COMPLETION_PERCENT = getattr(settings, "BOARD_GAME_COMPLETION_PERCENT", 100)
|
||||
|
||||
FIELDS_FROM_BGGEEK = [
|
||||
"igdb_id",
|
||||
@ -250,9 +237,7 @@ class BoardGame(ScrobblableMixin):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"boardgames:boardgame_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("boardgames:boardgame_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
@ -316,9 +301,7 @@ class BoardGame(ScrobblableMixin):
|
||||
self.cover.save(fname, ContentFile(r.content), save=True)
|
||||
|
||||
@classmethod
|
||||
def find_or_create(
|
||||
cls, lookup_id: str, data: dict[str, Any] = {}
|
||||
) -> "BoardGame":
|
||||
def find_or_create(cls, lookup_id: str, data: dict[str, Any] = {}) -> "BoardGame":
|
||||
"""Given a Lookup ID (either BGG or BGA ID), return a board game object"""
|
||||
game = cls.objects.filter(bggeek_id=lookup_id).first()
|
||||
if not game:
|
||||
@ -354,9 +337,7 @@ class BoardGame(ScrobblableMixin):
|
||||
game.uses_teams = data.get("useTeams", False)
|
||||
game.bgstats_id = data.get("uuid", None)
|
||||
if publisher:
|
||||
publisher, _ = BoardGamePublisher.objects.get_or_create(
|
||||
name=publisher
|
||||
)
|
||||
publisher, _ = BoardGamePublisher.objects.get_or_create(name=publisher)
|
||||
game.publisher = publisher
|
||||
game.save()
|
||||
|
||||
@ -369,9 +350,7 @@ class BoardGame(ScrobblableMixin):
|
||||
|
||||
if publishers:
|
||||
for name in publishers:
|
||||
publisher, _ = BoardGamePublisher.objects.get_or_create(
|
||||
name=name
|
||||
)
|
||||
publisher, _ = BoardGamePublisher.objects.get_or_create(name=name)
|
||||
game.publishers.add(publisher)
|
||||
|
||||
return game
|
||||
|
||||
@ -11,9 +11,7 @@ User = get_user_model()
|
||||
def import_chess_games_for_user_id(user_id: int, commit: bool = False) -> dict:
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
client = berserk.Client(
|
||||
session=berserk.TokenSession(settings.LICHESS_API_KEY)
|
||||
)
|
||||
client = berserk.Client(session=berserk.TokenSession(settings.LICHESS_API_KEY))
|
||||
games = client.games.export_by_player(user.profile.lichess_username)
|
||||
for game_dict in games:
|
||||
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", "")
|
||||
)
|
||||
else:
|
||||
other_player["name_str"] = white_player.get("user", {}).get(
|
||||
"name", ""
|
||||
)
|
||||
other_player["name_str"] = white_player.get("user", {}).get("name", "")
|
||||
other_player["lichess_username"] = other_player["name_str"]
|
||||
|
||||
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", "")
|
||||
)
|
||||
else:
|
||||
other_player["name_str"] = black_player.get("user", {}).get(
|
||||
"name", ""
|
||||
)
|
||||
other_player["name_str"] = black_player.get("user", {}).get("name", "")
|
||||
other_player["lichess_username"] = other_player["name_str"]
|
||||
other_player["color"] = "black"
|
||||
if winner == "white":
|
||||
|
||||
@ -37,9 +37,7 @@ class BoardGameListView(ScrobbleableListView):
|
||||
for designer in scrobble.board_game.designers.all():
|
||||
designers_this_week[designer.id] = {
|
||||
"designer": designer,
|
||||
"count": designers_this_week.get(designer.id, {}).get(
|
||||
"count", 0
|
||||
)
|
||||
"count": designers_this_week.get(designer.id, {}).get("count", 0)
|
||||
+ 1,
|
||||
}
|
||||
|
||||
@ -48,9 +46,7 @@ class BoardGameListView(ScrobbleableListView):
|
||||
for designer in scrobble.board_game.designers.all():
|
||||
designers_this_month[designer.id] = {
|
||||
"designer": designer,
|
||||
"count": designers_this_month.get(designer.id, {}).get(
|
||||
"count", 0
|
||||
)
|
||||
"count": designers_this_month.get(designer.id, {}).get("count", 0)
|
||||
+ 1,
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,7 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USER_AGENT = (
|
||||
"Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
|
||||
)
|
||||
USER_AGENT = "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}"
|
||||
|
||||
|
||||
@ -95,32 +93,20 @@ def get_amazon_product_dict(amazon_id: str) -> dict:
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
try:
|
||||
data_dict["title"] = soup.findAll("span", class_="celwidget")[
|
||||
1
|
||||
].text.strip()
|
||||
data_dict["cover_url"] = soup.find("img", class_="frontImage").get(
|
||||
"src"
|
||||
)
|
||||
data_dict["summary"] = soup.findAll(
|
||||
"div", class_="a-expander-content"
|
||||
)[1].text
|
||||
data_dict["title"] = soup.findAll("span", class_="celwidget")[1].text.strip()
|
||||
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")
|
||||
data_dict["isbn"] = meta[AmazonAttribute.ISBN_10.value].text.strip()
|
||||
pages = meta[AmazonAttribute.PAGES.value].text
|
||||
if "pages" in pages:
|
||||
data_dict["pages"] = (
|
||||
meta[AmazonAttribute.PAGES.value]
|
||||
.text.split("pages")[0]
|
||||
.strip()
|
||||
meta[AmazonAttribute.PAGES.value].text.split("pages")[0].strip()
|
||||
)
|
||||
except IndexError as e:
|
||||
logger.error(
|
||||
f"Amazon lookup is failing for this product {amazon_id}: {e}"
|
||||
)
|
||||
logger.error(f"Amazon lookup is failing for this product {amazon_id}: {e}")
|
||||
except AttributeError as e:
|
||||
logger.error(
|
||||
f"Amazon lookup is failing for this product {amazon_id}: {e}"
|
||||
)
|
||||
logger.error(f"Amazon lookup is failing for this product {amazon_id}: {e}")
|
||||
|
||||
return data_dict
|
||||
|
||||
|
||||
@ -146,9 +146,7 @@ def build_book_map(rows) -> dict:
|
||||
)
|
||||
continue
|
||||
book = Book.objects.filter(
|
||||
koreader_data_by_hash__icontains=book_row[
|
||||
KoReaderBookColumn.MD5.value
|
||||
]
|
||||
koreader_data_by_hash__icontains=book_row[KoReaderBookColumn.MD5.value]
|
||||
).first()
|
||||
|
||||
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,
|
||||
}
|
||||
if book_ids_not_found:
|
||||
logger.info(
|
||||
f"Found pages for books not in file: {set(book_ids_not_found)}"
|
||||
)
|
||||
logger.info(f"Found pages for books not in file: {set(book_ids_not_found)}")
|
||||
return book_map
|
||||
|
||||
|
||||
def build_scrobbles_from_book_map(
|
||||
book_map: dict, user: "User"
|
||||
) -> list["Scrobble"]:
|
||||
def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobble"]:
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
|
||||
scrobbles_to_create = []
|
||||
@ -240,9 +234,9 @@ def build_scrobbles_from_book_map(
|
||||
|
||||
seconds_from_last_page = 0
|
||||
if prev_page_stats:
|
||||
seconds_from_last_page = stats.get(
|
||||
"end_ts"
|
||||
) - prev_page_stats.get("start_ts")
|
||||
seconds_from_last_page = stats.get("end_ts") - prev_page_stats.get(
|
||||
"start_ts"
|
||||
)
|
||||
|
||||
playback_position_seconds = playback_position_seconds + stats.get(
|
||||
"duration"
|
||||
@ -251,9 +245,7 @@ def build_scrobbles_from_book_map(
|
||||
end_of_reading = pages_processed == total_pages_read
|
||||
big_jump_to_this_page = (cur_page_number - last_page_number) > 10
|
||||
is_session_gap = seconds_from_last_page > SESSION_GAP_SECONDS
|
||||
if (
|
||||
is_session_gap and not big_jump_to_this_page
|
||||
) or end_of_reading:
|
||||
if (is_session_gap and not big_jump_to_this_page) or end_of_reading:
|
||||
should_create_scrobble = True
|
||||
|
||||
if should_create_scrobble:
|
||||
@ -381,9 +373,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
|
||||
return new_scrobbles
|
||||
|
||||
book_map = build_page_data(
|
||||
cur.execute(
|
||||
"SELECT * from page_stat_data ORDER BY id_book, start_time"
|
||||
),
|
||||
cur.execute("SELECT * from page_stat_data ORDER BY id_book, start_time"),
|
||||
book_map,
|
||||
tz,
|
||||
)
|
||||
|
||||
@ -13,9 +13,7 @@ HEADERS = {
|
||||
}
|
||||
LOCG_WRTIER_URL = ""
|
||||
LOCG_WRITER_DETAIL_URL = "https://leagueofcomicgeeks.com/people/{slug}"
|
||||
LOCG_SEARCH_URL = (
|
||||
"https://leagueofcomicgeeks.com/search/ajax_issues?query={query}"
|
||||
)
|
||||
LOCG_SEARCH_URL = "https://leagueofcomicgeeks.com/search/ajax_issues?query={query}"
|
||||
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")
|
||||
try:
|
||||
data_dict["pages"] = (
|
||||
attrs[1]
|
||||
.find("div", class_="value")
|
||||
.text.split("pages")[0]
|
||||
.strip()
|
||||
attrs[1].find("div", class_="value").text.split("pages")[0].strip()
|
||||
)
|
||||
except IndexError:
|
||||
logger.warn(f"No ISBN field")
|
||||
try:
|
||||
data_dict["isbn"] = (
|
||||
attrs[3].find("div", class_="value").text.strip()
|
||||
)
|
||||
data_dict["isbn"] = attrs[3].find("div", class_="value").text.strip()
|
||||
except IndexError:
|
||||
logger.warn(f"No ISBN field")
|
||||
|
||||
writer_slug = None
|
||||
try:
|
||||
writer_slug = (
|
||||
soup.findAll("div", class_="name")[5]
|
||||
.a.get("href")
|
||||
.split("people/")[1]
|
||||
soup.findAll("div", class_="name")[5].a.get("href").split("people/")[1]
|
||||
)
|
||||
except IndexError:
|
||||
logger.warn(f"No wrtier found")
|
||||
|
||||
@ -37,7 +37,5 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
scrobbles_to_create = []
|
||||
|
||||
for scrobble in Scrobble.objects.filter(
|
||||
media_type="Book", source="KOReader"
|
||||
):
|
||||
for scrobble in Scrobble.objects.filter(media_type="Book", source="KOReader"):
|
||||
update_scrobble_from_page_data(scrobble)
|
||||
|
||||
@ -52,8 +52,7 @@ class Command(BaseCommand):
|
||||
seconds_from_last_page = 0
|
||||
if prev_page:
|
||||
seconds_from_last_page = (
|
||||
page.end_time.timestamp()
|
||||
- prev_page.start_time.timestamp()
|
||||
page.end_time.timestamp() - prev_page.start_time.timestamp()
|
||||
)
|
||||
playback_position_seconds = (
|
||||
playback_position_seconds + page.duration_seconds
|
||||
@ -62,9 +61,7 @@ class Command(BaseCommand):
|
||||
end_of_reading = pages_processed == total_pages
|
||||
big_jump_to_this_page = False
|
||||
if prev_page:
|
||||
big_jump_to_this_page = (
|
||||
page.number - prev_page.number
|
||||
) > 10
|
||||
big_jump_to_this_page = (page.number - prev_page.number) > 10
|
||||
if (
|
||||
seconds_from_last_page > SESSION_GAP_SECONDS
|
||||
and not big_jump_to_this_page
|
||||
@ -113,9 +110,7 @@ class Command(BaseCommand):
|
||||
user_id=user.id,
|
||||
).first()
|
||||
if scrobble:
|
||||
logger.info(
|
||||
f"Found existing scrobble {scrobble}, updating"
|
||||
)
|
||||
logger.info(f"Found existing scrobble {scrobble}, updating")
|
||||
scrobble.book_page_data = scrobble_page_data
|
||||
scrobble.playback_position_seconds = (
|
||||
scrobble.calc_reading_duration()
|
||||
|
||||
@ -133,9 +133,7 @@ class Author(TimeStampedModel):
|
||||
|
||||
class Book(LongPlayScrobblableMixin):
|
||||
COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
|
||||
AVG_PAGE_READING_SECONDS = getattr(
|
||||
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
||||
)
|
||||
AVG_PAGE_READING_SECONDS = getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60)
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
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
|
||||
book, created = cls.objects.get_or_create(original_title=title)
|
||||
if not created:
|
||||
logger.info(
|
||||
"Found exact match for book by title", extra={"title": title}
|
||||
)
|
||||
logger.info("Found exact match for book by title", extra={"title": title})
|
||||
|
||||
if not enrich:
|
||||
logger.info(
|
||||
@ -296,9 +292,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
if authors:
|
||||
for author_str in authors:
|
||||
if author_str:
|
||||
author, a_created = Author.objects.get_or_create(
|
||||
name=author_str
|
||||
)
|
||||
author, a_created = Author.objects.get_or_create(name=author_str)
|
||||
author_list.append(author)
|
||||
if a_created:
|
||||
# TODO enrich author
|
||||
@ -332,13 +326,9 @@ class Book(LongPlayScrobblableMixin):
|
||||
if not data:
|
||||
logger.warn(f"Checking openlibrary for {self.title}")
|
||||
if self.openlibrary_id and force_update:
|
||||
data = lookup_book_from_openlibrary(
|
||||
str(self.openlibrary_id)
|
||||
)
|
||||
data = lookup_book_from_openlibrary(str(self.openlibrary_id))
|
||||
else:
|
||||
data = lookup_book_from_openlibrary(
|
||||
str(self.title), author_name
|
||||
)
|
||||
data = lookup_book_from_openlibrary(str(self.title), author_name)
|
||||
|
||||
if not data:
|
||||
if self.locg_slug:
|
||||
@ -385,10 +375,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
if "pages" in data.keys() and data.get("pages") == None:
|
||||
data.pop("pages")
|
||||
|
||||
if (
|
||||
not isinstance(data.get("pages"), int)
|
||||
and "pages" in data.keys()
|
||||
):
|
||||
if not isinstance(data.get("pages"), int) and "pages" in data.keys():
|
||||
logger.info(
|
||||
f"Pages for {self} from OL expected to be int, but got {data.get('pages')}"
|
||||
)
|
||||
@ -420,9 +407,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
self.save()
|
||||
|
||||
def fix_authors_metadata(self, openlibrary_author_id):
|
||||
author = Author.objects.filter(
|
||||
openlibrary_id=openlibrary_author_id
|
||||
).first()
|
||||
author = Author.objects.filter(openlibrary_id=openlibrary_author_id).first()
|
||||
if not author:
|
||||
data = lookup_author_from_openlibrary(openlibrary_author_id)
|
||||
author_image_url = data.pop("author_headshot_url", None)
|
||||
@ -433,9 +418,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
r = requests.get(author_image_url)
|
||||
if r.status_code == 200:
|
||||
fname = f"{author.name}_{author.uuid}.jpg"
|
||||
author.headshot.save(
|
||||
fname, ContentFile(r.content), save=True
|
||||
)
|
||||
author.headshot.save(fname, ContentFile(r.content), save=True)
|
||||
self.authors.add(author)
|
||||
|
||||
def get_author_from_locg(self, locg_slug):
|
||||
@ -451,9 +434,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
author.headshot.save(fname, ContentFile(r.content), save=True)
|
||||
self.authors.add(author)
|
||||
|
||||
def page_data_for_user(
|
||||
self, user_id: int, convert_timestamps: bool = True
|
||||
) -> dict:
|
||||
def page_data_for_user(self, user_id: int, convert_timestamps: bool = True) -> dict:
|
||||
scrobbles = self.scrobble_set.filter(user=user_id)
|
||||
|
||||
pages = {}
|
||||
@ -461,9 +442,7 @@ class Book(LongPlayScrobblableMixin):
|
||||
if scrobble.logdata.page_data:
|
||||
for page, data in scrobble.logdata.page_data.items():
|
||||
if convert_timestamps:
|
||||
data["start_ts"] = datetime.fromtimestamp(
|
||||
data["start_ts"]
|
||||
)
|
||||
data["start_ts"] = datetime.fromtimestamp(data["start_ts"])
|
||||
data["end_ts"] = datetime.fromtimestamp(data["end_ts"])
|
||||
pages[page] = data
|
||||
sorted_pages = OrderedDict(
|
||||
@ -502,9 +481,7 @@ class Paper(LongPlayScrobblableMixin):
|
||||
"""Keeps track of Academic Papers"""
|
||||
|
||||
COMPLETION_PERCENT = getattr(settings, "PAPER_COMPLETION_PERCENT", 60)
|
||||
AVG_PAGE_READING_SECONDS = getattr(
|
||||
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
||||
)
|
||||
AVG_PAGE_READING_SECONDS = getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60)
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
semantic_title = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
@ -10,7 +10,9 @@ from thefuzz import fuzz
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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}"
|
||||
COVER_URL = "https://covers.openlibrary.org/b/olid/{id}-L.jpg"
|
||||
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(
|
||||
title: str, author: Optional[str] = None
|
||||
) -> dict:
|
||||
def lookup_book_from_openlibrary(title: str, author: Optional[str] = None) -> dict:
|
||||
title_quoted = urllib.parse.quote(title)
|
||||
author_quoted = ""
|
||||
if author:
|
||||
|
||||
@ -160,9 +160,7 @@ class ComicVineClient(object):
|
||||
# or not the request cache is to be used, store the procedure in a
|
||||
# local function to avoid repetition.
|
||||
def __httpget():
|
||||
response = requests.get(
|
||||
self.API_URL, headers=self.HEADERS, params=params
|
||||
)
|
||||
response = requests.get(self.API_URL, headers=self.HEADERS, params=params)
|
||||
|
||||
if not response.ok:
|
||||
self._handle_http_error(response)
|
||||
@ -214,19 +212,13 @@ def lookup_comic_from_comicvine(title: str) -> dict:
|
||||
|
||||
api_key = getattr(settings, "COMICVINE_API_KEY", "")
|
||||
if not api_key:
|
||||
logger.warning(
|
||||
"No ComicVine API key configured, not looking anything up"
|
||||
)
|
||||
logger.warning("No ComicVine API key configured, not looking anything up")
|
||||
return {}
|
||||
|
||||
client = ComicVineClient(
|
||||
api_key=getattr(settings, "COMICVINE_API_KEY", None)
|
||||
)
|
||||
client = ComicVineClient(api_key=getattr(settings, "COMICVINE_API_KEY", None))
|
||||
|
||||
raw_results = client.search(title).get("results")
|
||||
results = [
|
||||
r for r in raw_results if r.get("resource_type") == resource_type
|
||||
]
|
||||
results = [r for r in raw_results if r.get("resource_type") == resource_type]
|
||||
if not results:
|
||||
logger.warning("No comic found on ComicVine")
|
||||
return {}
|
||||
|
||||
@ -6,9 +6,7 @@ import requests
|
||||
from django.conf import settings
|
||||
|
||||
API_KEY = settings.GOOGLE_API_KEY
|
||||
GOOGLE_BOOKS_URL = (
|
||||
'https://www.googleapis.com/books/v1/volumes?q="{title}"&key={key}'
|
||||
)
|
||||
GOOGLE_BOOKS_URL = 'https://www.googleapis.com/books/v1/volumes?q="{title}"&key={key}'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -21,14 +19,10 @@ def lookup_book_from_google(title: str) -> dict:
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
"Bad response from Google", extra={"response": response}
|
||||
)
|
||||
logger.warning("Bad response from Google", extra={"response": response})
|
||||
return book_dict
|
||||
|
||||
google_result = (
|
||||
json.loads(response.content).get("items", [{}])[0].get("volumeInfo")
|
||||
)
|
||||
google_result = json.loads(response.content).get("items", [{}])[0].get("volumeInfo")
|
||||
if not google_result:
|
||||
return {}
|
||||
|
||||
@ -69,8 +63,8 @@ def lookup_book_from_google(title: str) -> dict:
|
||||
|
||||
book_dict["base_run_time_seconds"] = 3600
|
||||
if book_dict.get("pages"):
|
||||
book_dict["base_run_time_seconds"] = book_dict.get(
|
||||
"pages", 10
|
||||
) * getattr(settings, "AVERAGE_PAGE_READING_SECONDS", 60)
|
||||
book_dict["base_run_time_seconds"] = book_dict.get("pages", 10) * getattr(
|
||||
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
||||
)
|
||||
|
||||
return book_dict
|
||||
|
||||
@ -18,9 +18,7 @@ def get_api_result(url):
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
"Bad response from Semantic", extra={"response": response}
|
||||
)
|
||||
logger.warning("Bad response from Semantic", extra={"response": response})
|
||||
return None
|
||||
|
||||
return response
|
||||
@ -64,12 +62,10 @@ def lookup_paper_from_semantic(title: str) -> dict:
|
||||
paper_dict["tldr"] = result.get("bib", {}).get("abstract")
|
||||
paper_dict["journal"] = result.get("journal", {}).get("name")
|
||||
paper_dict["journal_volume"] = result.get("journal", {}).get("volume")
|
||||
paper_dict["openaccess_pdf_url"] = result.get("openAccessPdf", {}).get(
|
||||
"url"
|
||||
paper_dict["openaccess_pdf_url"] = result.get("openAccessPdf", {}).get("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["genres"] = result.get("fieldsOfStudy")
|
||||
|
||||
|
||||
@ -99,7 +99,9 @@ class KoReaderBookRows:
|
||||
]
|
||||
)
|
||||
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
|
||||
else:
|
||||
start_time += AVERAGE_PAGE_READING_SECONDS
|
||||
|
||||
@ -32,17 +32,13 @@ def test_load_page_data_to_map(get_mock, koreader_rows, valid_response):
|
||||
)
|
||||
assert (
|
||||
len(book_map[1]["pages"])
|
||||
== koreader_rows.BOOK_ROWS[0][
|
||||
KoReaderBookColumn.TOTAL_READ_PAGES.value
|
||||
]
|
||||
== koreader_rows.BOOK_ROWS[0][KoReaderBookColumn.TOTAL_READ_PAGES.value]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@mock.patch("requests.get")
|
||||
def test_build_scrobbles_from_pages(
|
||||
get_mock, koreader_rows, demo_user, valid_response
|
||||
):
|
||||
def test_build_scrobbles_from_pages(get_mock, koreader_rows, demo_user, valid_response):
|
||||
get_mock.return_value = valid_response
|
||||
book_map = build_book_map(koreader_rows.BOOK_ROWS)
|
||||
book_map = build_page_data(koreader_rows.PAGE_STATS_ROWS, book_map)
|
||||
|
||||
@ -26,9 +26,7 @@ class BrickSet(LongPlayScrobblableMixin):
|
||||
number = models.CharField(max_length=10, **BNULL)
|
||||
release_year = models.IntegerField(**BNULL)
|
||||
piece_count = models.IntegerField(**BNULL)
|
||||
brickset_rating = models.DecimalField(
|
||||
max_digits=3, decimal_places=1, **BNULL
|
||||
)
|
||||
brickset_rating = models.DecimalField(max_digits=3, decimal_places=1, **BNULL)
|
||||
lego_item_number = models.CharField(max_length=10, **BNULL)
|
||||
box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
|
||||
box_image_small = ImageSpecField(
|
||||
|
||||
@ -7,14 +7,16 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO charts_chartrecord
|
||||
(created, modified, user_id, rank, count, year, month, week, day,
|
||||
video_id, series_id, artist_id, track_id, period_start, period_end)
|
||||
SELECT created, modified, user_id, rank, count, year, month, week, day,
|
||||
video_id, series_id, artist_id, track_id, period_start, period_end
|
||||
FROM scrobbles_chartrecord
|
||||
""")
|
||||
"""
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Copied ChartRecord data to charts app")
|
||||
)
|
||||
|
||||
@ -87,9 +87,7 @@ def get_ibu_from_soup(soup) -> Optional[int]:
|
||||
def get_rating_from_soup(soup) -> str:
|
||||
rating = ""
|
||||
try:
|
||||
rating = float(
|
||||
soup.find(class_="num").get_text().strip("(").strip(")")
|
||||
)
|
||||
rating = float(soup.find(class_="num").get_text().strip("(").strip(")"))
|
||||
except AttributeError:
|
||||
rating = None
|
||||
except ValueError:
|
||||
@ -127,9 +125,7 @@ def get_food_from_allrecipe_id(allrecipe_id: str) -> dict:
|
||||
food_dict = {"allrecipe_id": allrecipe_id}
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warn(
|
||||
"Bad response from allrecipe", extra={"response": response}
|
||||
)
|
||||
logger.warn("Bad response from allrecipe", extra={"response": response})
|
||||
return food_dict
|
||||
|
||||
import pdb
|
||||
|
||||
@ -63,17 +63,13 @@ class Food(ScrobblableMixin):
|
||||
cook_time_minutes = models.IntegerField(**BNULL)
|
||||
total_time_minutes = models.IntegerField(**BNULL)
|
||||
servings = models.IntegerField(**BNULL)
|
||||
yield_text = models.CharField(
|
||||
max_length=255, **BNULL
|
||||
) # e.g., "8 calzones"
|
||||
yield_text = models.CharField(max_length=255, **BNULL) # e.g., "8 calzones"
|
||||
|
||||
# Nutrition (per serving)
|
||||
calories = models.IntegerField(**BNULL)
|
||||
protein = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
|
||||
fat = models.DecimalField(max_digits=10, decimal_places=2, **BNULL)
|
||||
carbohydrates = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, **BNULL
|
||||
)
|
||||
carbohydrates = 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)
|
||||
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_rating = models.FloatField(**BNULL)
|
||||
category = models.ForeignKey(
|
||||
FoodCategory, on_delete=models.DO_NOTHING, **BNULL
|
||||
)
|
||||
category = models.ForeignKey(FoodCategory, on_delete=models.DO_NOTHING, **BNULL)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
@ -136,9 +130,7 @@ class Food(ScrobblableMixin):
|
||||
return FoodLogData
|
||||
|
||||
@classmethod
|
||||
def find_or_create_from_recipe(
|
||||
cls, url: str, category=None
|
||||
) -> Tuple["Food", bool]:
|
||||
def find_or_create_from_recipe(cls, url: str, category=None) -> Tuple["Food", bool]:
|
||||
"""
|
||||
Scrape a recipe URL and create/update Food instance.
|
||||
Uses django-taggit for tags and existing FoodCategory for category.
|
||||
@ -164,16 +156,12 @@ class Food(ScrobblableMixin):
|
||||
nutrition = recipe_data.get("nutrition")
|
||||
if not nutrition:
|
||||
calculator = NutritionCalculator()
|
||||
servings = (
|
||||
scraper.parse_servings(recipe_data.get("yields", "1")) or 1
|
||||
)
|
||||
servings = scraper.parse_servings(recipe_data.get("yields", "1")) or 1
|
||||
nutrition = calculator.calculate_nutrition(
|
||||
recipe_data.get("ingredients", []), servings
|
||||
)
|
||||
else:
|
||||
servings = (
|
||||
scraper.parse_servings(recipe_data.get("yields", "1")) or 1
|
||||
)
|
||||
servings = scraper.parse_servings(recipe_data.get("yields", "1")) or 1
|
||||
|
||||
# Get or create category (if not provided)
|
||||
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,
|
||||
protein=nutrition.get("protein") if nutrition else None,
|
||||
fat=nutrition.get("fat") if nutrition else None,
|
||||
carbohydrates=(
|
||||
nutrition.get("carbohydrates") if nutrition else None
|
||||
),
|
||||
carbohydrates=(nutrition.get("carbohydrates") if nutrition else None),
|
||||
fiber=nutrition.get("fiber") if nutrition else None,
|
||||
category=category,
|
||||
)
|
||||
@ -246,9 +232,7 @@ class Food(ScrobblableMixin):
|
||||
calories=int(nutrition.get("calories", 0)) if nutrition else None,
|
||||
protein=nutrition.get("protein") if nutrition else None,
|
||||
fat=nutrition.get("fat") if nutrition else None,
|
||||
carbohydrates=(
|
||||
nutrition.get("carbohydrates") if nutrition else None
|
||||
),
|
||||
carbohydrates=(nutrition.get("carbohydrates") if nutrition else None),
|
||||
fiber=nutrition.get("fiber") if nutrition else None,
|
||||
category=category,
|
||||
)
|
||||
@ -263,9 +247,7 @@ class Food(ScrobblableMixin):
|
||||
try:
|
||||
ingredients = json.loads(self.ingredients)
|
||||
calculator = NutritionCalculator()
|
||||
nutrition = calculator.calculate_nutrition(
|
||||
ingredients, self.servings or 1
|
||||
)
|
||||
nutrition = calculator.calculate_nutrition(ingredients, self.servings or 1)
|
||||
|
||||
self.calories = int(nutrition.get("calories", 0))
|
||||
self.protein = nutrition.get("protein")
|
||||
|
||||
@ -87,9 +87,7 @@ class RecipeScraperService:
|
||||
response = requests.get(
|
||||
url,
|
||||
timeout=10,
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (compatible; Vrobbler/0.3)"
|
||||
},
|
||||
headers={"User-Agent": "Mozilla/5.0 (compatible; Vrobbler/0.3)"},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
logger.debug("Recipe website returned non 200 response")
|
||||
@ -181,9 +179,7 @@ class RecipeScraperService:
|
||||
keywords = recipe_data.get("keywords")
|
||||
if keywords:
|
||||
if isinstance(keywords, str):
|
||||
tags.update(
|
||||
[k.strip() for k in keywords.split(",") if k.strip()]
|
||||
)
|
||||
tags.update([k.strip() for k in keywords.split(",") if k.strip()])
|
||||
elif isinstance(keywords, list):
|
||||
tags.update([k.strip() for k in keywords if k.strip()])
|
||||
|
||||
|
||||
@ -110,22 +110,14 @@ class USDAFoodAPI:
|
||||
if "nutrientNumber" in nutrient:
|
||||
nutrient_id = str(nutrient.get("nutrientNumber", ""))
|
||||
value = nutrient.get("value")
|
||||
elif "nutrient" in nutrient and isinstance(
|
||||
nutrient["nutrient"], dict
|
||||
):
|
||||
nutrient_id = str(
|
||||
nutrient.get("nutrient", {}).get("id", "")
|
||||
)
|
||||
elif "nutrient" in nutrient and isinstance(nutrient["nutrient"], dict):
|
||||
nutrient_id = str(nutrient.get("nutrient", {}).get("id", ""))
|
||||
value = nutrient.get("value")
|
||||
elif "number" in nutrient:
|
||||
nutrient_id = str(nutrient.get("number", ""))
|
||||
value = nutrient.get("value")
|
||||
|
||||
if (
|
||||
nutrient_id
|
||||
and nutrient_id in nutrient_map
|
||||
and value is not None
|
||||
):
|
||||
if nutrient_id and nutrient_id in nutrient_map and value is not None:
|
||||
key = nutrient_map[nutrient_id]
|
||||
try:
|
||||
nutrients[key] = float(value)
|
||||
@ -404,11 +396,7 @@ class NutritionCalculator:
|
||||
# Strategy 1: Direct search
|
||||
ingredient_name,
|
||||
# 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
|
||||
re.sub(
|
||||
r"\s+(fresh|dried|ground|chopped|sliced)$",
|
||||
@ -417,11 +405,7 @@ class NutritionCalculator:
|
||||
flags=re.IGNORECASE,
|
||||
),
|
||||
# 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:
|
||||
@ -431,9 +415,7 @@ class NutritionCalculator:
|
||||
try:
|
||||
results = self.usda.search_foods(query, page_size=5)
|
||||
if results:
|
||||
logger.debug(
|
||||
f"Found {query}: {results[0].get('description')}"
|
||||
)
|
||||
logger.debug(f"Found {query}: {results[0].get('description')}")
|
||||
return results[0]
|
||||
except Exception as e:
|
||||
logger.warning(f"USDA search failed for '{query}': {e}")
|
||||
@ -471,9 +453,7 @@ class NutritionCalculator:
|
||||
}
|
||||
|
||||
# Get base weight in grams
|
||||
base_grams = volume_to_grams.get(
|
||||
unit, 100
|
||||
) # Default to 100g if unknown
|
||||
base_grams = volume_to_grams.get(unit, 100) # Default to 100g if unknown
|
||||
|
||||
# Adjust for ingredient type (very simplified)
|
||||
if any(
|
||||
@ -481,22 +461,16 @@ class NutritionCalculator:
|
||||
for word in ["flour", "sugar", "rice", "grain"]
|
||||
):
|
||||
base_grams = volume_to_grams.get(unit, 125) # Dry ingredients
|
||||
elif any(
|
||||
word in ingredient_name.lower()
|
||||
for word in ["oil", "butter", "fat"]
|
||||
):
|
||||
elif any(word in ingredient_name.lower() for word in ["oil", "butter", "fat"]):
|
||||
base_grams = volume_to_grams.get(unit, 220) # Dense liquids
|
||||
elif any(
|
||||
word in ingredient_name.lower()
|
||||
for word in ["milk", "water", "broth"]
|
||||
word in ingredient_name.lower() for word in ["milk", "water", "broth"]
|
||||
):
|
||||
base_grams = volume_to_grams.get(unit, 240) # Liquids
|
||||
|
||||
return quantity * base_grams
|
||||
|
||||
def calculate_nutrition(
|
||||
self, ingredients: List[str], servings: int = 1
|
||||
) -> Dict:
|
||||
def calculate_nutrition(self, ingredients: List[str], servings: int = 1) -> Dict:
|
||||
"""
|
||||
Calculate total nutrition for a recipe from ingredient list.
|
||||
Returns nutrition per serving.
|
||||
@ -543,12 +517,8 @@ class NutritionCalculator:
|
||||
# USDA data is typically per 100g, so scale accordingly
|
||||
multiplier = grams / 100
|
||||
|
||||
totals["calories"] += (
|
||||
nutrients.get("calories", 0) * multiplier
|
||||
)
|
||||
totals["protein"] += (
|
||||
nutrients.get("protein", 0) * multiplier
|
||||
)
|
||||
totals["calories"] += nutrients.get("calories", 0) * multiplier
|
||||
totals["protein"] += nutrients.get("protein", 0) * multiplier
|
||||
totals["fat"] += nutrients.get("fat", 0) * multiplier
|
||||
totals["carbohydrates"] += (
|
||||
nutrients.get("carbohydrates", 0) * multiplier
|
||||
|
||||
@ -20,9 +20,7 @@ class LifeEvent(ScrobblableMixin):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"life-events:life-event_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("life-events:life-event_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
@ -38,6 +36,6 @@ class LifeEvent(ScrobblableMixin):
|
||||
|
||||
def scrobbles(self, user_id):
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
return Scrobble.objects.filter(
|
||||
user_id=user_id, life_event=self
|
||||
).order_by("-timestamp")
|
||||
return Scrobble.objects.filter(user_id=user_id, life_event=self).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
|
||||
@ -5,9 +5,7 @@ app_name = "lifeevents"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"lifeevents/", views.LifeEventListView.as_view(), name="lifeevent_list"
|
||||
),
|
||||
path("lifeevents/", views.LifeEventListView.as_view(), name="lifeevent_list"),
|
||||
path(
|
||||
"lifeevent/<slug:slug>/",
|
||||
views.LifeEventDetailView.as_view(),
|
||||
|
||||
@ -42,9 +42,7 @@ class GeoLocation(ScrobblableMixin):
|
||||
return f"{self.lat} x {self.lon}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"locations:geolocation_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("locations:geolocation_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@property
|
||||
def logdata_cls(self):
|
||||
@ -101,9 +99,7 @@ class GeoLocation(ScrobblableMixin):
|
||||
|
||||
@property
|
||||
def strings(self) -> ScrobblableConstants:
|
||||
return ScrobblableConstants(
|
||||
verb="Going", tags="world_map", priority="low"
|
||||
)
|
||||
return ScrobblableConstants(verb="Going", tags="world_map", priority="low")
|
||||
|
||||
def loc_diff(self, old_lat_lon: tuple) -> tuple:
|
||||
return (
|
||||
@ -114,9 +110,7 @@ class GeoLocation(ScrobblableMixin):
|
||||
def has_moved(self, previous_location: "GeoLocation") -> bool:
|
||||
has_moved = False
|
||||
|
||||
loc_diff = self.loc_diff(
|
||||
(previous_location.lat, previous_location.lon)
|
||||
)
|
||||
loc_diff = self.loc_diff((previous_location.lat, previous_location.lon))
|
||||
if loc_diff[0] > GEOLOC_PROXIMITY or loc_diff[1] > GEOLOC_PROXIMITY:
|
||||
has_moved = True
|
||||
logger.debug(
|
||||
|
||||
@ -13,9 +13,7 @@ def test_find_or_create(caplog):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_find_or_create_truncation():
|
||||
loc = GeoLocation.find_or_create(
|
||||
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
|
||||
)
|
||||
loc = GeoLocation.find_or_create({"lat": 44.2345, "lon": -68.2345, "alt": 60.356})
|
||||
assert loc.lat == 44.234
|
||||
assert loc.lon == -68.234
|
||||
assert loc.altitude == 60
|
||||
@ -25,9 +23,7 @@ def test_find_or_create_truncation():
|
||||
def test_find_or_create_finds_existing():
|
||||
extant = GeoLocation.objects.create(lat=44.234, lon=-68.234, altitude=50)
|
||||
|
||||
loc = GeoLocation.find_or_create(
|
||||
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
|
||||
)
|
||||
loc = GeoLocation.find_or_create({"lat": 44.2345, "lon": -68.2345, "alt": 60.356})
|
||||
assert loc.id == extant.id
|
||||
|
||||
|
||||
@ -35,9 +31,7 @@ def test_find_or_create_finds_existing():
|
||||
def test_find_or_create_creates_new():
|
||||
extant = GeoLocation.objects.create(lat=44.234, lon=-69.234, altitude=60)
|
||||
|
||||
loc = GeoLocation.find_or_create(
|
||||
{"lat": 44.2345, "lon": -68.2345, "alt": 60.356}
|
||||
)
|
||||
loc = GeoLocation.find_or_create({"lat": 44.2345, "lon": -68.2345, "alt": 60.356})
|
||||
assert not loc.id == extant.id
|
||||
|
||||
|
||||
@ -47,9 +41,7 @@ def test_found_in_proximity_location():
|
||||
lon = -69.234
|
||||
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
|
||||
|
||||
close = GeoLocation.objects.create(
|
||||
lat=lat + 0.0001, lon=lon - 0.0001, altitude=60
|
||||
)
|
||||
close = GeoLocation.objects.create(lat=lat + 0.0001, lon=lon - 0.0001, altitude=60)
|
||||
assert close not in loc.in_proximity(named=True)
|
||||
assert close in loc.in_proximity()
|
||||
|
||||
@ -60,9 +52,7 @@ def test_not_found_in_proximity_location():
|
||||
lon = -69.234
|
||||
loc = GeoLocation.objects.create(lat=lat, lon=lon, altitude=60)
|
||||
|
||||
far = GeoLocation.objects.create(
|
||||
lat=lat + 0.0002, lon=lon - 0.0001, altitude=60
|
||||
)
|
||||
far = GeoLocation.objects.create(lat=lat + 0.0002, lon=lon - 0.0001, altitude=60)
|
||||
assert far not in loc.in_proximity()
|
||||
|
||||
|
||||
|
||||
@ -109,9 +109,9 @@ class Artist(TimeStampedModel):
|
||||
def scrobbles(self):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
return Scrobble.objects.filter(
|
||||
track__in=self.track_set.all()
|
||||
).order_by("-timestamp")
|
||||
return Scrobble.objects.filter(track__in=self.track_set.all()).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
@ -130,9 +130,7 @@ class Artist(TimeStampedModel):
|
||||
if not self.allmusic_id or force:
|
||||
slug = get_allmusic_slug(self.name)
|
||||
if not slug:
|
||||
logger.info(
|
||||
"No allmusic link found", extra={"track_id": self.id}
|
||||
)
|
||||
logger.info("No allmusic link found", extra={"track_id": self.id})
|
||||
return
|
||||
self.allmusic_id = slug
|
||||
self.save(update_fields=["allmusic_id"])
|
||||
@ -141,9 +139,7 @@ class Artist(TimeStampedModel):
|
||||
if not self.bandcamp_id or force:
|
||||
slug = get_bandcamp_slug(self.name)
|
||||
if not slug:
|
||||
logger.info(
|
||||
"No bandcamp link found", extra={"track_id": self.id}
|
||||
)
|
||||
logger.info("No bandcamp link found", extra={"track_id": self.id})
|
||||
return
|
||||
self.bandcamp_id = slug
|
||||
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
|
||||
album name or track name, but probably not both."""
|
||||
if album_name:
|
||||
logger.info(
|
||||
f"Looking for artist with name {name} and album {album_name}"
|
||||
)
|
||||
logger.info(f"Looking for artist with name {name} and album {album_name}")
|
||||
if track_name:
|
||||
logger.info(
|
||||
f"Looking for artist with name {name} and track {track_name}"
|
||||
)
|
||||
logger.info(f"Looking for artist with name {name} and track {track_name}")
|
||||
keys = {}
|
||||
|
||||
name = clean_artist_name(name)
|
||||
@ -310,9 +302,9 @@ class Album(TimeStampedModel):
|
||||
def scrobbles(self):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
return Scrobble.objects.filter(
|
||||
track__in=self.track_set.all()
|
||||
).order_by("-timestamp")
|
||||
return Scrobble.objects.filter(track__in=self.track_set.all()).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
|
||||
@property
|
||||
def primary_image_url(self) -> str:
|
||||
@ -349,9 +341,7 @@ class Album(TimeStampedModel):
|
||||
if self.album_artist and (not self.allmusic_id or force):
|
||||
slug = get_allmusic_slug(self.album_artist.name, self.name)
|
||||
if not slug:
|
||||
logger.info(
|
||||
f"No allmsuic link for {self} by {self.album_artist}"
|
||||
)
|
||||
logger.info(f"No allmsuic link for {self} by {self.album_artist}")
|
||||
return
|
||||
self.allmusic_id = slug
|
||||
self.save(update_fields=["allmusic_id"])
|
||||
@ -380,9 +370,7 @@ class Album(TimeStampedModel):
|
||||
try:
|
||||
Album.objects.filter(pk=self.pk).update(**album_data)
|
||||
except:
|
||||
logger.info(
|
||||
f"Could not save info for album {self} with data {album_data}"
|
||||
)
|
||||
logger.info(f"Could not save info for album {self} with data {album_data}")
|
||||
|
||||
def scrape_bandcamp(self, force=False) -> None:
|
||||
if not self.bandcamp_id or force:
|
||||
@ -404,13 +392,13 @@ class Album(TimeStampedModel):
|
||||
self.musicbrainz_id, includes=["artists", "release-groups"]
|
||||
)
|
||||
if not self.musicbrainz_releasegroup_id:
|
||||
self.musicbrainz_releasegroup_id = mb_data["release"][
|
||||
"release-group"
|
||||
]["id"]
|
||||
self.musicbrainz_releasegroup_id = mb_data["release"]["release-group"][
|
||||
"id"
|
||||
]
|
||||
if not self.musicbrainz_albumartist_id:
|
||||
self.musicbrainz_albumartist_id = mb_data["release"][
|
||||
"artist-credit"
|
||||
][0]["artist"]["id"]
|
||||
self.musicbrainz_albumartist_id = mb_data["release"]["artist-credit"][
|
||||
0
|
||||
]["artist"]["id"]
|
||||
if not self.year:
|
||||
try:
|
||||
self.year = mb_data["release"]["date"][0:4]
|
||||
@ -435,10 +423,7 @@ class Album(TimeStampedModel):
|
||||
if not new_artist:
|
||||
for t in self.track_set.all():
|
||||
self.artists.add(t.artist)
|
||||
if (
|
||||
not self.cover_image
|
||||
or self.cover_image == "default-image-replace-me"
|
||||
):
|
||||
if not self.cover_image or self.cover_image == "default-image-replace-me":
|
||||
self.fetch_artwork()
|
||||
self.fix_album_artist()
|
||||
self.scrape_theaudiodb()
|
||||
@ -448,20 +433,15 @@ class Album(TimeStampedModel):
|
||||
if not self.cover_image and not force:
|
||||
if self.musicbrainz_id:
|
||||
try:
|
||||
img_data = musicbrainzngs.get_image_front(
|
||||
self.musicbrainz_id
|
||||
)
|
||||
img_data = musicbrainzngs.get_image_front(self.musicbrainz_id)
|
||||
name = f"{self.name}_{self.uuid}.jpg"
|
||||
self.cover_image = ContentFile(img_data, name=name)
|
||||
logger.info(f"Setting image to {name}")
|
||||
except musicbrainzngs.ResponseError:
|
||||
logger.warning(
|
||||
f"No cover art found for {self.name} by release"
|
||||
)
|
||||
logger.warning(f"No cover art found for {self.name} by release")
|
||||
|
||||
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"
|
||||
) and self.musicbrainz_releasegroup_id:
|
||||
try:
|
||||
img_data = musicbrainzngs.get_release_group_image_front(
|
||||
@ -522,9 +502,7 @@ class Album(TimeStampedModel):
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls, name: str, artist_name: str) -> "Album":
|
||||
logger.info(
|
||||
f"Looking for album with name {name} and artist_name {artist_name}"
|
||||
)
|
||||
logger.info(f"Looking for album with name {name} and artist_name {artist_name}")
|
||||
artist = Artist.find_or_create(artist_name, album_name=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:
|
||||
alt_name = name
|
||||
|
||||
album = Album.objects.filter(
|
||||
musicbrainz_id=album_dict.get("mbid")
|
||||
).first()
|
||||
album = Album.objects.filter(musicbrainz_id=album_dict.get("mbid")).first()
|
||||
|
||||
if not album:
|
||||
year = None
|
||||
@ -583,9 +559,7 @@ class Album(TimeStampedModel):
|
||||
album = Album.objects.create(
|
||||
name=found_name,
|
||||
musicbrainz_id=album_dict.get("mbid"),
|
||||
musicbrainz_releasegroup_id=album_dict.get(
|
||||
"release_group_mbid"
|
||||
),
|
||||
musicbrainz_releasegroup_id=album_dict.get("release_group_mbid"),
|
||||
year=year,
|
||||
album_artist=artist,
|
||||
alt_names=alt_name,
|
||||
@ -666,9 +640,7 @@ class Track(ScrobblableMixin):
|
||||
album = None
|
||||
if album_name:
|
||||
logger.info("Looking up album for: {album_name}")
|
||||
album = Album.find_or_create(
|
||||
name=album_name, artist_name=artist_name
|
||||
)
|
||||
album = Album.find_or_create(name=album_name, artist_name=artist_name)
|
||||
artist = album.album_artist
|
||||
else:
|
||||
artist = Artist.find_or_create(artist_name, track_name=title)
|
||||
@ -709,15 +681,11 @@ class Track(ScrobblableMixin):
|
||||
},
|
||||
)
|
||||
try:
|
||||
mbid, length = get_recording_mbid_exact(
|
||||
title, artist_name, album_name
|
||||
)
|
||||
mbid, length = get_recording_mbid_exact(title, artist_name, album_name)
|
||||
except Exception:
|
||||
print("No musicbrainz result found, cannot enrich")
|
||||
return track
|
||||
track.base_run_time_seconds = run_time_seconds or int(
|
||||
length / 1000
|
||||
)
|
||||
track.base_run_time_seconds = run_time_seconds or int(length / 1000)
|
||||
track.musicbrainz_id = mbid
|
||||
if commit:
|
||||
track.save()
|
||||
|
||||
@ -29,9 +29,7 @@ def lookup_album_from_mb(musicbrainz_id: str) -> dict:
|
||||
"album": {
|
||||
"name": release_data.get("title"),
|
||||
"musicbrainz_id": musicbrainz_id,
|
||||
"musicbrainz_releasegroup_id": release_data.get(
|
||||
"release-group"
|
||||
).get("id"),
|
||||
"musicbrainz_releasegroup_id": release_data.get("release-group").get("id"),
|
||||
"musicbrainz_albumaritist_id": primary_artist.get("id"),
|
||||
"year": release_data.get("year")[0:4],
|
||||
},
|
||||
@ -56,13 +54,11 @@ def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
|
||||
top_result = {}
|
||||
|
||||
try:
|
||||
top_result = musicbrainzngs.search_releases(
|
||||
release_name, artist=artist_name
|
||||
)["release-list"][0]
|
||||
top_result = musicbrainzngs.search_releases(release_name, artist=artist_name)[
|
||||
"release-list"
|
||||
][0]
|
||||
except IndexError:
|
||||
logger.info(
|
||||
f"No release found on MB for {artist_name} and {release_name}"
|
||||
)
|
||||
logger.info(f"No release found on MB for {artist_name} and {release_name}")
|
||||
|
||||
score = int(top_result.get("ext:score", 0))
|
||||
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:
|
||||
|
||||
try:
|
||||
top_result = musicbrainzngs.search_artists(artist=artist_name)[
|
||||
"artist-list"
|
||||
][0]
|
||||
top_result = musicbrainzngs.search_artists(artist=artist_name)["artist-list"][0]
|
||||
except IndexError:
|
||||
return {}
|
||||
score = int(top_result.get("ext:score"))
|
||||
@ -204,9 +198,7 @@ def get_recording_mbid_exact(
|
||||
|
||||
for track in tracks:
|
||||
if track["recording"]["title"].lower() == track_title.lower():
|
||||
return track["recording"]["id"], int(
|
||||
track["recording"]["length"]
|
||||
)
|
||||
return track["recording"]["id"], int(track["recording"]["length"])
|
||||
|
||||
raise Exception("No recording found")
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
@ -224,9 +216,7 @@ def get_artist_metadata_extended(artist_name, strict=True):
|
||||
"""
|
||||
try:
|
||||
# Step 1: Search for artist
|
||||
search_results = musicbrainzngs.search_artists(
|
||||
artist=artist_name, limit=5
|
||||
)
|
||||
search_results = musicbrainzngs.search_artists(artist=artist_name, limit=5)
|
||||
for artist in search_results.get("artist-list", []):
|
||||
if not strict or artist["name"].lower() == artist_name.lower():
|
||||
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"]
|
||||
all_artists = [
|
||||
ac["artist"]["name"]
|
||||
for ac in release["artist-credit"]
|
||||
if "artist" in ac
|
||||
ac["artist"]["name"] for ac in release["artist-credit"] if "artist" in ac
|
||||
]
|
||||
|
||||
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"]:
|
||||
release_date = parse_date(release.get("date"))
|
||||
if release_date:
|
||||
valid_candidates.append(
|
||||
(recording["id"], release, release_date)
|
||||
)
|
||||
valid_candidates.append((recording["id"], release, release_date))
|
||||
|
||||
if not valid_candidates:
|
||||
return None
|
||||
|
||||
@ -6,9 +6,15 @@ import requests
|
||||
from django.conf import settings
|
||||
|
||||
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_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="
|
||||
ARTIST_SEARCH_URL = (
|
||||
f"https://www.theaudiodb.com/api/v1/json/{THEAUDIODB_API_KEY}/search.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__)
|
||||
|
||||
@ -94,14 +100,10 @@ def lookup_album_from_tadb(name: str, artist: str) -> dict:
|
||||
album_info["rateyourmusic_id"] = album.get("strRateYourMusicID")
|
||||
|
||||
if album.get("intYearReleased"):
|
||||
album_info["theaudiodb_year_released"] = float(
|
||||
album.get("intYearReleased")
|
||||
)
|
||||
album_info["theaudiodb_year_released"] = float(album.get("intYearReleased"))
|
||||
if album.get("intScore"):
|
||||
album_info["theaudiodb_score"] = float(album.get("intScore"))
|
||||
if album.get("intScoreVotes"):
|
||||
album_info["theaudiodb_score_votes"] = int(
|
||||
album.get("intScoreVotes")
|
||||
)
|
||||
album_info["theaudiodb_score_votes"] = int(album.get("intScoreVotes"))
|
||||
|
||||
return album_info
|
||||
|
||||
@ -88,9 +88,7 @@ def condense_albums(commit: bool = False):
|
||||
track.albums.add(dup_track.album)
|
||||
|
||||
# Find out if this track appears more than once
|
||||
duplicates = Track.objects.filter(
|
||||
title=track.title, artist=track.artist
|
||||
)
|
||||
duplicates = Track.objects.filter(title=track.title, artist=track.artist)
|
||||
if duplicates.count() > 1:
|
||||
logger.info(f"Track appears more than once, condensing: {track}")
|
||||
|
||||
@ -98,9 +96,7 @@ def condense_albums(commit: bool = False):
|
||||
# Find all scrobbles
|
||||
duplicate_ids = duplicates.values_list("id", flat=True)
|
||||
scrobbles = Scrobble.objects.filter(track_id__in=duplicate_ids)
|
||||
logger.info(
|
||||
f"Found {scrobbles.count()} scrobbles to merge onto {track}"
|
||||
)
|
||||
logger.info(f"Found {scrobbles.count()} scrobbles to merge onto {track}")
|
||||
if commit:
|
||||
scrobbles.update(track=track)
|
||||
track.albums.add(*list(set(albums_to_add)))
|
||||
|
||||
@ -5,9 +5,7 @@ app_name = "people"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"people/", views.PersonCreateUpdateView.as_view(), name="person_form"
|
||||
),
|
||||
path("people/", views.PersonCreateUpdateView.as_view(), name="person_form"),
|
||||
path(
|
||||
"people/<int:pk>/edit/",
|
||||
views.PersonUpdateView.as_view(),
|
||||
|
||||
@ -39,9 +39,7 @@ class Producer(TimeStampedModel):
|
||||
class Podcast(TimeStampedModel):
|
||||
name = models.CharField(max_length=255)
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
producer = models.ForeignKey(
|
||||
Producer, on_delete=models.DO_NOTHING, **BNULL
|
||||
)
|
||||
producer = models.ForeignKey(Producer, on_delete=models.DO_NOTHING, **BNULL)
|
||||
podcastindex_id = models.CharField(max_length=100, **BNULL)
|
||||
owner = models.CharField(max_length=150, *BNULL)
|
||||
description = models.TextField(**BNULL)
|
||||
@ -123,9 +121,7 @@ class PodcastEpisode(ScrobblableMixin):
|
||||
mopidy_uri = models.CharField(max_length=255, **BNULL)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid}
|
||||
)
|
||||
return reverse("podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid})
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
||||
@ -14,9 +14,7 @@ PODCASTINDEX_API_SECRET = getattr(settings, "PODCASTINDEX_API_SECRET")
|
||||
def get_auth_headers():
|
||||
now = int(time.time())
|
||||
hash_data = hashlib.sha1(
|
||||
(PODCASTINDEX_API_KEY + PODCASTINDEX_API_SECRET + str(now)).encode(
|
||||
"utf-8"
|
||||
)
|
||||
(PODCASTINDEX_API_KEY + PODCASTINDEX_API_SECRET + str(now)).encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
return {
|
||||
|
||||
@ -53,9 +53,7 @@ def fetch_metadata_from_rss(uri: str) -> dict[str, Any]:
|
||||
if isinstance(feed.feed.itunes_owner, dict)
|
||||
else feed.feed.itunes_owner
|
||||
)
|
||||
podcast_other = feed.feed.get("managingeditor") or feed.feed.get(
|
||||
"copyright"
|
||||
)
|
||||
podcast_other = feed.feed.get("managingeditor") or feed.feed.get("copyright")
|
||||
except AttributeError:
|
||||
podcast_owner = 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_description": getattr(feed.feed, "description", ""),
|
||||
# "podcast_link": getattr(feed.feed, "link", ""),
|
||||
"podcast_producer": podcast_publisher
|
||||
or podcast_owner
|
||||
or podcast_other,
|
||||
"podcast_producer": podcast_publisher or podcast_owner or podcast_other,
|
||||
}
|
||||
|
||||
for entry in feed.entries:
|
||||
@ -116,8 +112,7 @@ def parse_mopidy_uri(uri: str) -> dict[str, Any]:
|
||||
# Beacuse we have epsiode numbers on
|
||||
podcast_data["pub_date"] = parse(
|
||||
episode_str[
|
||||
episode_num_pad : len(PODCAST_DATE_FORMAT)
|
||||
+ episode_num_pad
|
||||
episode_num_pad : len(PODCAST_DATE_FORMAT) + episode_num_pad
|
||||
]
|
||||
)
|
||||
except ParserError:
|
||||
@ -129,9 +124,7 @@ def parse_mopidy_uri(uri: str) -> dict[str, Any]:
|
||||
if podcast_data["episode_num"]:
|
||||
gap_to_strip += episode_num_pad
|
||||
|
||||
podcast_data["title"] = (
|
||||
episode_str[gap_to_strip:].replace("-", " ").strip()
|
||||
)
|
||||
podcast_data["title"] = episode_str[gap_to_strip:].replace("-", " ").strip()
|
||||
|
||||
return podcast_data
|
||||
|
||||
|
||||
@ -3,9 +3,7 @@ from datetime import datetime
|
||||
import pytz
|
||||
|
||||
ALL_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
|
||||
COMMON_TIMEZONE_CHOICES = tuple(
|
||||
zip(pytz.common_timezones, pytz.common_timezones)
|
||||
)
|
||||
COMMON_TIMEZONE_CHOICES = tuple(zip(pytz.common_timezones, pytz.common_timezones))
|
||||
PRETTY_TIMEZONE_CHOICES = []
|
||||
|
||||
for tz in pytz.common_timezones:
|
||||
|
||||
@ -5,7 +5,5 @@ app_name = "profiles"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"settings/", views.ProfileFormView.as_view(), name="profile_settings"
|
||||
),
|
||||
path("settings/", views.ProfileFormView.as_view(), name="profile_settings"),
|
||||
]
|
||||
|
||||
@ -82,15 +82,9 @@ def fix_profile_historic_timezones(profile):
|
||||
|
||||
profile.timezone_change_log = ""
|
||||
profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n"
|
||||
profile.timezone_change_log += (
|
||||
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"{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"{camp_tz} - {pendulum.parse(camp)}\n"
|
||||
profile.timezone_change_log += f"{home_tz} - {pendulum.parse(camp_end)}\n"
|
||||
profile.timezone_change_log += f"{summer_tz} - {pendulum.parse(summer)}\n"
|
||||
|
||||
@ -110,9 +110,7 @@ class Puzzle(ScrobblableMixin):
|
||||
(
|
||||
manufacturer,
|
||||
_created,
|
||||
) = PuzzleManufacturer.objects.get_or_create(
|
||||
name=manufacturer_name
|
||||
)
|
||||
) = PuzzleManufacturer.objects.get_or_create(name=manufacturer_name)
|
||||
puzzle_dict["manufacturer_id"] = manufacturer.id
|
||||
|
||||
genres = puzzle_dict.pop("genres", None)
|
||||
@ -126,9 +124,7 @@ class Puzzle(ScrobblableMixin):
|
||||
r = requests.get(cover_url)
|
||||
if r.status_code == 200:
|
||||
fname = f"{puzzle.title}_{puzzle.uuid}.jpg"
|
||||
puzzle.ipdb_image.save(
|
||||
fname, ContentFile(r.content), save=True
|
||||
)
|
||||
puzzle.ipdb_image.save(fname, ContentFile(r.content), save=True)
|
||||
|
||||
return puzzle
|
||||
|
||||
|
||||
@ -30,9 +30,7 @@ def get_title_from_soup(soup) -> str:
|
||||
def get_manufacturer_from_soup(soup) -> str:
|
||||
manufacturer = ""
|
||||
try:
|
||||
manufacturer = (
|
||||
soup.find(class_="infobox").div.contents[0].split("|")[0].strip()
|
||||
)
|
||||
manufacturer = soup.find(class_="infobox").div.contents[0].split("|")[0].strip()
|
||||
except AttributeError:
|
||||
pass
|
||||
except ValueError:
|
||||
@ -46,10 +44,7 @@ def get_pieces_count_from_soup(soup) -> int:
|
||||
pieces = 0
|
||||
try:
|
||||
pieces = int(
|
||||
soup.find(class_="infobox")
|
||||
.div.contents[0]
|
||||
.split("|")[1]
|
||||
.split(" ")[1]
|
||||
soup.find(class_="infobox").div.contents[0].split("|")[1].split(" ")[1]
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
@ -63,9 +58,7 @@ def get_pieces_count_from_soup(soup) -> int:
|
||||
def get_publish_year_from_soup(soup) -> int:
|
||||
year = 1900
|
||||
try:
|
||||
year = int(
|
||||
soup.find(class_="infobox").div.contents[0].split("|")[2].strip()
|
||||
)
|
||||
year = int(soup.find(class_="infobox").div.contents[0].split("|")[2].strip())
|
||||
except AttributeError:
|
||||
pass
|
||||
except ValueError:
|
||||
@ -152,9 +145,7 @@ def get_barcode_from_soup(soup) -> str:
|
||||
def get_image_url_from_soup(soup) -> str:
|
||||
url = ""
|
||||
try:
|
||||
url = (
|
||||
soup.find(class_="image-container").contents[0].contents[0]["src"]
|
||||
)
|
||||
url = soup.find(class_="image-container").contents[0].contents[0]["src"]
|
||||
except AttributeError:
|
||||
pass
|
||||
except ValueError:
|
||||
@ -210,9 +201,7 @@ def get_dimensions_from_soup(soup) -> str:
|
||||
def get_rating_from_soup(soup) -> str:
|
||||
rating = ""
|
||||
try:
|
||||
rating = float(
|
||||
soup.find(class_="num").get_text().strip("(").strip(")")
|
||||
)
|
||||
rating = float(soup.find(class_="num").get_text().strip("(").strip(")"))
|
||||
except AttributeError:
|
||||
rating = None
|
||||
except ValueError:
|
||||
@ -227,9 +216,7 @@ def get_puzzle_from_ipdb_id(ipdb_id: str) -> dict:
|
||||
puzzle_dict = {"ipdb_id": ipdb_id}
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warn(
|
||||
"Bad response from untappd.com", extra={"response": response}
|
||||
)
|
||||
logger.warn("Bad response from untappd.com", extra={"response": response})
|
||||
return puzzle_dict
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
|
||||
@ -70,7 +70,10 @@ class KoReaderImportAdmin(ImportBaseAdmin):
|
||||
@admin.register(RetroarchImport)
|
||||
class RetroarchImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
class RetroarchImportAdmin(ImportBaseAdmin): ...
|
||||
|
||||
|
||||
class RetroarchImportAdmin(ImportBaseAdmin):
|
||||
...
|
||||
|
||||
|
||||
@admin.register(Genre)
|
||||
|
||||
@ -63,9 +63,7 @@ class KoReaderImportSerializer(serializers.HyperlinkedModelSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AudioScrobblerTSVImportSerializer(
|
||||
serializers.HyperlinkedModelSerializer
|
||||
):
|
||||
class AudioScrobblerTSVImportSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = AudioScrobblerTSVImport
|
||||
fields = "__all__"
|
||||
|
||||
@ -13,9 +13,7 @@ def export_scrobbles(start_date=None, end_date=None, format="AS"):
|
||||
if start_date:
|
||||
end_query = Q(timestamp__lte=end_date)
|
||||
|
||||
scrobble_qs = Scrobble.objects.filter(
|
||||
start_query, end_query, track__isnull=False
|
||||
)
|
||||
scrobble_qs = Scrobble.objects.filter(start_query, end_query, track__isnull=False)
|
||||
headers = []
|
||||
extension = "tsv"
|
||||
delimiter = "\t"
|
||||
|
||||
@ -56,9 +56,7 @@ def django_form_field_from_type(field_type, required=True):
|
||||
if type(None) in args:
|
||||
required = False
|
||||
non_none_type = [arg for arg in args if arg is not type(None)][0]
|
||||
return django_form_field_from_type(
|
||||
non_none_type, required=required
|
||||
)
|
||||
return django_form_field_from_type(non_none_type, required=required)
|
||||
|
||||
# Determine actual type
|
||||
base_type = origin if origin else field_type
|
||||
@ -84,9 +82,7 @@ def form_from_dataclass(dataclass):
|
||||
continue
|
||||
|
||||
required = f.default is None and f.default_factory is None
|
||||
form_fields[f.name] = django_form_field_from_type(
|
||||
f.type, required=required
|
||||
)
|
||||
form_fields[f.name] = django_form_field_from_type(f.type, required=required)
|
||||
|
||||
if f.name in dataclass._excluded_fields:
|
||||
form_fields[f.name].disabled = True
|
||||
@ -101,9 +97,7 @@ def form_from_dataclass(dataclass):
|
||||
|
||||
def clean_notes(self):
|
||||
notes_str = self.cleaned_data.get("notes", "")
|
||||
return [
|
||||
line.strip() for line in notes_str.splitlines() if line.strip()
|
||||
]
|
||||
return [line.strip() for line in notes_str.splitlines() if line.strip()]
|
||||
|
||||
form_cls.clean_notes = clean_notes
|
||||
return form_cls
|
||||
|
||||
@ -43,9 +43,7 @@ def import_scrobbles_from_imap() -> list[Scrobble]:
|
||||
|
||||
try:
|
||||
message = email.message_from_bytes(msg_data[0][1])
|
||||
logger.info(
|
||||
"Processing email message", extra={"email_msg": message}
|
||||
)
|
||||
logger.info("Processing email message", extra={"email_msg": message})
|
||||
except IndexError:
|
||||
logger.info("No email message data found")
|
||||
return
|
||||
@ -75,9 +73,7 @@ def import_scrobbles_from_imap() -> list[Scrobble]:
|
||||
if filename.lower().endswith(".bgsplay"):
|
||||
# TODO Pull this out into a parse_pgsplay function
|
||||
try:
|
||||
parsed_json = json.loads(
|
||||
file_data.decode("utf-8")
|
||||
)
|
||||
parsed_json = json.loads(file_data.decode("utf-8"))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to parse JSON file",
|
||||
|
||||
@ -54,9 +54,7 @@ class LastFM:
|
||||
lfm_scrobble.get("timestamp")
|
||||
)
|
||||
timestamp = lfm_scrobble.get("timestamp")
|
||||
stop_timestamp = timestamp + timedelta(
|
||||
seconds=track.run_time_seconds
|
||||
)
|
||||
stop_timestamp = timestamp + timedelta(seconds=track.run_time_seconds)
|
||||
new_scrobble = Scrobble(
|
||||
user=self.vrobbler_user,
|
||||
timestamp=timestamp,
|
||||
@ -149,13 +147,11 @@ class LastFM:
|
||||
|
||||
# TODO figure out if this will actually work
|
||||
# timestamp = datetime.fromtimestamp(int(scrobble.timestamp), UTC)
|
||||
timestamp = datetime.utcfromtimestamp(
|
||||
int(scrobble.timestamp)
|
||||
).replace(tzinfo=pytz.utc)
|
||||
|
||||
logger.info(
|
||||
f"Scrobble appended to list for bulk create", extra=log_dict
|
||||
timestamp = datetime.utcfromtimestamp(int(scrobble.timestamp)).replace(
|
||||
tzinfo=pytz.utc
|
||||
)
|
||||
|
||||
logger.info(f"Scrobble appended to list for bulk create", extra=log_dict)
|
||||
scrobbles.append(
|
||||
{
|
||||
"artist": artist,
|
||||
|
||||
@ -20,9 +20,7 @@ def import_from_webdav_for_all_users(restart=False):
|
||||
webdav_pass__isnull=False,
|
||||
webdav_auto_import=True,
|
||||
).values_list("user_id", flat=True)
|
||||
logger.info(
|
||||
f"Start import of {webdav_enabled_user_ids.count()} webdav accounts"
|
||||
)
|
||||
logger.info(f"Start import of {webdav_enabled_user_ids.count()} webdav accounts")
|
||||
|
||||
koreader_import_count = 0
|
||||
|
||||
|
||||
@ -24,6 +24,4 @@ class Command(BaseCommand):
|
||||
print(f"Deleted {scrobbles_found} zombie scrobbles")
|
||||
return
|
||||
|
||||
print(
|
||||
f"Found {scrobbles_found} zombie scrobbles, use --delete to remove them"
|
||||
)
|
||||
print(f"Found {scrobbles_found} zombie scrobbles, use --delete to remove them")
|
||||
|
||||
@ -41,8 +41,6 @@ class Command(BaseCommand):
|
||||
if not dry_run:
|
||||
scrobble.save(update_fields=["scrobble_log"])
|
||||
else:
|
||||
print(
|
||||
f"Scrobble {scrobble} scrobble_log updated to {old_data}"
|
||||
)
|
||||
print(f"Scrobble {scrobble} scrobble_log updated to {old_data}")
|
||||
|
||||
print(f"Migrated scrobble logs for {updated_scrobble_count} scrobbles")
|
||||
|
||||
@ -42,9 +42,7 @@ class ScrobbleNtfyNotification(ScrobbleNotification):
|
||||
def __init__(self, scrobble, **kwargs):
|
||||
super().__init__(scrobble)
|
||||
self.ntfy_str: str = f"{self.scrobble.media_obj}"
|
||||
self.click_url = self.url_tmpl.format(
|
||||
path=self.media_obj.get_absolute_url()
|
||||
)
|
||||
self.click_url = self.url_tmpl.format(path=self.media_obj.get_absolute_url())
|
||||
self.title = self.media_obj.strings.verb
|
||||
self.actions = ""
|
||||
if kwargs.get("end", False):
|
||||
@ -89,11 +87,7 @@ class MoodNtfyNotification(BasicNtfyNotification):
|
||||
self.title = "Mood Check-in!"
|
||||
|
||||
def send(self):
|
||||
if (
|
||||
self.profile
|
||||
and self.profile.ntfy_enabled
|
||||
and self.profile.ntfy_url
|
||||
):
|
||||
if self.profile and self.profile.ntfy_enabled and self.profile.ntfy_url:
|
||||
requests.post(
|
||||
self.profile.ntfy_url,
|
||||
data=self.ntfy_str.encode(encoding="utf-8"),
|
||||
|
||||
@ -37,9 +37,7 @@ User = get_user_model()
|
||||
|
||||
|
||||
def timestamp_user_tz_to_utc(timestamp: int, user_tz: ZoneInfo) -> datetime:
|
||||
return user_tz.localize(datetime.utcfromtimestamp(timestamp)).astimezone(
|
||||
pytz.utc
|
||||
)
|
||||
return user_tz.localize(datetime.utcfromtimestamp(timestamp)).astimezone(pytz.utc)
|
||||
|
||||
|
||||
def convert_to_seconds(run_time: str) -> int:
|
||||
@ -150,9 +148,7 @@ def import_lastfm_for_all_users(restart=False):
|
||||
)
|
||||
continue
|
||||
|
||||
lfm_client = LastFM(
|
||||
user=get_user_model().objects.filter(id=user_id).first()
|
||||
)
|
||||
lfm_client = LastFM(user=get_user_model().objects.filter(id=user_id).first())
|
||||
|
||||
has_scrobbles = lfm_client.get_last_scrobbles(
|
||||
time_from=last_processed, check=True
|
||||
@ -237,9 +233,7 @@ def import_from_webdav_for_all_users(restart=False):
|
||||
webdav_pass__isnull=False,
|
||||
webdav_auto_import=True,
|
||||
).values_list("user_id", flat=True)
|
||||
logger.info(
|
||||
f"start import of {webdav_enabled_user_ids.count()} webdav accounts"
|
||||
)
|
||||
logger.info(f"start import of {webdav_enabled_user_ids.count()} webdav accounts")
|
||||
|
||||
koreader_import_count = 0
|
||||
|
||||
@ -348,11 +342,7 @@ def send_mood_checkin_reminders() -> int:
|
||||
|
||||
def extract_domain(url):
|
||||
parsed_url = urlparse(url)
|
||||
domain = (
|
||||
parsed_url.netloc.split(".")[-2]
|
||||
+ "."
|
||||
+ parsed_url.netloc.split(".")[-1]
|
||||
)
|
||||
domain = parsed_url.netloc.split(".")[-2] + "." + parsed_url.netloc.split(".")[-1]
|
||||
return domain
|
||||
|
||||
|
||||
@ -367,20 +357,14 @@ def fix_playback_position_seconds(
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
scrobble.media_type == "Track"
|
||||
and scrobble.media_obj.run_time_seconds
|
||||
):
|
||||
if scrobble.media_type == "Track" and scrobble.media_obj.run_time_seconds:
|
||||
too_long = (
|
||||
scrobble.playback_position_seconds
|
||||
> scrobble.media_obj.run_time_seconds
|
||||
scrobble.playback_position_seconds > scrobble.media_obj.run_time_seconds
|
||||
)
|
||||
zero = scrobble.playback_position_seconds == 0
|
||||
null = not scrobble.playback_position_seconds
|
||||
if too_long or zero or null:
|
||||
scrobble.playback_position_seconds = (
|
||||
scrobble.media_obj.run_time_seconds
|
||||
)
|
||||
scrobble.playback_position_seconds = scrobble.media_obj.run_time_seconds
|
||||
updated_scrobbles.append(scrobble)
|
||||
if commit:
|
||||
scrobble.save(update_fields=["playback_position_seconds"])
|
||||
|
||||
@ -43,9 +43,7 @@ class Sport(TheSportsDbMixin):
|
||||
|
||||
@property
|
||||
def default_event_run_time(self):
|
||||
default_run_time = getattr(
|
||||
settings, "DEFAULT_EVENT_RUNTIME_SECONDS", 14400
|
||||
)
|
||||
default_run_time = getattr(settings, "DEFAULT_EVENT_RUNTIME_SECONDS", 14400)
|
||||
if self.default_event_run_time_seconds:
|
||||
default_run_time = self.default_event_run_time_seconds
|
||||
return default_run_time
|
||||
@ -124,7 +122,9 @@ class SportEvent(ScrobblableMixin):
|
||||
)
|
||||
|
||||
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):
|
||||
return reverse("sports:event_detail", kwargs={"slug": self.uuid})
|
||||
@ -206,14 +206,10 @@ class SportEvent(ScrobblableMixin):
|
||||
round.save(update_fields=["name"])
|
||||
|
||||
players_list = get_players_from_event(event_name)
|
||||
player_one = Player.objects.filter(
|
||||
name__icontains=players_list[0]
|
||||
).first()
|
||||
player_one = Player.objects.filter(name__icontains=players_list[0]).first()
|
||||
if not player_one:
|
||||
player_one = Player.objects.create(name=players_list[0])
|
||||
player_two = Player.objects.filter(
|
||||
name__icontains=players_list[1]
|
||||
).first()
|
||||
player_two = Player.objects.filter(name__icontains=players_list[1]).first()
|
||||
if not player_two:
|
||||
player_two = Player.objects.create(name=players_list[1])
|
||||
|
||||
|
||||
@ -21,9 +21,7 @@ def lookup_event_from_thesportsdb(event_id: str) -> dict:
|
||||
|
||||
if not event or type(event) != dict:
|
||||
return {}
|
||||
sport, _created = Sport.objects.get_or_create(
|
||||
thesportsdb_id=event.get("strSport")
|
||||
)
|
||||
sport, _created = Sport.objects.get_or_create(thesportsdb_id=event.get("strSport"))
|
||||
|
||||
try:
|
||||
start = parse(event.get("strTimestamp"))
|
||||
|
||||
@ -29,9 +29,7 @@ def get_title_from_labels(
|
||||
|
||||
|
||||
def convert_old_orgmode_log_to_new(commit=False):
|
||||
scrobbles = Scrobble.objects.filter(
|
||||
source="Org-mode", log__has_key="drawers"
|
||||
)
|
||||
scrobbles = Scrobble.objects.filter(source="Org-mode", log__has_key="drawers")
|
||||
for scrobble in scrobbles:
|
||||
scrobble.log["title"] = scrobble.log.pop("description")
|
||||
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):
|
||||
scrobbles = Scrobble.objects.filter(
|
||||
source="Todoist", log__has_key="todoist_type"
|
||||
)
|
||||
scrobbles = Scrobble.objects.filter(source="Todoist", log__has_key="todoist_type")
|
||||
for scrobble in scrobbles:
|
||||
scrobble.log["title"] = scrobble.log.pop("description")
|
||||
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):
|
||||
scrobbles = Scrobble.objects.filter(
|
||||
board_game__isnull=False, log__has_key="notes"
|
||||
)
|
||||
scrobbles = Scrobble.objects.filter(board_game__isnull=False, log__has_key="notes")
|
||||
for scrobble in scrobbles:
|
||||
if isinstance(scrobble.log.get("notes"), str):
|
||||
scrobble.log["notes"] = [scrobble.log.pop("notes")]
|
||||
|
||||
@ -34,20 +34,14 @@ def todoist_webhook(request):
|
||||
is_note_type = todoist_tyllll = "note"
|
||||
new_labels = event_data.get("labels", [])
|
||||
old_labels = (
|
||||
post_data.get("event_data_extra", {})
|
||||
.get("old_item", {})
|
||||
.get("labels", [])
|
||||
post_data.get("event_data_extra", {}).get("old_item", {}).get("labels", [])
|
||||
)
|
||||
# TODO Don't hard code status strings in here
|
||||
is_updated = todoist_event in ["updated"]
|
||||
is_added = todoist_event in ["added"]
|
||||
|
||||
task_started = (
|
||||
"inprogress" in new_labels and "inprogress" not in old_labels
|
||||
)
|
||||
task_stopped = (
|
||||
"inprogress" not in new_labels and "inprogress" in old_labels
|
||||
)
|
||||
task_started = "inprogress" in new_labels and "inprogress" not 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):
|
||||
todoist_task = {
|
||||
@ -71,9 +65,7 @@ def todoist_webhook(request):
|
||||
"updated_at": task_data.get("updated_at"),
|
||||
"details": task_data.get("description"),
|
||||
"notes": event_data.get("content"),
|
||||
"is_deleted": (
|
||||
True if event_data.get("is_deleted") == "true" else False
|
||||
),
|
||||
"is_deleted": (True if event_data.get("is_deleted") == "true" else False),
|
||||
}
|
||||
|
||||
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:
|
||||
scrobble = todoist_scrobble_update_task(
|
||||
todoist_note, user_profile.user_id
|
||||
)
|
||||
scrobble = todoist_scrobble_update_task(todoist_note, user_profile.user_id)
|
||||
|
||||
if not scrobble:
|
||||
logger.info(
|
||||
|
||||
@ -72,6 +72,6 @@ class Trail(ScrobblableMixin):
|
||||
|
||||
def scrobbles(self, user_id):
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
return Scrobble.objects.filter(
|
||||
user_id=user_id, life_event=self
|
||||
).order_by("-timestamp")
|
||||
return Scrobble.objects.filter(user_id=user_id, life_event=self).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
|
||||
@ -37,9 +37,7 @@ def lookup_game_from_hltb(name_or_id: str) -> Optional[dict]:
|
||||
found_games = []
|
||||
for g in results:
|
||||
found_games.append(f"{g.game_name} ({g.game_id})")
|
||||
logger.info(
|
||||
f"Found more than one match {found_games}, taking {hltb_game}"
|
||||
)
|
||||
logger.info(f"Found more than one match {found_games}, taking {hltb_game}")
|
||||
|
||||
game_dict = {
|
||||
"title": hltb_game.game_name,
|
||||
|
||||
@ -29,9 +29,7 @@ User = get_user_model()
|
||||
|
||||
|
||||
def get_igdb_token() -> str:
|
||||
token_url = REFRESH_TOKEN_URL.format(
|
||||
id=IGDB_CLIENT_ID, secret=IGDB_CLIENT_SECRET
|
||||
)
|
||||
token_url = REFRESH_TOKEN_URL.format(id=IGDB_CLIENT_ID, secret=IGDB_CLIENT_SECRET)
|
||||
response = requests.post(token_url)
|
||||
results = json.loads(response.content)
|
||||
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")
|
||||
screenshot_url = None
|
||||
if "screenshots" in game.keys():
|
||||
screenshot_url = "https:" + game.get("screenshots")[0].get(
|
||||
"url"
|
||||
).replace("t_thumb", "t_screenshot_big_2x")
|
||||
screenshot_url = "https:" + game.get("screenshots")[0].get("url").replace(
|
||||
"t_thumb", "t_screenshot_big_2x"
|
||||
)
|
||||
cover_url = None
|
||||
if "cover" in game.keys():
|
||||
cover_url = "https:" + game.get("cover").get("url").replace(
|
||||
|
||||
@ -58,9 +58,7 @@ class VideoGamePlatform(TimeStampedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"videogames:platform_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("videogames:platform_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
|
||||
class VideoGameCollection(TimeStampedModel):
|
||||
@ -85,9 +83,7 @@ class VideoGameCollection(TimeStampedModel):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"videogames:collection_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("videogames:collection_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
|
||||
class VideoGame(LongPlayScrobblableMixin):
|
||||
@ -188,9 +184,7 @@ class VideoGame(LongPlayScrobblableMixin):
|
||||
return url
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"videogames:videogame_detail", kwargs={"slug": self.uuid}
|
||||
)
|
||||
return reverse("videogames:videogame_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
def hltb_link(self):
|
||||
return f"https://howlongtobeat.com/game/{self.hltb_id}"
|
||||
|
||||
@ -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")
|
||||
raise UserNotFound
|
||||
|
||||
game_logs = load_game_data(
|
||||
playlog_path, pytz.timezone(user.profile.timezone)
|
||||
)
|
||||
game_logs = load_game_data(playlog_path, pytz.timezone(user.profile.timezone))
|
||||
found_game = None
|
||||
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
|
||||
end_datetime = game_data.get("last_played")
|
||||
found_scrobble = found_game.scrobble_set.filter(
|
||||
stop_timestamp=end_datetime
|
||||
)
|
||||
found_scrobble = found_game.scrobble_set.filter(stop_timestamp=end_datetime)
|
||||
if found_scrobble:
|
||||
logger.info(f"Skipping scrobble for game {found_game.id}")
|
||||
continue
|
||||
|
||||
@ -8,7 +8,9 @@ from vrobbler.apps.videogames.exceptions import GameNotFound
|
||||
|
||||
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):
|
||||
|
||||
@ -32,9 +32,7 @@ def get_or_create_videogame(
|
||||
platforms = game_dict.get("platforms", [])
|
||||
if platforms:
|
||||
for platform in game_dict.get("platforms", []):
|
||||
p, _created = VideoGamePlatform.objects.get_or_create(
|
||||
name=platform
|
||||
)
|
||||
p, _created = VideoGamePlatform.objects.get_or_create(name=platform)
|
||||
platform_ids.append(p.id)
|
||||
game_dict.pop("platforms")
|
||||
|
||||
@ -98,9 +96,7 @@ def get_or_create_videogame(
|
||||
return game
|
||||
|
||||
|
||||
def load_game_data_from_igdb(
|
||||
game_id: int, igdb_id: str = ""
|
||||
) -> Optional[VideoGame]:
|
||||
def load_game_data_from_igdb(game_id: int, igdb_id: str = "") -> Optional[VideoGame]:
|
||||
"""Look up game, if it doesn't exist, lookup data from igdb"""
|
||||
game = VideoGame.objects.filter(id=game_id).first()
|
||||
if not game:
|
||||
|
||||
@ -15,9 +15,7 @@ class SeriesSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
|
||||
class VideoSerializer(serializers.HyperlinkedModelSerializer):
|
||||
channel = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Channel.objects.all()
|
||||
)
|
||||
channel = serializers.PrimaryKeyRelatedField(queryset=Channel.objects.all())
|
||||
tv_series = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Series.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
|
||||
@ -8,22 +8,18 @@ class Command(BaseCommand):
|
||||
help = "Find or create a Video by ID and output it as JSON"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"video_id", type=str, help="The video ID to find or create"
|
||||
)
|
||||
parser.add_argument("video_id", type=str, help="The video ID to find or create")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
instance = Video.find_or_create(
|
||||
options.get("video_id", ""), overwrite=True
|
||||
)
|
||||
instance = Video.find_or_create(options.get("video_id", ""), overwrite=True)
|
||||
data = json.loads(serializers.serialize("json", [instance]))[0]
|
||||
|
||||
# --- Enrich with series model ---
|
||||
if instance.tv_series_id:
|
||||
series_instance = instance.tv_series
|
||||
series_json = json.loads(
|
||||
serializers.serialize("json", [series_instance])
|
||||
)[0]
|
||||
series_json = json.loads(serializers.serialize("json", [series_instance]))[
|
||||
0
|
||||
]
|
||||
data["series"] = series_json # new nested field
|
||||
|
||||
if instance.channel_id:
|
||||
|
||||
@ -7,9 +7,7 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USER_AGENT = (
|
||||
"Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
|
||||
)
|
||||
USER_AGENT = "Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0"
|
||||
SKATEVIDEOSITE_URL = "https://www.skatevideosite.com"
|
||||
SKATEVIDEOSITE_SEARCH_URL = SKATEVIDEOSITE_URL + "/search/?q={title}"
|
||||
|
||||
|
||||
@ -18,9 +18,7 @@ TMDB_IMAGE_URL = "https://image.tmdb.org/t/p/original"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def lookup_video_from_tmdb(
|
||||
name_or_id: str, kind: str = "movie"
|
||||
) -> VideoMetadata:
|
||||
def lookup_video_from_tmdb(name_or_id: str, kind: str = "movie") -> VideoMetadata:
|
||||
from videos.models import Series
|
||||
|
||||
imdb_id = name_or_id
|
||||
|
||||
@ -26,14 +26,10 @@ def lookup_video_from_youtube(youtube_id: str) -> VideoMetadata:
|
||||
response = requests.get(url, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
"Bad response from Google", extra={"response": response}
|
||||
)
|
||||
logger.warning("Bad response from Google", extra={"response": response})
|
||||
return video_metadata
|
||||
|
||||
yt_metadata = (
|
||||
json.loads(response.content).get("items", [None])[0].get("snippet")
|
||||
)
|
||||
yt_metadata = json.loads(response.content).get("items", [None])[0].get("snippet")
|
||||
duration_iso8601 = (
|
||||
json.loads(response.content)
|
||||
.get("items", [None])[0]
|
||||
|
||||
@ -19,9 +19,7 @@ def clean_up_videos():
|
||||
try:
|
||||
video.save(update_fields=["imdb_id"])
|
||||
except IntegrityError:
|
||||
new_video = Video.objects.filter(
|
||||
imdb_id="tt" + video.imdb_id
|
||||
).first()
|
||||
new_video = Video.objects.filter(imdb_id="tt" + video.imdb_id).first()
|
||||
video.scrobble_set.all().update(video=new_video)
|
||||
video.delete()
|
||||
|
||||
|
||||
@ -32,9 +32,7 @@ class SeriesDetailView(LoginRequiredMixin, ChartContextMixin, generic.DetailView
|
||||
context_data = super().get_context_data(**kwargs)
|
||||
|
||||
context_data["scrobbles"] = self.object.scrobbles_for_user(user_id)
|
||||
next_episode_id = (
|
||||
self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
|
||||
)
|
||||
next_episode_id = self.object.last_scrobbled_episode(user_id).next_imdb_id or ""
|
||||
if self.object.is_episode_playing(user_id):
|
||||
next_episode_id = ""
|
||||
if next_episode_id:
|
||||
@ -96,10 +94,7 @@ class VideoListView(ScrobbleableListView):
|
||||
if channel:
|
||||
channels_this_week[channel.id] = {
|
||||
"channel": channel,
|
||||
"count": channels_this_week.get(channel.id, {}).get(
|
||||
"count", 0
|
||||
)
|
||||
+ 1,
|
||||
"count": channels_this_week.get(channel.id, {}).get("count", 0) + 1,
|
||||
}
|
||||
|
||||
channels_this_month = {}
|
||||
@ -108,9 +103,7 @@ class VideoListView(ScrobbleableListView):
|
||||
if channel:
|
||||
channels_this_month[channel.id] = {
|
||||
"channel": channel,
|
||||
"count": channels_this_month.get(channel.id, {}).get(
|
||||
"count", 0
|
||||
)
|
||||
"count": channels_this_month.get(channel.id, {}).get("count", 0)
|
||||
+ 1,
|
||||
}
|
||||
|
||||
|
||||
@ -39,9 +39,9 @@ class Domain(TimeStampedModel):
|
||||
def scrobbles_for_user(self, user_id):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
return Scrobble.objects.filter(
|
||||
web_page__domain=self, user_id=user_id
|
||||
).order_by("-timestamp")
|
||||
return Scrobble.objects.filter(web_page__domain=self, user_id=user_id).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
|
||||
|
||||
class WebPage(ScrobblableMixin):
|
||||
@ -136,9 +136,7 @@ class WebPage(ScrobblableMixin):
|
||||
|
||||
def scrobbles(self, user):
|
||||
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||
return Scrobble.objects.filter(user=user, web_page=self).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
return Scrobble.objects.filter(user=user, web_page=self).order_by("-timestamp")
|
||||
|
||||
def clean_title(self, title: str, save=True):
|
||||
if len(title.split("|")) > 1:
|
||||
@ -213,9 +211,7 @@ class WebPage(ScrobblableMixin):
|
||||
return
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info(
|
||||
"Website already exists in archive", extra={"url": self.url}
|
||||
)
|
||||
logger.info("Website already exists in archive", extra={"url": self.url})
|
||||
else:
|
||||
raise Exception(
|
||||
f"Failed to push URL to archivebox (Response {response.status_code})"
|
||||
|
||||
@ -40,10 +40,7 @@ class WebPageListView(ScrobbleableListView):
|
||||
if domain:
|
||||
domains_this_week[domain.id] = {
|
||||
"domain": domain,
|
||||
"count": domains_this_week.get(domain.id, {}).get(
|
||||
"count", 0
|
||||
)
|
||||
+ 1,
|
||||
"count": domains_this_week.get(domain.id, {}).get("count", 0) + 1,
|
||||
}
|
||||
|
||||
domains_this_month = {}
|
||||
@ -52,10 +49,7 @@ class WebPageListView(ScrobbleableListView):
|
||||
if domain:
|
||||
domains_this_month[domain.id] = {
|
||||
"domain": domain,
|
||||
"count": domains_this_month.get(domain.id, {}).get(
|
||||
"count", 0
|
||||
)
|
||||
+ 1,
|
||||
"count": domains_this_month.get(domain.id, {}).get("count", 0) + 1,
|
||||
}
|
||||
|
||||
context_data["domains_this_week"] = sorted(
|
||||
@ -82,9 +76,7 @@ class WebPageDetailView(ScrobbleableDetailView):
|
||||
return context
|
||||
|
||||
|
||||
class WebPageReadView(
|
||||
LoginRequiredMixin, generic.edit.FormView, generic.DetailView
|
||||
):
|
||||
class WebPageReadView(LoginRequiredMixin, generic.edit.FormView, generic.DetailView):
|
||||
model = WebPage
|
||||
slug_field = "uuid"
|
||||
template_name = "webpages/webpage_read.html"
|
||||
@ -95,11 +87,7 @@ class WebPageReadView(
|
||||
webpage = WebPage.objects.get(uuid=kwargs.get("slug"))
|
||||
latest_scrobble = webpage.scrobbles(user).last()
|
||||
if latest_scrobble.played_to_completion:
|
||||
redirect(
|
||||
reverse(
|
||||
"webpages:webpage_detail", kwargs={"slug": webpage.uuid}
|
||||
)
|
||||
)
|
||||
redirect(reverse("webpages:webpage_detail", kwargs={"slug": webpage.uuid}))
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def form_valid(self, *args):
|
||||
|
||||
@ -19,9 +19,7 @@ class HealthCheckMiddleware:
|
||||
is_db_connected = False
|
||||
else:
|
||||
is_db_connected = True
|
||||
logger.info(
|
||||
"[health-check]", extra={"is_db_connected": is_db_connected}
|
||||
)
|
||||
logger.info("[health-check]", extra={"is_db_connected": is_db_connected})
|
||||
if is_db_connected:
|
||||
return HttpResponse("ok")
|
||||
return self.get_response(request)
|
||||
|
||||
@ -37,9 +37,7 @@ TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
|
||||
|
||||
TAGGIT_CASE_INSENSITIVE = True
|
||||
|
||||
KEEP_DETAILED_SCROBBLE_LOGS = os.getenv(
|
||||
"VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False
|
||||
)
|
||||
KEEP_DETAILED_SCROBBLE_LOGS = os.getenv("VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False)
|
||||
|
||||
# Key must be 16, 24 or 32 bytes long and will be converted to a byte stream
|
||||
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
|
||||
DUMP_REQUEST_DATA = (
|
||||
os.getenv("VROBBLER_DUMP_REQUEST_DATA", "false").lower() in TRUTHY
|
||||
)
|
||||
DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", "false").lower() in TRUTHY
|
||||
|
||||
USDA_API_KEY = os.getenv("VROBBLER_USDA_API_KEY")
|
||||
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", "")
|
||||
GEOLOC_ACCURACY = os.getenv("VROBBLER_GEOLOC_ACCURACY", 3)
|
||||
GEOLOC_PROXIMITY = os.getenv("VROBBLER_GEOLOC_PROXIMITY", "0.0001")
|
||||
POINTS_FOR_MOVEMENT_HISTORY = os.getenv(
|
||||
"VROBBLER_POINTS_FOR_MOVEMENT_HISTORY", 3
|
||||
)
|
||||
POINTS_FOR_MOVEMENT_HISTORY = os.getenv("VROBBLER_POINTS_FOR_MOVEMENT_HISTORY", 3)
|
||||
TODOIST_CLIENT_ID = os.getenv("VROBBLER_TODOIST_CLIENT_ID", "")
|
||||
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")
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
os.getenv("VROBBLER_TRUSTED_ORIGINS", "http://localhost:8000")
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = [os.getenv("VROBBLER_TRUSTED_ORIGINS", "http://localhost:8000")]
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
|
||||
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
|
||||
@ -108,9 +100,7 @@ if REDIS_URL:
|
||||
else:
|
||||
print("Eagerly running all tasks")
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = (
|
||||
os.getenv("VROBBLER_SKIP_CELERY", "false").lower() in TRUTHY
|
||||
)
|
||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", "false").lower() in TRUTHY
|
||||
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
|
||||
CELERY_RESULT_BACKEND = "django-db"
|
||||
CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "America/New_York")
|
||||
@ -231,9 +221,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
if TESTING:
|
||||
DATABASES = {
|
||||
"default": dj_database_url.config(default="sqlite:///testdb.sqlite3")
|
||||
}
|
||||
DATABASES = {"default": dj_database_url.config(default="sqlite:///testdb.sqlite3")}
|
||||
|
||||
db_str = ""
|
||||
if "sqlite" in DATABASES["default"]["ENGINE"]:
|
||||
@ -269,9 +257,7 @@ REST_FRAMEWORK = {
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
],
|
||||
"DEFAULT_CONTENT_NEGOTIATION_CLASS": "vrobbler.negotiation.IgnoreClientContentNegotiation",
|
||||
"DEFAULT_FILTER_BACKENDS": [
|
||||
"django_filters.rest_framework.DjangoFilterBackend"
|
||||
],
|
||||
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 200,
|
||||
}
|
||||
@ -331,9 +317,7 @@ else:
|
||||
STATIC_ROOT = os.getenv(
|
||||
"VROBBLER_STATIC_ROOT", os.path.join(PROJECT_ROOT, "static")
|
||||
)
|
||||
MEDIA_ROOT = os.getenv(
|
||||
"VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media")
|
||||
)
|
||||
MEDIA_ROOT = os.getenv("VROBBLER_MEDIA_ROOT", os.path.join(PROJECT_ROOT, "media"))
|
||||
STATIC_URL = os.getenv("VROBBLER_STATIC_URL", "/static/")
|
||||
MEDIA_URL = os.getenv("VROBBLER_MEDIA_URL", "/media/")
|
||||
|
||||
@ -417,9 +401,7 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
|
||||
LOG_TO_CONSOLE = (
|
||||
os.getenv("VROBBLER_LOG_TO_CONSOLE", "false").lower() in TRUTHY
|
||||
)
|
||||
LOG_TO_CONSOLE = os.getenv("VROBBLER_LOG_TO_CONSOLE", "false").lower() in TRUTHY
|
||||
if LOG_TO_CONSOLE:
|
||||
LOGGING["loggers"]["django"]["handlers"] = ["console"]
|
||||
LOGGING["loggers"]["vrobbler"]["handlers"] = ["console"]
|
||||
|
||||
Reference in New Issue
Block a user