Merge branch 'develop'
This commit is contained in:
@ -16,7 +16,9 @@ class BrickSet(LongPlayScrobblableMixin):
|
|||||||
number = models.CharField(max_length=10, **BNULL)
|
number = models.CharField(max_length=10, **BNULL)
|
||||||
release_year = models.IntegerField(**BNULL)
|
release_year = models.IntegerField(**BNULL)
|
||||||
piece_count = models.IntegerField(**BNULL)
|
piece_count = models.IntegerField(**BNULL)
|
||||||
brickset_rating = models.DecimalField(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)
|
lego_item_number = models.CharField(max_length=10, **BNULL)
|
||||||
box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
|
box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
|
||||||
box_image_small = ImageSpecField(
|
box_image_small = ImageSpecField(
|
||||||
@ -48,13 +50,23 @@ class BrickSet(LongPlayScrobblableMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("bricksets:brickset_detail", kwargs={"slug": self.uuid})
|
return reverse("bricksets:brickset_detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
name = str(self.title)
|
||||||
|
if not self.title:
|
||||||
|
name = str(self.number)
|
||||||
|
return name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def logdata_cls(self):
|
def logdata_cls(self):
|
||||||
return BrickSetLogData
|
return BrickSetLogData
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(cls, title: str) -> "BrickSet":
|
def find_or_create(cls, brickset_id: str) -> "BrickSet":
|
||||||
return cls.objects.filter(title=title).first()
|
brickset = cls.objects.filter(number=brickset_id).first()
|
||||||
|
if not brickset:
|
||||||
|
# TODO: enrich this from the website
|
||||||
|
brickset = cls.objects.create(number=brickset_id)
|
||||||
|
return brickset
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def primary_image_url(self) -> str:
|
def primary_image_url(self) -> str:
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class AlbumAdmin(admin.ModelAdmin):
|
|||||||
"theaudiodb_mood",
|
"theaudiodb_mood",
|
||||||
"musicbrainz_id",
|
"musicbrainz_id",
|
||||||
)
|
)
|
||||||
|
raw_id_fields = ("album_artist",)
|
||||||
list_filter = (
|
list_filter = (
|
||||||
"theaudiodb_score",
|
"theaudiodb_score",
|
||||||
"theaudiodb_genre",
|
"theaudiodb_genre",
|
||||||
@ -53,6 +54,10 @@ class TrackAdmin(admin.ModelAdmin):
|
|||||||
"artist",
|
"artist",
|
||||||
"musicbrainz_id",
|
"musicbrainz_id",
|
||||||
)
|
)
|
||||||
|
raw_id_fields = (
|
||||||
|
"album",
|
||||||
|
"artist",
|
||||||
|
)
|
||||||
list_filter = ("album", "artist")
|
list_filter = ("album", "artist")
|
||||||
search_fields = ("title",)
|
search_fields = ("title",)
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class ScrobbleInline(admin.TabularInline):
|
|||||||
"geo_location",
|
"geo_location",
|
||||||
"task",
|
"task",
|
||||||
"mood",
|
"mood",
|
||||||
"brickset",
|
"brick_set",
|
||||||
"trail",
|
"trail",
|
||||||
"beer",
|
"beer",
|
||||||
"web_page",
|
"web_page",
|
||||||
@ -122,9 +122,12 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
|||||||
"video_game",
|
"video_game",
|
||||||
"board_game",
|
"board_game",
|
||||||
"geo_location",
|
"geo_location",
|
||||||
|
"puzzle",
|
||||||
|
"paper",
|
||||||
|
"food",
|
||||||
"task",
|
"task",
|
||||||
"mood",
|
"mood",
|
||||||
"brickset",
|
"brick_set",
|
||||||
"trail",
|
"trail",
|
||||||
"beer",
|
"beer",
|
||||||
"web_page",
|
"web_page",
|
||||||
|
|||||||
@ -27,14 +27,14 @@ MEDIA_END_PADDING_SECONDS = {
|
|||||||
TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}"
|
TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}"
|
||||||
|
|
||||||
SCROBBLE_CONTENT_URLS = {
|
SCROBBLE_CONTENT_URLS = {
|
||||||
"-i": "https://www.imdb.com/title/",
|
"-i": ["https://www.imdb.com/title/", "https://www.youtube.com/watch?v="],
|
||||||
"-s": "https://www.thesportsdb.com/event/",
|
"-s": ["https://www.thesportsdb.com/event/"],
|
||||||
"-g": "https://boardgamegeek.com/boardgame/",
|
"-g": ["https://boardgamegeek.com/boardgame/"],
|
||||||
"-u": "https://untappd.com/",
|
"-u": ["https://untappd.com/"],
|
||||||
"-b": "https://www.amazon.com/",
|
"-b": ["https://www.amazon.com/"],
|
||||||
"-t": "https://app.todoist.com/app/task/{id}",
|
"-t": ["https://app.todoist.com/app/task/{id}"],
|
||||||
"-i": "https://www.youtube.com/watch?v=",
|
"-p": ["https://www.ipdb.plus/IPDb/puzzle.php?id="],
|
||||||
"-p": "https://www.ipdb.plus/IPDb/puzzle.php?id=",
|
"-l": ["https://brickset.com/sets/"],
|
||||||
}
|
}
|
||||||
|
|
||||||
EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",)
|
EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",)
|
||||||
@ -49,6 +49,7 @@ MANUAL_SCROBBLE_FNS = {
|
|||||||
"-w": "manual_scrobble_webpage",
|
"-w": "manual_scrobble_webpage",
|
||||||
"-t": "manual_scrobble_task",
|
"-t": "manual_scrobble_task",
|
||||||
"-p": "manual_scrobble_puzzle",
|
"-p": "manual_scrobble_puzzle",
|
||||||
|
"-l": "manual_scrobble_brickset",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-06-09 14:33
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("scrobbles", "0069_scrobble_puzzle_alter_scrobble_media_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="scrobble",
|
||||||
|
old_name="brickset",
|
||||||
|
new_name="brick_set",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -549,7 +549,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
LifeEvent, on_delete=models.DO_NOTHING, **BNULL
|
LifeEvent, on_delete=models.DO_NOTHING, **BNULL
|
||||||
)
|
)
|
||||||
mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
|
mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
|
||||||
brickset = models.ForeignKey(
|
brick_set = models.ForeignKey(
|
||||||
BrickSet, on_delete=models.DO_NOTHING, **BNULL
|
BrickSet, on_delete=models.DO_NOTHING, **BNULL
|
||||||
)
|
)
|
||||||
media_type = models.CharField(
|
media_type = models.CharField(
|
||||||
@ -635,6 +635,14 @@ class Scrobble(TimeStampedModel):
|
|||||||
|
|
||||||
for scrobble in scrobble_qs:
|
for scrobble in scrobble_qs:
|
||||||
scrobbles_by_type[scrobble.media_type].append(scrobble)
|
scrobbles_by_type[scrobble.media_type].append(scrobble)
|
||||||
|
if not scrobbles_by_type.get(scrobble.media_type + "_count"):
|
||||||
|
scrobbles_by_type[scrobble.media_type + "_count"] = 0
|
||||||
|
scrobbles_by_type[scrobble.media_type + "_count"] += 1
|
||||||
|
if not scrobbles_by_type.get(scrobble.media_type + "_time"):
|
||||||
|
scrobbles_by_type[scrobble.media_type + "_time"] = 0
|
||||||
|
scrobbles_by_type[scrobble.media_type + "_time"] += int(
|
||||||
|
(scrobble.elapsed_time)
|
||||||
|
)
|
||||||
|
|
||||||
return scrobbles_by_type
|
return scrobbles_by_type
|
||||||
|
|
||||||
@ -1011,8 +1019,8 @@ class Scrobble(TimeStampedModel):
|
|||||||
media_obj = self.life_event
|
media_obj = self.life_event
|
||||||
if self.mood:
|
if self.mood:
|
||||||
media_obj = self.mood
|
media_obj = self.mood
|
||||||
if self.brickset:
|
if self.brick_set:
|
||||||
media_obj = self.brickset
|
media_obj = self.brick_set
|
||||||
if self.trail:
|
if self.trail:
|
||||||
media_obj = self.trail
|
media_obj = self.trail
|
||||||
if self.beer:
|
if self.beer:
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import pytz
|
|||||||
from beers.models import Beer
|
from beers.models import Beer
|
||||||
from boardgames.models import BoardGame
|
from boardgames.models import BoardGame
|
||||||
from books.models import Book
|
from books.models import Book
|
||||||
|
from bricksets.models import BrickSet
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from locations.constants import LOCATION_PROVIDERS
|
from locations.constants import LOCATION_PROVIDERS
|
||||||
@ -23,7 +24,7 @@ from scrobbles.constants import (
|
|||||||
SCROBBLE_CONTENT_URLS,
|
SCROBBLE_CONTENT_URLS,
|
||||||
)
|
)
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from scrobbles.utils import convert_to_seconds
|
from scrobbles.utils import convert_to_seconds, extract_domain
|
||||||
from sports.models import SportEvent
|
from sports.models import SportEvent
|
||||||
from sports.thesportsdb import lookup_event_from_thesportsdb
|
from sports.thesportsdb import lookup_event_from_thesportsdb
|
||||||
from tasks.models import Task
|
from tasks.models import Task
|
||||||
@ -322,14 +323,12 @@ def manual_scrobble_from_url(
|
|||||||
we know about the content type, and routes it to the appropriate media
|
we know about the content type, and routes it to the appropriate media
|
||||||
scrobbler. Otherwise, return nothing."""
|
scrobbler. Otherwise, return nothing."""
|
||||||
content_key = ""
|
content_key = ""
|
||||||
try:
|
domain = extract_domain(url)
|
||||||
domain = url.split("//")[-1].split("/")[0]
|
|
||||||
except IndexError:
|
|
||||||
domain = None
|
|
||||||
|
|
||||||
for key, content_url in SCROBBLE_CONTENT_URLS.items():
|
for key, content_urls in SCROBBLE_CONTENT_URLS.items():
|
||||||
if domain in content_url:
|
for content_url in content_urls:
|
||||||
content_key = key
|
if domain in content_url:
|
||||||
|
content_key = key
|
||||||
|
|
||||||
item_id = None
|
item_id = None
|
||||||
if not content_key:
|
if not content_key:
|
||||||
@ -343,8 +342,10 @@ def manual_scrobble_from_url(
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if content_key == "-i":
|
if content_key == "-i" and "v=" in url:
|
||||||
item_id = url.split("v=")[1].split("&")[0]
|
item_id = url.split("v=")[1].split("&")[0]
|
||||||
|
elif content_key == "-i" and "title/tt" in url:
|
||||||
|
item_id = "tt" + str(item_id)
|
||||||
|
|
||||||
scrobble_fn = MANUAL_SCROBBLE_FNS[content_key]
|
scrobble_fn = MANUAL_SCROBBLE_FNS[content_key]
|
||||||
return eval(scrobble_fn)(item_id, user_id, action=action)
|
return eval(scrobble_fn)(item_id, user_id, action=action)
|
||||||
@ -500,6 +501,151 @@ def todoist_scrobble_task(
|
|||||||
return scrobble
|
return scrobble
|
||||||
|
|
||||||
|
|
||||||
|
def emacs_scrobble_update_task(
|
||||||
|
emacs_id: str, emacs_notes: dict, user_id: int
|
||||||
|
) -> Optional[Scrobble]:
|
||||||
|
scrobble = Scrobble.objects.filter(
|
||||||
|
in_progress=True,
|
||||||
|
user_id=user_id,
|
||||||
|
log__source_id=emacs_id,
|
||||||
|
log__source="emacs",
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not scrobble:
|
||||||
|
logger.info(
|
||||||
|
"[emacs_scrobble_update_task] no task found",
|
||||||
|
extra={
|
||||||
|
"emacs_notes": emacs_notes,
|
||||||
|
"user_id": user_id,
|
||||||
|
"media_type": Scrobble.MediaType.TASK,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
notes_updated = False
|
||||||
|
for note in emacs_notes:
|
||||||
|
existing_note_ts = [
|
||||||
|
n.get("timestamp") for n in scrobble.log.get("notes", [])
|
||||||
|
]
|
||||||
|
if not scrobble.log.get('notes"'):
|
||||||
|
scrobble.log["notes"] = []
|
||||||
|
if note.get("timestamp") not in existing_note_ts:
|
||||||
|
scrobble.log["notes"].append(
|
||||||
|
{note.get("timestamp"): note.get("content")}
|
||||||
|
)
|
||||||
|
notes_updated = True
|
||||||
|
|
||||||
|
if notes_updated:
|
||||||
|
scrobble.save(update_fields=["log"])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[emacs_scrobble_update_task] emacs note added",
|
||||||
|
extra={
|
||||||
|
"emacs_note": emacs_notes,
|
||||||
|
"user_id": user_id,
|
||||||
|
"media_type": Scrobble.MediaType.TASK,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return scrobble
|
||||||
|
|
||||||
|
|
||||||
|
def emacs_scrobble_task(
|
||||||
|
task_data: dict,
|
||||||
|
user_id: int,
|
||||||
|
started: bool = False,
|
||||||
|
stopped: bool = False,
|
||||||
|
) -> Scrobble | None:
|
||||||
|
|
||||||
|
prefix = ""
|
||||||
|
suffix = ""
|
||||||
|
|
||||||
|
source_id = task_data.get("source_id")
|
||||||
|
for label in task_data.get("labels"):
|
||||||
|
if label in TODOIST_TITLE_PREFIX_LABELS:
|
||||||
|
prefix = label
|
||||||
|
if label in TODOIST_TITLE_SUFFIX_LABELS:
|
||||||
|
suffix = label
|
||||||
|
|
||||||
|
if not prefix and not suffix:
|
||||||
|
logger.warning(
|
||||||
|
"Missing a prefix and suffix tag for task",
|
||||||
|
extra={"emacs_scrobble_task": task_data},
|
||||||
|
)
|
||||||
|
|
||||||
|
title = " ".join([prefix.capitalize(), suffix.capitalize()])
|
||||||
|
task = Task.find_or_create(title)
|
||||||
|
|
||||||
|
timestamp = pendulum.parse(task_data.get("updated_at", timezone.now()))
|
||||||
|
in_progress_scrobble = Scrobble.objects.filter(
|
||||||
|
user_id=user_id,
|
||||||
|
in_progress=True,
|
||||||
|
log__source_id=source_id,
|
||||||
|
log__source="orgmode",
|
||||||
|
task=task,
|
||||||
|
).last()
|
||||||
|
|
||||||
|
if not in_progress_scrobble and stopped:
|
||||||
|
logger.info(
|
||||||
|
"[emacs_scrobble_task] cannot stop already stopped task",
|
||||||
|
extra={
|
||||||
|
"emacs_id": source_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if in_progress_scrobble and started:
|
||||||
|
logger.info(
|
||||||
|
"[emacs_scrobble_task] cannot start already started task",
|
||||||
|
extra={
|
||||||
|
"emacs_id": source_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return in_progress_scrobble
|
||||||
|
|
||||||
|
# Finish an in-progress scrobble
|
||||||
|
if in_progress_scrobble and stopped:
|
||||||
|
logger.info(
|
||||||
|
"[emacs_scrobble_task] finishing",
|
||||||
|
extra={
|
||||||
|
"emacs_id": source_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
in_progress_scrobble.stop(timestamp=timestamp, force_finish=True)
|
||||||
|
return in_progress_scrobble
|
||||||
|
|
||||||
|
if in_progress_scrobble:
|
||||||
|
return in_progress_scrobble
|
||||||
|
|
||||||
|
notes = task_data.pop("notes")
|
||||||
|
if notes:
|
||||||
|
task_data["notes"] = []
|
||||||
|
for note in notes:
|
||||||
|
task_data["notes"].append(
|
||||||
|
{note.get("timestamp"): note.get("content")}
|
||||||
|
)
|
||||||
|
|
||||||
|
scrobble_dict = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"playback_position_seconds": 0,
|
||||||
|
"source": "Org-mode",
|
||||||
|
"log": task_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[emacs_scrobble_task] creating",
|
||||||
|
extra={
|
||||||
|
"task_id": task.id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"scrobble_dict": scrobble_dict,
|
||||||
|
"media_type": Scrobble.MediaType.TASK,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
scrobble = Scrobble.create_or_update(task, user_id, scrobble_dict)
|
||||||
|
return scrobble
|
||||||
|
|
||||||
|
|
||||||
def manual_scrobble_task(url: str, user_id: int, action: Optional[str] = None):
|
def manual_scrobble_task(url: str, user_id: int, action: Optional[str] = None):
|
||||||
source_id = re.findall(r"\d+", url)[0]
|
source_id = re.findall(r"\d+", url)[0]
|
||||||
|
|
||||||
@ -708,3 +854,34 @@ def manual_scrobble_puzzle(
|
|||||||
|
|
||||||
# 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
|
||||||
return Scrobble.create_or_update(puzzle, user_id, scrobble_dict)
|
return Scrobble.create_or_update(puzzle, user_id, scrobble_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def manual_scrobble_brickset(
|
||||||
|
brickset_id: str, user_id: int, action: Optional[str] = None
|
||||||
|
):
|
||||||
|
brickset = BrickSet.find_or_create(brickset_id)
|
||||||
|
|
||||||
|
if not brickset:
|
||||||
|
logger.error(f"No brickset found for Brickset ID {brickset_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
scrobble_dict = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": timezone.now(),
|
||||||
|
"playback_position_seconds": 0,
|
||||||
|
"source": "Vrobbler",
|
||||||
|
"log": {"serial_scrobble_id": ""},
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"[vrobbler-scrobble] brickset scrobble request received",
|
||||||
|
extra={
|
||||||
|
"brickset_id": brickset.id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"scrobble_dict": scrobble_dict,
|
||||||
|
"media_type": Scrobble.MediaType.BRICKSET,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
return Scrobble.create_or_update(brickset, user_id, scrobble_dict)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from scrobbles import views
|
from scrobbles import views
|
||||||
from tasks.webhooks import todoist_webhook
|
from tasks.webhooks import todoist_webhook, emacs_webhook
|
||||||
|
|
||||||
app_name = "scrobbles"
|
app_name = "scrobbles"
|
||||||
|
|
||||||
@ -36,11 +36,6 @@ urlpatterns = [
|
|||||||
views.web_scrobbler_webhook,
|
views.web_scrobbler_webhook,
|
||||||
name="web-scrobbler-webhook",
|
name="web-scrobbler-webhook",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"webhook/emacs/",
|
|
||||||
views.emacs_webhook,
|
|
||||||
name="emacs-webhook",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"webhook/gps/",
|
"webhook/gps/",
|
||||||
views.gps_webhook,
|
views.gps_webhook,
|
||||||
@ -57,6 +52,13 @@ urlpatterns = [
|
|||||||
name="mopidy-webhook",
|
name="mopidy-webhook",
|
||||||
),
|
),
|
||||||
path("webhook/todoist/", todoist_webhook, name="todoist-webhook"),
|
path("webhook/todoist/", todoist_webhook, name="todoist-webhook"),
|
||||||
|
path("webhook/emacs/", emacs_webhook, name="emacs_webhook"),
|
||||||
|
path("export/", views.export, name="export"),
|
||||||
|
path(
|
||||||
|
"imports/",
|
||||||
|
views.ScrobbleImportListView.as_view(),
|
||||||
|
name="import-detail",
|
||||||
|
),
|
||||||
path("export/", views.export, name="export"),
|
path("export/", views.export, name="export"),
|
||||||
path(
|
path(
|
||||||
"imports/",
|
"imports/",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, tzinfo
|
from datetime import datetime, timedelta, tzinfo
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -17,9 +18,10 @@ from scrobbles.tasks import (
|
|||||||
process_lastfm_import,
|
process_lastfm_import,
|
||||||
process_retroarch_import,
|
process_retroarch_import,
|
||||||
)
|
)
|
||||||
from vrobbler.apps.scrobbles.notifications import NtfyNotification
|
|
||||||
from webdav.client import get_webdav_client
|
from webdav.client import get_webdav_client
|
||||||
|
|
||||||
|
from vrobbler.apps.scrobbles.notifications import NtfyNotification
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@ -67,7 +69,8 @@ def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet:
|
|||||||
return Scrobble.objects.filter(media_query, user=user)
|
return Scrobble.objects.filter(media_query, user=user)
|
||||||
|
|
||||||
|
|
||||||
def get_recently_played_board_games(user: User) -> dict: ...
|
def get_recently_played_board_games(user: User) -> dict:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
def get_long_plays_in_progress(user: User) -> dict:
|
def get_long_plays_in_progress(user: User) -> dict:
|
||||||
@ -187,8 +190,8 @@ def delete_zombie_scrobbles(dry_run=True):
|
|||||||
|
|
||||||
def import_from_webdav_for_all_users(restart=False):
|
def import_from_webdav_for_all_users(restart=False):
|
||||||
"""Grab a list of all users with WebDAV enabled and kickoff imports for them"""
|
"""Grab a list of all users with WebDAV enabled and kickoff imports for them"""
|
||||||
from scrobbles.models import KoReaderImport
|
|
||||||
from books.koreader import fetch_file_from_webdav
|
from books.koreader import fetch_file_from_webdav
|
||||||
|
from scrobbles.models import KoReaderImport
|
||||||
|
|
||||||
# LastFmImport = apps.get_model("scrobbles", "LastFMImport")
|
# LastFmImport = apps.get_model("scrobbles", "LastFMImport")
|
||||||
webdav_enabled_user_ids = UserProfile.objects.filter(
|
webdav_enabled_user_ids = UserProfile.objects.filter(
|
||||||
@ -271,6 +274,7 @@ def get_file_md5_hash(file_path: str) -> str:
|
|||||||
file_hash.update(chunk)
|
file_hash.update(chunk)
|
||||||
return file_hash.hexdigest()
|
return file_hash.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def deduplicate_tracks(commit=False) -> int:
|
def deduplicate_tracks(commit=False) -> int:
|
||||||
from music.models import Track
|
from music.models import Track
|
||||||
|
|
||||||
@ -293,7 +297,11 @@ def deduplicate_tracks(commit=False) -> int:
|
|||||||
try:
|
try:
|
||||||
other.delete()
|
other.delete()
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
print("could not delete ", other.id, f": IntegrityError {e}")
|
print(
|
||||||
|
"could not delete ",
|
||||||
|
other.id,
|
||||||
|
f": IntegrityError {e}",
|
||||||
|
)
|
||||||
return len(dups)
|
return len(dups)
|
||||||
|
|
||||||
|
|
||||||
@ -318,3 +326,13 @@ def send_stop_notifications_for_in_progress_scrobbles() -> int:
|
|||||||
notifications_sent += 1
|
notifications_sent += 1
|
||||||
|
|
||||||
return notifications_sent
|
return notifications_sent
|
||||||
|
|
||||||
|
|
||||||
|
def extract_domain(url):
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
domain = (
|
||||||
|
parsed_url.netloc.split(".")[-2]
|
||||||
|
+ "."
|
||||||
|
+ parsed_url.netloc.split(".")[-1]
|
||||||
|
)
|
||||||
|
return domain
|
||||||
|
|||||||
@ -524,35 +524,6 @@ def gps_webhook(request):
|
|||||||
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
|
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@api_view(["POST"])
|
|
||||||
def emacs_webhook(request):
|
|
||||||
try:
|
|
||||||
data_dict = json.loads(request.data)
|
|
||||||
except TypeError:
|
|
||||||
data_dict = request.data
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"[emacs_webhook] called",
|
|
||||||
extra={
|
|
||||||
"post_data": data_dict,
|
|
||||||
"user_id": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO Fix this so we have to authenticate!
|
|
||||||
user_id = 1
|
|
||||||
if request.user.id:
|
|
||||||
user_id = request.user.id
|
|
||||||
|
|
||||||
# scrobble = gpslogger_scrobble_location(data_dict, user_id)
|
|
||||||
|
|
||||||
# if not scrobble:
|
|
||||||
# return Response({}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
return Response({"post_data": post_data}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
|||||||
@ -50,7 +50,8 @@ class Task(LongPlayScrobblableMixin):
|
|||||||
url = ""
|
url = ""
|
||||||
scrobble = self.scrobbles(user_id).first()
|
scrobble = self.scrobbles(user_id).first()
|
||||||
if scrobble:
|
if scrobble:
|
||||||
url = TODOIST_TASK_URL.format(id=scrobble.logdata.todoist_id)
|
if scrobble.log.get("source") == "todoist":
|
||||||
|
url = TODOIST_TASK_URL.format(id=scrobble.logdata.todist_id)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def subtitle_for_user(self, user_id):
|
def subtitle_for_user(self, user_id):
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pendulum
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from profiles.models import UserProfile
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from scrobbles.scrobblers import (
|
from scrobbles.scrobblers import (
|
||||||
|
emacs_scrobble_task,
|
||||||
|
emacs_scrobble_update_task,
|
||||||
todoist_scrobble_task,
|
todoist_scrobble_task,
|
||||||
todoist_scrobble_update_task,
|
todoist_scrobble_update_task,
|
||||||
)
|
)
|
||||||
from profiles.models import UserProfile
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ def todoist_webhook(request):
|
|||||||
todoist_type, todoist_event = post_data.get("event_name").split(":")
|
todoist_type, todoist_event = post_data.get("event_name").split(":")
|
||||||
event_data = post_data.get("event_data", {})
|
event_data = post_data.get("event_data", {})
|
||||||
is_item_type = todoist_type == "item"
|
is_item_type = todoist_type == "item"
|
||||||
is_note_type = todoist_type == "note"
|
is_note_type = todoist_tyllll = "note"
|
||||||
new_labels = event_data.get("labels", [])
|
new_labels = event_data.get("labels", [])
|
||||||
old_labels = (
|
old_labels = (
|
||||||
post_data.get("event_data_extra", {})
|
post_data.get("event_data_extra", {})
|
||||||
@ -118,3 +120,54 @@ def todoist_webhook(request):
|
|||||||
extra={"scrobble_id": scrobble.id},
|
extra={"scrobble_id": scrobble.id},
|
||||||
)
|
)
|
||||||
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
|
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
@api_view(["POST"])
|
||||||
|
def emacs_webhook(request):
|
||||||
|
try:
|
||||||
|
post_data = json.loads(request.data)
|
||||||
|
except TypeError:
|
||||||
|
post_data = request.data
|
||||||
|
logger.info(
|
||||||
|
"[emacs_webhook] called",
|
||||||
|
extra={"post_data": post_data},
|
||||||
|
)
|
||||||
|
task_in_progress = post_data.get("state") == "STRT"
|
||||||
|
task_stopped = post_data.get("state") == "DONE"
|
||||||
|
post_data["source_id"] = post_data.pop("emacs_id")
|
||||||
|
|
||||||
|
# TODO: Figure out why token auth is not working
|
||||||
|
user_id = request.user.id
|
||||||
|
if not user_id:
|
||||||
|
user_id = 1
|
||||||
|
|
||||||
|
scrobble = None
|
||||||
|
if post_data.get("source_id"):
|
||||||
|
scrobble = emacs_scrobble_task(
|
||||||
|
post_data, user_id, started=task_in_progress, stopped=task_stopped
|
||||||
|
)
|
||||||
|
|
||||||
|
if scrobble and task_in_progress and post_data.get("notes"):
|
||||||
|
emacs_scrobble_update_task(
|
||||||
|
post_data.get("source_id"),
|
||||||
|
post_data.get("notes"),
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not scrobble:
|
||||||
|
logger.info(
|
||||||
|
"[emacs_webhook] finished with no note or task found",
|
||||||
|
extra={"scrobble_id": None},
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"error": "No scrobble found to be updated"},
|
||||||
|
status=status.HTTP_304_NOT_MODIFIED,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[emacs_webhook] finished",
|
||||||
|
extra={"scrobble_id": scrobble.id},
|
||||||
|
)
|
||||||
|
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@ -57,6 +57,8 @@ class VideoMetadata:
|
|||||||
|
|
||||||
def as_dict_with_cover_and_genres(self) -> tuple:
|
def as_dict_with_cover_and_genres(self) -> tuple:
|
||||||
video_dict = vars(self)
|
video_dict = vars(self)
|
||||||
cover = video_dict.pop("cover_url")
|
cover = None
|
||||||
|
if "cover_url" in video_dict.keys():
|
||||||
|
cover = video_dict.pop("cover_url")
|
||||||
genres = video_dict.pop("genres")
|
genres = video_dict.pop("genres")
|
||||||
return video_dict, cover, genres
|
return video_dict, cover, genres
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
{% if Track %}
|
{% if Track %}
|
||||||
<h2>Music</h2>
|
<h2>Music</h2>
|
||||||
{% with scrobbles=Track %}
|
{% with scrobbles=Track count=Track_count time=Track_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -18,63 +18,70 @@
|
|||||||
|
|
||||||
{% if Task %}
|
{% if Task %}
|
||||||
<h2>Latest tasks</h2>
|
<h2>Latest tasks</h2>
|
||||||
{% with scrobbles=Task %}
|
{% with scrobbles=Task count=Task_count time=Task_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if Video %}
|
{% if Video %}
|
||||||
<h2>Videos</h2>
|
<h2>Videos</h2>
|
||||||
{% with scrobbles=Video %}
|
{% with scrobbles=Video count=Video_count time=Video_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if WebPage %}
|
{% if WebPage %}
|
||||||
<h4>Web pages</h4>
|
<h4>Web pages</h4>
|
||||||
{% with scrobbles=WebPage %}
|
{% with scrobbles=WebPage count=WebPage_count time=WebPage_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if SportEvent %}
|
{% if SportEvent %}
|
||||||
<h2>Sports</h2>
|
<h2>Sports</h2>
|
||||||
{% with scrobbles=SportEvent %}
|
{% with scrobbles=SportEvent count=SportEvent_count time=SportEvent_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if PodcastEpisode %}
|
{% if PodcastEpisode %}
|
||||||
<h2>Latest podcasts</h2>
|
<h2>Latest podcasts</h2>
|
||||||
{% with scrobbles=PodcastEpisode %}
|
{% with scrobbles=PodcastEpisode count=PodcastEpisode_count time=PodcastEpisode_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if VideoGame %}
|
{% if VideoGame %}
|
||||||
<h4>Video games</h4>
|
<h4>Video games</h4>
|
||||||
{% with scrobbles=VideoGame %}
|
{% with scrobbles=VideoGame count=VideoGame_count time=VideoGame_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if BoardGame %}
|
{% if BoardGame %}
|
||||||
<h4>Board games</h4>
|
<h4>Board games</h4>
|
||||||
{% with scrobbles=BoardGame %}
|
{% with scrobbles=BoardGame count=BoardGame_count time=BoardGame_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if Beer %}
|
{% if Beer %}
|
||||||
<h4>Beers</h4>
|
<h4>Beers</h4>
|
||||||
{% with scrobbles=Beer %}
|
{% with scrobbles=Beer count=Beer_count time=Beer_time %}
|
||||||
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if BrickSet %}
|
||||||
|
<h4>Brick sets</h4>
|
||||||
|
{% with scrobbles=BrickSet count=BrickSet_count time=BrickSet_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if Book %}
|
{% if Book %}
|
||||||
<h4>Books</h4>
|
<h4>Books</h4>
|
||||||
{% with scrobbles=Book %}
|
{% with scrobbles=Book count=Book_count time=Book_time %}
|
||||||
{% include "scrobbles/_scrobble_table.html" %}
|
{% include "scrobbles/_scrobble_table.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load naturalduration %}
|
{% load naturalduration %}
|
||||||
<tr>
|
<tr {% if scrobble.in_progress %}class="in-progress"{% endif %}>
|
||||||
<td>{% if scrobble.in_progress %}{{scrobble.media_obj.strings.verb}} now | <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>{% else %}{{scrobble.timestamp|naturaltime}}{% endif %}</td>
|
<td>{% if scrobble.in_progress %}{{scrobble.media_obj.strings.verb}} now | <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>{% else %}{{scrobble.timestamp|naturaltime}}{% endif %}</td>
|
||||||
<td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj|truncatechars_html:50}}</a></td>
|
<td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj|truncatechars_html:45}}</a></td>
|
||||||
<td>{{scrobble.elapsed_time|natural_duration}}</td>
|
<td>{{scrobble.elapsed_time|natural_duration}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<div class="tab-pane fade show" id="latest-beers" role="tabpanel"
|
<div class="tab-pane fade show" id="latest-beers" role="tabpanel"
|
||||||
aria-labelledby="latest-beers-tab">
|
aria-labelledby="latest-beers-tab">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
{{count}} scrobble {% if time %}| {{time|natural_duration}}{% endif %}
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -43,6 +43,10 @@
|
|||||||
background:rgba(0,0,0,0.4);
|
background:rgba(0,0,0,0.4);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.in-progress {
|
||||||
|
background: #CCFFFF;
|
||||||
|
}
|
||||||
|
.in-progress a { color: black }
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@ -62,6 +62,7 @@
|
|||||||
<td>{{scrobble.timestamp}}</td>
|
<td>{{scrobble.timestamp}}</td>
|
||||||
<td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td>
|
<td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td>
|
||||||
<td>{{scrobble.source}}</td>
|
<td>{{scrobble.source}}</td>
|
||||||
|
<td>{{scrobble.log.notes}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user