Add webpage scrobbling
This commit is contained in:
@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-11-30 16:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("webpages", "0001_initial"),
|
||||||
|
("scrobbles", "0044_scrobble_geo_location_alter_scrobble_media_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="scrobble",
|
||||||
|
name="webpage",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||||
|
to="webpages.webpage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="scrobble",
|
||||||
|
name="media_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("Video", "Video"),
|
||||||
|
("Track", "Track"),
|
||||||
|
("Episode", "Podcast episode"),
|
||||||
|
("SportEvent", "Sport event"),
|
||||||
|
("Book", "Book"),
|
||||||
|
("VideoGame", "Video game"),
|
||||||
|
("BoardGame", "Board game"),
|
||||||
|
("GeoLocation", "GeoLocation"),
|
||||||
|
("WebPage", "Web Page"),
|
||||||
|
],
|
||||||
|
default="Video",
|
||||||
|
max_length=14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -14,6 +14,7 @@ from django.utils import timezone
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from locations.models import GeoLocation
|
from locations.models import GeoLocation
|
||||||
|
from webpages.models import WebPage
|
||||||
from music.lastfm import LastFM
|
from music.lastfm import LastFM
|
||||||
from music.models import Artist, Track
|
from music.models import Artist, Track
|
||||||
from podcasts.models import Episode
|
from podcasts.models import Episode
|
||||||
@ -471,6 +472,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
VIDEO_GAME = "VideoGame", "Video game"
|
VIDEO_GAME = "VideoGame", "Video game"
|
||||||
BOARD_GAME = "BoardGame", "Board game"
|
BOARD_GAME = "BoardGame", "Board game"
|
||||||
GEO_LOCATION = "GeoLocation", "GeoLocation"
|
GEO_LOCATION = "GeoLocation", "GeoLocation"
|
||||||
|
WEBPAGE = "WebPage", "Web Page"
|
||||||
|
|
||||||
uuid = models.UUIDField(editable=False, **BNULL)
|
uuid = models.UUIDField(editable=False, **BNULL)
|
||||||
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
|
||||||
@ -491,6 +493,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
geo_location = models.ForeignKey(
|
geo_location = models.ForeignKey(
|
||||||
GeoLocation, on_delete=models.DO_NOTHING, **BNULL
|
GeoLocation, on_delete=models.DO_NOTHING, **BNULL
|
||||||
)
|
)
|
||||||
|
webpage = models.ForeignKey(WebPage, on_delete=models.DO_NOTHING, **BNULL)
|
||||||
media_type = models.CharField(
|
media_type = models.CharField(
|
||||||
max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
|
max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
|
||||||
)
|
)
|
||||||
@ -697,19 +700,24 @@ class Scrobble(TimeStampedModel):
|
|||||||
if media_class == "BoardGame":
|
if media_class == "BoardGame":
|
||||||
media_query = models.Q(board_game=media)
|
media_query = models.Q(board_game=media)
|
||||||
scrobble_data["board_game_id"] = media.id
|
scrobble_data["board_game_id"] = media.id
|
||||||
|
if media_class == "WebPage":
|
||||||
|
media_query = models.Q(webpage=media)
|
||||||
|
scrobble_data["webpage_id"] = media.id
|
||||||
if media_class == "GeoLocation":
|
if media_class == "GeoLocation":
|
||||||
media_query = models.Q(media_type=Scrobble.MediaType.GEO_LOCATION)
|
media_query = models.Q(media_type=Scrobble.MediaType.GEO_LOCATION)
|
||||||
scrobble_data["geo_location_id"] = media.id
|
scrobble_data["geo_location_id"] = media.id
|
||||||
dup = cls.objects.filter(
|
dup = cls.objects.filter(
|
||||||
media_type=cls.MediaType.GEO_LOCATION,
|
media_type=cls.MediaType.GEO_LOCATION,
|
||||||
timestamp = scrobble_data.get("timestamp"),
|
timestamp=scrobble_data.get("timestamp"),
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if dup:
|
if dup:
|
||||||
logger.info("[scrobbling] scrobble with identical timestamp found")
|
logger.info(
|
||||||
|
"[scrobbling] scrobble for geo location with identical timestamp found"
|
||||||
|
)
|
||||||
|
# TODO Fix return type, can we ever return a Scrobble?
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
scrobble = (
|
scrobble = (
|
||||||
cls.objects.filter(
|
cls.objects.filter(
|
||||||
media_query,
|
media_query,
|
||||||
@ -719,7 +727,6 @@ class Scrobble(TimeStampedModel):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if scrobble and scrobble.can_be_updated:
|
if scrobble and scrobble.can_be_updated:
|
||||||
source = scrobble_data["source"]
|
source = scrobble_data["source"]
|
||||||
mtype = media.__class__.__name__
|
mtype = media.__class__.__name__
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
import pendulum
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import pendulum
|
||||||
from boardgames.bgg import lookup_boardgame_from_bgg
|
from boardgames.bgg import lookup_boardgame_from_bgg
|
||||||
from boardgames.models import BoardGame
|
from boardgames.models import BoardGame
|
||||||
from books.models import Book
|
from books.models import Book
|
||||||
from books.openlibrary import lookup_book_from_openlibrary
|
from books.openlibrary import lookup_book_from_openlibrary
|
||||||
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.models import GeoLocation
|
||||||
from music.constants import JELLYFIN_POST_KEYS
|
from music.constants import JELLYFIN_POST_KEYS
|
||||||
from music.models import Track
|
from music.models import Track
|
||||||
from music.utils import (
|
from music.utils import (
|
||||||
@ -23,8 +25,7 @@ from sports.thesportsdb import lookup_event_from_thesportsdb
|
|||||||
from videogames.howlongtobeat import lookup_game_from_hltb
|
from videogames.howlongtobeat import lookup_game_from_hltb
|
||||||
from videogames.models import VideoGame
|
from videogames.models import VideoGame
|
||||||
from videos.models import Video
|
from videos.models import Video
|
||||||
from locations.models import GeoLocation, RawGeoLocation
|
from webpages.models import WebPage
|
||||||
from locations.constants import LOCATION_PROVIDERS
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -247,6 +248,20 @@ def manual_scrobble_board_game(bggeek_id: str, user_id: int):
|
|||||||
return Scrobble.create_or_update(boardgame, user_id, scrobble_dict)
|
return Scrobble.create_or_update(boardgame, user_id, scrobble_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def manual_scrobble_webpage(url: str, user_id: int):
|
||||||
|
webpage = WebPage.find_or_create({"url": url})
|
||||||
|
|
||||||
|
scrobble_dict = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"timestamp": timezone.now(),
|
||||||
|
"playback_position_seconds": 0,
|
||||||
|
"source": "Vrobbler",
|
||||||
|
"source_id": "Manually scrobbled from Vrobbler",
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scrobble.create_or_update(webpage, user_id, scrobble_dict)
|
||||||
|
|
||||||
|
|
||||||
def gpslogger_scrobble_location(
|
def gpslogger_scrobble_location(
|
||||||
data_dict: dict, user_id: Optional[int]
|
data_dict: dict, user_id: Optional[int]
|
||||||
) -> Optional[Scrobble]:
|
) -> Optional[Scrobble]:
|
||||||
|
|||||||
@ -65,6 +65,7 @@ from scrobbles.utils import (
|
|||||||
get_long_plays_in_progress,
|
get_long_plays_in_progress,
|
||||||
get_recently_played_board_games,
|
get_recently_played_board_games,
|
||||||
)
|
)
|
||||||
|
from vrobbler.apps.scrobbles.scrobblers import manual_scrobble_webpage
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -441,6 +442,8 @@ def scrobble_start(request, uuid):
|
|||||||
scrobble = manual_scrobble_video_game(media_obj.hltb_id, user_id)
|
scrobble = manual_scrobble_video_game(media_obj.hltb_id, user_id)
|
||||||
if media_obj.__class__.__name__ == Scrobble.MediaType.BOARD_GAME:
|
if media_obj.__class__.__name__ == Scrobble.MediaType.BOARD_GAME:
|
||||||
scrobble = manual_scrobble_board_game(media_obj.bggeek_id, user_id)
|
scrobble = manual_scrobble_board_game(media_obj.bggeek_id, user_id)
|
||||||
|
if media_obj.__class__.__name__ == Scrobble.MediaType.WEBPAGE:
|
||||||
|
scrobble = manual_scrobble_webpage(media_obj.url, user_id)
|
||||||
|
|
||||||
if scrobble:
|
if scrobble:
|
||||||
messages.add_message(
|
messages.add_message(
|
||||||
|
|||||||
0
vrobbler/apps/webpages/__init__.py
Normal file
0
vrobbler/apps/webpages/__init__.py
Normal file
5
vrobbler/apps/webpages/apps.py
Normal file
5
vrobbler/apps/webpages/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WebpagesConfig(AppConfig):
|
||||||
|
name = "webpages"
|
||||||
79
vrobbler/apps/webpages/migrations/0001_initial.py
Normal file
79
vrobbler/apps/webpages/migrations/0001_initial.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-11-30 16:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django_extensions.db.fields
|
||||||
|
import taggit.managers
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("scrobbles", "0044_scrobble_geo_location_alter_scrobble_media_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WebPage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
django_extensions.db.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
django_extensions.db.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"title",
|
||||||
|
models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"run_time_seconds",
|
||||||
|
models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"run_time_ticks",
|
||||||
|
models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
blank=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("url", models.URLField(max_length=500)),
|
||||||
|
(
|
||||||
|
"genre",
|
||||||
|
taggit.managers.TaggableManager(
|
||||||
|
blank=True,
|
||||||
|
help_text="A comma-separated list of tags.",
|
||||||
|
through="scrobbles.ObjectWithGenres",
|
||||||
|
to="scrobbles.Genre",
|
||||||
|
verbose_name="Tags",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
vrobbler/apps/webpages/migrations/__init__.py
Normal file
0
vrobbler/apps/webpages/migrations/__init__.py
Normal file
65
vrobbler/apps/webpages/models.py
Normal file
65
vrobbler/apps/webpages/models.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from scrobbles.mixins import ScrobblableMixin
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
BNULL = {"blank": True, "null": True}
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class WebPage(ScrobblableMixin):
|
||||||
|
COMPLETION_PERCENT = getattr(settings, "WEBSITE_COMPLETION_PERCENT", 100)
|
||||||
|
|
||||||
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
|
url = models.URLField(max_length=500)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.title:
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
return self.domain
|
||||||
|
|
||||||
|
@property
|
||||||
|
def domain(self):
|
||||||
|
self.url.split("//")[-1].split("/")[0]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"locations:geo_location_detail", kwargs={"slug": self.uuid}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_title_from_web(self, force=False):
|
||||||
|
headers = {
|
||||||
|
"headers": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0"
|
||||||
|
}
|
||||||
|
raw_text = requests.get(self.url, headers=headers).text
|
||||||
|
if not self.title or force:
|
||||||
|
self.title = raw_text[
|
||||||
|
raw_text.find("<title>") + 7 : raw_text.find("</title>")
|
||||||
|
]
|
||||||
|
self.save(update_fields=["title"])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_or_create(cls, data_dict: Dict) -> "GeoLocation":
|
||||||
|
"""Given a data dict from an manual URL scrobble, does the heavy lifting of looking up
|
||||||
|
the url, creating if if doesn't exist yet.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO Add constants for all these data keys
|
||||||
|
if "url" not in data_dict.keys():
|
||||||
|
logger.error("No url in data dict")
|
||||||
|
return
|
||||||
|
|
||||||
|
webpage = cls.objects.filter(url=data_dict.get("url")).first()
|
||||||
|
|
||||||
|
if not webpage:
|
||||||
|
webpage = cls.objects.create(url=data_dict.get("url"))
|
||||||
|
webpage._update_title_from_web()
|
||||||
|
return webpage
|
||||||
@ -107,6 +107,7 @@ INSTALLED_APPS = [
|
|||||||
"boardgames",
|
"boardgames",
|
||||||
"videogames",
|
"videogames",
|
||||||
"locations",
|
"locations",
|
||||||
|
"webpages",
|
||||||
"mathfilters",
|
"mathfilters",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"allauth",
|
"allauth",
|
||||||
|
|||||||
@ -115,6 +115,7 @@ INSTALLED_APPS = [
|
|||||||
"boardgames",
|
"boardgames",
|
||||||
"videogames",
|
"videogames",
|
||||||
"locations",
|
"locations",
|
||||||
|
"webpages",
|
||||||
"mathfilters",
|
"mathfilters",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"allauth",
|
"allauth",
|
||||||
|
|||||||
Reference in New Issue
Block a user