[scrobblers] Allow scrobbling food from the top bar
This commit is contained in:
@ -55,6 +55,7 @@ MANUAL_SCROBBLE_FNS = {
|
|||||||
"-p": "manual_scrobble_puzzle",
|
"-p": "manual_scrobble_puzzle",
|
||||||
"-l": "manual_scrobble_brickset",
|
"-l": "manual_scrobble_brickset",
|
||||||
"-c": "manual_scrobble_book",
|
"-c": "manual_scrobble_book",
|
||||||
|
"-f": "manual_scrobble_food",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class ScrobbleForm(forms.Form):
|
|||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "form-control form-control-dark w-100",
|
"class": "form-control form-control-dark w-100",
|
||||||
"placeholder": "Scrobble something (ttIMDB, -v Video Game title, -b Book title, -s TheSportsDB ID)",
|
"placeholder": "Scrobble something (ttIMDB, -v Video Game title, -b Book title, -s TheSportsDB ID, -f Food name - calories)",
|
||||||
"aria-label": "Scrobble something",
|
"aria-label": "Scrobble something",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -56,9 +56,7 @@ def django_form_field_from_type(field_type, required=True):
|
|||||||
if type(None) in args:
|
if type(None) in args:
|
||||||
required = False
|
required = False
|
||||||
non_none_type = [arg for arg in args if arg is not type(None)][0]
|
non_none_type = [arg for arg in args if arg is not type(None)][0]
|
||||||
return django_form_field_from_type(
|
return django_form_field_from_type(non_none_type, required=required)
|
||||||
non_none_type, required=required
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine actual type
|
# Determine actual type
|
||||||
base_type = origin if origin else field_type
|
base_type = origin if origin else field_type
|
||||||
@ -84,9 +82,7 @@ def form_from_dataclass(dataclass):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
required = f.default is None and f.default_factory is None
|
required = f.default is None and f.default_factory is None
|
||||||
form_fields[f.name] = django_form_field_from_type(
|
form_fields[f.name] = django_form_field_from_type(f.type, required=required)
|
||||||
f.type, required=required
|
|
||||||
)
|
|
||||||
|
|
||||||
if f.name in dataclass._excluded_fields:
|
if f.name in dataclass._excluded_fields:
|
||||||
form_fields[f.name].disabled = True
|
form_fields[f.name].disabled = True
|
||||||
@ -101,9 +97,7 @@ def form_from_dataclass(dataclass):
|
|||||||
|
|
||||||
def clean_notes(self):
|
def clean_notes(self):
|
||||||
notes_str = self.cleaned_data.get("notes", "")
|
notes_str = self.cleaned_data.get("notes", "")
|
||||||
return [
|
return [line.strip() for line in notes_str.splitlines() if line.strip()]
|
||||||
line.strip() for line in notes_str.splitlines() if line.strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
form_cls.clean_notes = clean_notes
|
form_cls.clean_notes = clean_notes
|
||||||
return form_cls
|
return form_cls
|
||||||
|
|||||||
@ -84,11 +84,7 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
|
|||||||
run_time_seconds=post_data.get("run_time", 900000),
|
run_time_seconds=post_data.get("run_time", 900000),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
album_id = (
|
album_id = Album.objects.filter(name=post_data.get("album", "")).first().id
|
||||||
Album.objects.filter(name=post_data.get("album", ""))
|
|
||||||
.first()
|
|
||||||
.id
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -108,17 +104,14 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
|
|||||||
user_id,
|
user_id,
|
||||||
source="Mopidy",
|
source="Mopidy",
|
||||||
playback_position_seconds=int(
|
playback_position_seconds=int(
|
||||||
post_data.get(MOPIDY_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1)
|
post_data.get(MOPIDY_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1) / 1000
|
||||||
/ 1000
|
|
||||||
),
|
),
|
||||||
status=post_data.get(MOPIDY_POST_KEYS.get("STATUS"), ""),
|
status=post_data.get(MOPIDY_POST_KEYS.get("STATUS"), ""),
|
||||||
log=log,
|
log=log,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def jellyfin_scrobble_media(
|
def jellyfin_scrobble_media(post_data: dict, user_id: int) -> Optional[Scrobble]:
|
||||||
post_data: dict, user_id: int
|
|
||||||
) -> Optional[Scrobble]:
|
|
||||||
media_type = Scrobble.MediaType.VIDEO
|
media_type = Scrobble.MediaType.VIDEO
|
||||||
if post_data.pop("ItemType", "") in JELLYFIN_AUDIO_ITEM_TYPES:
|
if post_data.pop("ItemType", "") in JELLYFIN_AUDIO_ITEM_TYPES:
|
||||||
media_type = Scrobble.MediaType.TRACK
|
media_type = Scrobble.MediaType.TRACK
|
||||||
@ -136,13 +129,12 @@ def jellyfin_scrobble_media(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
timestamp = parse(
|
timestamp = parse(post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"), "")).replace(
|
||||||
post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"), "")
|
tzinfo=pytz.utc
|
||||||
).replace(tzinfo=pytz.utc)
|
)
|
||||||
|
|
||||||
playback_position_seconds = int(
|
playback_position_seconds = int(
|
||||||
post_data.get(JELLYFIN_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1)
|
post_data.get(JELLYFIN_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1) / 10000000
|
||||||
/ 10000000
|
|
||||||
)
|
)
|
||||||
album_id = None
|
album_id = None
|
||||||
if media_type == Scrobble.MediaType.VIDEO:
|
if media_type == Scrobble.MediaType.VIDEO:
|
||||||
@ -153,16 +145,10 @@ def jellyfin_scrobble_media(
|
|||||||
title=post_data.get("Name", ""),
|
title=post_data.get("Name", ""),
|
||||||
artist_name=post_data.get("Artist", ""),
|
artist_name=post_data.get("Artist", ""),
|
||||||
album_name=post_data.get("Album", ""),
|
album_name=post_data.get("Album", ""),
|
||||||
run_time_seconds=convert_to_seconds(
|
run_time_seconds=convert_to_seconds(post_data.get("RunTime", 900000)),
|
||||||
post_data.get("RunTime", 900000)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
album_id = (
|
album_id = Album.objects.filter(name=post_data.get("Album", "")).first().id
|
||||||
Album.objects.filter(name=post_data.get("Album", ""))
|
|
||||||
.first()
|
|
||||||
.id
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# A hack because we don't worry about updating music ... we either finish it or we don't
|
# A hack because we don't worry about updating music ... we either finish it or we don't
|
||||||
@ -328,9 +314,7 @@ def manual_scrobble_book(
|
|||||||
if not page:
|
if not page:
|
||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
logger.info(
|
logger.info("[scrobblers] Book page included in scrobble, should update!")
|
||||||
"[scrobblers] Book page included in scrobble, should update!"
|
|
||||||
)
|
|
||||||
|
|
||||||
source = READCOMICSONLINE_URL.replace("https://", "")
|
source = READCOMICSONLINE_URL.replace("https://", "")
|
||||||
|
|
||||||
@ -447,9 +431,7 @@ def email_scrobble_board_game(
|
|||||||
if not location.name:
|
if not location.name:
|
||||||
location.name = location_dict.get("name")
|
location.name = location_dict.get("name")
|
||||||
update_fields.append("name")
|
update_fields.append("name")
|
||||||
geoloc = GeoLocation.objects.filter(
|
geoloc = GeoLocation.objects.filter(title__icontains=location.name).first()
|
||||||
title__icontains=location.name
|
|
||||||
).first()
|
|
||||||
if geoloc:
|
if geoloc:
|
||||||
location.geo_location = geoloc
|
location.geo_location = geoloc
|
||||||
update_fields.append("geo_location")
|
update_fields.append("geo_location")
|
||||||
@ -501,9 +483,7 @@ def email_scrobble_board_game(
|
|||||||
log_data.pop("expansion_ids")
|
log_data.pop("expansion_ids")
|
||||||
|
|
||||||
if play_dict.get("locationRefId", False):
|
if play_dict.get("locationRefId", False):
|
||||||
log_data["location_id"] = locations[
|
log_data["location_id"] = locations[play_dict.get("locationRefId")].id
|
||||||
play_dict.get("locationRefId")
|
|
||||||
].id
|
|
||||||
if play_dict.get("rounds", False):
|
if play_dict.get("rounds", False):
|
||||||
log_data["rounds"] = play_dict.get("rounds")
|
log_data["rounds"] = play_dict.get("rounds")
|
||||||
if play_dict.get("board", False):
|
if play_dict.get("board", False):
|
||||||
@ -526,9 +506,7 @@ def email_scrobble_board_game(
|
|||||||
timestamp = parse(play_dict.get("playDate"))
|
timestamp = parse(play_dict.get("playDate"))
|
||||||
if hour and minute:
|
if hour and minute:
|
||||||
logger.info(f"Scrobble playDate has manual start time {timestamp}")
|
logger.info(f"Scrobble playDate has manual start time {timestamp}")
|
||||||
timestamp = timestamp.replace(
|
timestamp = timestamp.replace(hour=hour, minute=minute, second=second or 0)
|
||||||
hour=hour, minute=minute, second=second or 0
|
|
||||||
)
|
|
||||||
logger.info(f"Update to {timestamp}")
|
logger.info(f"Update to {timestamp}")
|
||||||
|
|
||||||
profile = UserProfile.objects.filter(user_id=user_id).first()
|
profile = UserProfile.objects.filter(user_id=user_id).first()
|
||||||
@ -623,9 +601,7 @@ def manual_scrobble_from_url(
|
|||||||
scrobbler. Otherwise, return nothing."""
|
scrobbler. Otherwise, return nothing."""
|
||||||
|
|
||||||
if RecipeScraperService.is_recipe_url(url):
|
if RecipeScraperService.is_recipe_url(url):
|
||||||
return scrobble_from_recipe_website(
|
return scrobble_from_recipe_website(url, user_id, source=source, action=action)
|
||||||
url, user_id, source=source, action=action
|
|
||||||
)
|
|
||||||
|
|
||||||
content_key = ""
|
content_key = ""
|
||||||
domain = extract_domain(url)
|
domain = extract_domain(url)
|
||||||
@ -828,9 +804,7 @@ def emacs_scrobble_update_task(
|
|||||||
if not scrobble.log.get('notes"'):
|
if not scrobble.log.get('notes"'):
|
||||||
scrobble.log["notes"] = []
|
scrobble.log["notes"] = []
|
||||||
if note.get("timestamp") not in existing_note_ts:
|
if note.get("timestamp") not in existing_note_ts:
|
||||||
scrobble.log["notes"].append(
|
scrobble.log["notes"].append({note.get("timestamp"): note.get("content")})
|
||||||
{note.get("timestamp"): note.get("content")}
|
|
||||||
)
|
|
||||||
notes_updated = True
|
notes_updated = True
|
||||||
|
|
||||||
if notes_updated:
|
if notes_updated:
|
||||||
@ -856,9 +830,7 @@ def emacs_scrobble_task(
|
|||||||
user_context_list: list[str] = [],
|
user_context_list: list[str] = [],
|
||||||
) -> Scrobble | None:
|
) -> Scrobble | None:
|
||||||
orgmode_id = task_data.get("source_id")
|
orgmode_id = task_data.get("source_id")
|
||||||
title = get_title_from_labels(
|
title = get_title_from_labels(task_data.get("labels", []), user_context_list)
|
||||||
task_data.get("labels", []), user_context_list
|
|
||||||
)
|
|
||||||
|
|
||||||
task = Task.find_or_create(title)
|
task = Task.find_or_create(title)
|
||||||
|
|
||||||
@ -981,9 +953,7 @@ def manual_scrobble_webpage(
|
|||||||
):
|
):
|
||||||
|
|
||||||
if RecipeScraperService.is_recipe_url(url):
|
if RecipeScraperService.is_recipe_url(url):
|
||||||
return scrobble_from_recipe_website(
|
return scrobble_from_recipe_website(url, user_id, source=source, action=action)
|
||||||
url, user_id, source=source, action=action
|
|
||||||
)
|
|
||||||
|
|
||||||
webpage = WebPage.find_or_create({"url": url})
|
webpage = WebPage.find_or_create({"url": url})
|
||||||
|
|
||||||
@ -1205,3 +1175,48 @@ def manual_scrobble_brickset(
|
|||||||
# TODO Kick out a process to enrich the media here, and in every scrobble event
|
# TODO Kick out a process to enrich the media here, and in every scrobble event
|
||||||
# TODO Need to check for past scrobbles and auto populate serial scrobble id if possible
|
# TODO Need to check for past scrobbles and auto populate serial scrobble id if possible
|
||||||
return Scrobble.create_or_update(brickset, user_id, scrobble_dict)
|
return Scrobble.create_or_update(brickset, user_id, scrobble_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def manual_scrobble_food(
|
||||||
|
item_id: str,
|
||||||
|
user_id: int,
|
||||||
|
source: str = "Vrobbler",
|
||||||
|
action: Optional[str] = None,
|
||||||
|
):
|
||||||
|
match = re.match(r"(.+?)\s+-\s+(\d+)", item_id)
|
||||||
|
if not match:
|
||||||
|
logger.info(
|
||||||
|
"[manual_scrobble_food] invalid format, expected 'Food Name - calories'",
|
||||||
|
extra={"item_id": item_id, "user_id": user_id},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
food_name = match.group(1).strip()
|
||||||
|
calories = int(match.group(2))
|
||||||
|
|
||||||
|
food = Food.objects.filter(title=food_name).first()
|
||||||
|
if not food:
|
||||||
|
food = Food.objects.create(
|
||||||
|
title=food_name,
|
||||||
|
description=food_name,
|
||||||
|
calories=calories,
|
||||||
|
)
|
||||||
|
|
||||||
|
scrobble_dict = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": timezone.now(),
|
||||||
|
"playback_position_seconds": 0,
|
||||||
|
"source": source,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[scrobblers] manual food scrobble request received",
|
||||||
|
extra={
|
||||||
|
"food_id": food.id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"scrobble_dict": scrobble_dict,
|
||||||
|
"media_type": Scrobble.MediaType.FOOD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return Scrobble.create_or_update(food, user_id, scrobble_dict)
|
||||||
|
|||||||
Reference in New Issue
Block a user