Compare commits

...

19 Commits

Author SHA1 Message Date
db36329011 Merge branch 'develop' 2025-06-07 18:11:08 -04:00
69aa80e6c1 [templates] Allow going back and forward in time 2025-06-07 15:17:56 -04:00
99a6e5107b [scrobbles] Add orgparse and start of emacs webhook 2025-06-06 17:25:04 -04:00
39e2fdce27 [music] Fix allmusic lookup 2025-06-06 17:24:41 -04:00
b2f98d780b [middleware] Add auto timezone setting 2025-06-06 09:49:53 -04:00
07b5dc6a2c [templates] Clean up dashboard 2025-06-06 09:49:08 -04:00
7207ca385e [scrobbles] Add two new methods to Scrobble model 2025-06-06 09:48:15 -04:00
2a254e28d0 [templates] Start redesigning the dashboard 2025-05-18 23:20:47 -04:00
d4377e49ac [music] Fix getting album metadata 2025-05-18 23:19:17 -04:00
9dc0a818ff [templates] Fix chart loading 2025-05-18 23:19:05 -04:00
5e672fc9ed [webpages] Add title to webpage reading 2025-05-18 09:37:53 -04:00
6d194d227e Merge branch 'develop' 2025-05-11 01:43:55 -04:00
ed217cbad2 [beers] Blacken and clean up imports 2025-05-11 01:42:30 -04:00
3cca30dc70 [puzzles] Add puzzle lookup via IPDB 2025-05-11 01:41:43 -04:00
d3a15d5e7b Merge branch 'develop' 2025-05-11 00:03:48 -04:00
deeaa0af4b [puzzles] Fix misnamed ipdb field 2025-05-10 23:58:44 -04:00
dd71bdd38c [puzzles] Add puzzle model and hooks 2025-05-10 23:36:03 -04:00
d066a98282 [importers] Fixes TSV comma missing 2025-04-15 16:32:14 -04:00
73bc4a1cd1 [webpages] Fixes 500 errors when webpage lookup fails 2025-04-15 16:18:26 -04:00
19 changed files with 902 additions and 464 deletions

15
poetry.lock generated
View File

@ -2228,6 +2228,7 @@ python-versions = "*"
groups = ["main"]
files = [
{file = "jusText-3.0.1-py2.py3-none-any.whl", hash = "sha256:e0fb882dd7285415709f4b7466aed23d6b98b7b89404c36e8a2e730facfed02b"},
{file = "justext-3.0.1-py2.py3-none-any.whl", hash = "sha256:0a5225c5cd7c5a124fec7bfa9a55110a73135e8b58ce784470af67d051ac9fd3"},
{file = "justext-3.0.1.tar.gz", hash = "sha256:b6ed2fb6c5d21618e2e34b2295c4edfc0bcece3bd549ed5c8ef5a8d20f0b3451"},
]
@ -2891,6 +2892,18 @@ rsa = ["cryptography (>=3.0.0)"]
signals = ["blinker (>=1.4.0)"]
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "orgparse"
version = "0.4.20250520"
description = "orgparse - Emacs org-mode parser in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "orgparse-0.4.20250520-py3-none-any.whl", hash = "sha256:24d454432385016ae91c3518c8357a3a31fdd4cebfbb0c5926cf31247bf4c7e3"},
{file = "orgparse-0.4.20250520.tar.gz", hash = "sha256:6472fd16ddcabb523918505c865263abfa05ac80b593e668a089cda291b1a2de"},
]
[[package]]
name = "packaging"
version = "24.2"
@ -5439,4 +5452,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<3.12"
content-hash = "17358679b06dd15b7f119307013ed578ef81dbf31c592bc2ca11d13787a81215"
content-hash = "cdd7f577fe3a4c5c8cc960e0070d93b7ddbb2a7968fab63d72bb039afaa05bbe"

View File

@ -53,6 +53,7 @@ django-oauth-toolkit = "^3.0.1"
meta-yt = "^0.1.9"
berserk = "^0.13.2"
poetry-bumpversion = "^0.3.3"
orgparse = "^0.4.20250520"
[tool.poetry.group.test]
optional = true

View File

@ -1,6 +1,4 @@
import json
import logging
import urllib
from typing import Optional
import requests
@ -87,7 +85,9 @@ 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:
@ -130,7 +130,6 @@ def get_beer_from_untappd_id(untappd_id: str) -> dict:
return beer_dict
soup = BeautifulSoup(response.text, "html.parser")
beer_dict["untappd_id"] = untappd_id
beer_dict["title"] = get_title_from_soup(soup)
beer_dict["description"] = get_description_from_soup(soup)
beer_dict["styles"] = get_styles_from_soup(soup)

View File

@ -307,8 +307,15 @@ class Album(TimeStampedModel):
self.save(update_fields=["album_artist"])
def scrape_allmusic(self, force=False) -> None:
if not self.name:
logger.warning(
"Album without a name cannot be scraped",
extra={"album_id": self.id},
)
return
if not self.allmusic_id or force:
slug = get_allmusic_slug(self.name, self.album_artist.name)
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}"
@ -317,7 +324,9 @@ class Album(TimeStampedModel):
self.allmusic_id = slug
self.save(update_fields=["allmusic_id"])
allmusic_data = scrape_data_from_allmusic(self.allmusic_link)
allmusic_data = None
if self.allmusic_link:
allmusic_data = scrape_data_from_allmusic(self.allmusic_link)
if not allmusic_data:
logger.info(f"No allmsuic data for {self} by {self.album_artist}")

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.19 on 2025-05-11 03:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("puzzles", "0002_alter_puzzle_dimensions_in_inches"),
]
operations = [
migrations.RenameField(
model_name="puzzle",
old_name="igdb_id",
new_name="ipdb_id",
),
migrations.RemoveField(
model_name="puzzle",
name="igdb_image",
),
migrations.AddField(
model_name="puzzle",
name="ipdb_image",
field=models.ImageField(
blank=True, null=True, upload_to="puzzles/ipdb/"
),
),
]

View File

@ -1,12 +1,14 @@
from uuid import uuid4
from puzzles.sources import ipdb
import requests
from django.apps import apps
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from puzzles.sources import ipdb
from scrobbles.dataclasses import PuzzleLogData
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
@ -31,17 +33,17 @@ class Puzzle(ScrobblableMixin):
material = models.CharField(max_length=50, **BNULL)
cut_style = models.CharField(max_length=100, **BNULL)
pieces_count = models.IntegerField(**BNULL)
igdb_id = models.CharField(max_length=255, **BNULL)
ipdb_id = models.CharField(max_length=255, **BNULL)
barcode = models.CharField(max_length=13, **BNULL)
igdb_image = models.ImageField(upload_to="puzzles/igdb/", **BNULL)
igdb_image_small = ImageSpecField(
source="igdb_image",
ipdb_image = models.ImageField(upload_to="puzzles/ipdb/", **BNULL)
ipdb_image_small = ImageSpecField(
source="ipdb_image",
processors=[ResizeToFit(100, 100)],
format="JPEG",
options={"quality": 60},
)
igdb_image_medium = ImageSpecField(
source="igdb_image",
ipdb_image_medium = ImageSpecField(
source="ipdb_image",
processors=[ResizeToFit(300, 300)],
format="JPEG",
options={"quality": 75},
@ -65,10 +67,10 @@ class Puzzle(ScrobblableMixin):
return ScrobblableConstants(verb="Solving", tags="puzzle")
@property
def igdb_link(self) -> str:
def ipdb_link(self) -> str:
link = ""
if self.igdb_id:
link = f"https://www.ipdb.plus/IPDb/puzzle.php?id={self.igdb_id}"
if self.ipdb_id:
link = f"https://www.ipdb.plus/IPDb/puzzle.php?id={self.ipdb_id}"
return link
@property
@ -88,17 +90,31 @@ class Puzzle(ScrobblableMixin):
if not puzzle:
puzzle_dict = ipdb.get_puzzle_from_ipdb_id(ipdb_id)
manufacturer_name = puzzle_dict.pop("manufacturer")
manufacturer, _created = PuzzleManufacturer.objects.get_or_create(
name=manufacturer_name
)
ipdb_dict["manufacturer_id"] = manufacturer.id
manufacturer_name = puzzle_dict.pop("manufacturer", None)
if manufacturer_name:
(
manufacturer,
_created,
) = PuzzleManufacturer.objects.get_or_create(
name=manufacturer_name
)
puzzle_dict["manufacturer_id"] = manufacturer.id
genres = puzzle_dict.pop("genres")
genres = puzzle_dict.pop("genres", None)
cover_url = puzzle_dict.pop("ipdb_image_url", None)
puzzle = Puzzle.objects.create(**puzzle_dict)
if genres:
puzzle.genre.add(*genres)
if cover_url:
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
)
return puzzle
def scrobbles(self, user_id):

View File

@ -1,5 +1,246 @@
#!/usr/bin/env python3
import logging
from typing import Optional
import requests
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
IPDB_URL = "https://www.ipdb.plus/IPDb/puzzle.php?id={id}"
def get_first(key: str, result: dict) -> str:
obj = ""
if obj_list := result.get(key):
obj = obj_list[0]
return obj
def get_title_from_soup(soup) -> str:
title = ""
try:
title = soup.find("h1").get_text()
except AttributeError:
pass
except ValueError:
pass
return title
def get_manufacturer_from_soup(soup) -> str:
manufacturer = ""
try:
manufacturer = (
soup.find(class_="infobox").div.contents[0].split("|")[0].strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return manufacturer
def get_pieces_count_from_soup(soup) -> int:
pieces = 0
try:
pieces = int(
soup.find(class_="infobox")
.div.contents[0]
.split("|")[1]
.split(" ")[1]
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return pieces
def get_publish_year_from_soup(soup) -> int:
year = 1900
try:
year = int(
soup.find(class_="infobox").div.contents[0].split("|")[2].strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return year
def get_material_from_soup(soup) -> str:
material = ""
try:
material = (
soup.findAll(class_="infobox")[1]
.findAll("div")[1]
.contents[2]
.split("|")[-1]
.strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return material
def get_cut_style_from_soup(soup) -> str:
cut_style = ""
try:
cut_style = (
soup.findAll(class_="infobox")[1]
.findAll("div")[1]
.contents[2]
.split("|")[-2]
.strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return cut_style
def get_barcode_from_soup(soup) -> str:
barcode = ""
try:
barcode = (
soup.findAll(class_="infobox")[2]
.findAll("div")[0]
.contents[0]
.split("|")[-1]
.split(":")[-1]
.strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
if not barcode:
try:
barcode = (
soup.findAll(class_="infobox")[3]
.findAll("div")[0]
.contents[0]
.split("|")[-1]
.split(":")[-1]
.strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return barcode
def get_image_url_from_soup(soup) -> str:
url = ""
try:
url = (
soup.find(class_="image-container").contents[0].contents[0]["src"]
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return url
def get_orientation_from_soup(soup) -> str:
orientation = ""
try:
orientation = (
soup.findAll(class_="infobox")[1]
.findAll("div")[1]
.contents[0]
.split("|")[0]
.strip()
)
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return orientation
def get_dimensions_from_soup(soup) -> str:
dimensions = ""
try:
height = (
soup.findAll(class_="infobox")[1]
.findAll("div")[1]
.contents[1]["data-bs-height"]
)
width = (
soup.findAll(class_="infobox")[1]
.findAll("div")[1]
.contents[1]["data-bs-width"]
)
dimensions = "x".join([height, width])
except AttributeError:
pass
except ValueError:
pass
except IndexError:
pass
return dimensions
def get_rating_from_soup(soup) -> str:
rating = ""
try:
rating = float(
soup.find(class_="num").get_text().strip("(").strip(")")
)
except AttributeError:
rating = None
except ValueError:
rating = None
return rating
def get_puzzle_from_ipdb_id(ipdb_id: str) -> dict:
return {}
puzzle_url = IPDB_URL.format(id=ipdb_id)
headers = {"User-Agent": "Vrobbler 0.11.12"}
response = requests.get(puzzle_url, headers=headers)
puzzle_dict = {"ipdb_id": ipdb_id}
if response.status_code != 200:
logger.warn(
"Bad response from untappd.com", extra={"response": response}
)
return puzzle_dict
soup = BeautifulSoup(response.text, "html.parser")
puzzle_dict["title"] = get_title_from_soup(soup)
puzzle_dict["orientation"] = get_orientation_from_soup(soup)
puzzle_dict["manufacturer"] = get_manufacturer_from_soup(soup)
puzzle_dict["pieces_count"] = get_pieces_count_from_soup(soup)
puzzle_dict["ipdb_image_url"] = get_image_url_from_soup(soup)
puzzle_dict["dimensions_in_inches"] = get_dimensions_from_soup(soup)
puzzle_dict["publish_year"] = get_publish_year_from_soup(soup)
puzzle_dict["material"] = get_material_from_soup(soup)
puzzle_dict["cut_style"] = get_cut_style_from_soup(soup)
puzzle_dict["barcode"] = get_barcode_from_soup(soup)
return puzzle_dict

View File

@ -0,0 +1,28 @@
import logging
from django.conf import settings
from django.utils import timezone
logger = logging.getLogger(__name__)
class TimezoneMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
def __getUserTimeZone(self, request):
# info = IPResolver(request).getGeoInfo()
# return pytz.country_timezones[info["country_code"]][0]
if not request.user.is_anonymous:
return request.user.profile.timezone
def process_request(self, request):
try:
tz = self.__getUserTimeZone(request) or settings.TIME_ZONE
timezone.activate(tz)
logger.info('Time zone "%s" activated' % str(tz))
except Exception as e:
logger.error("Unable to set timezone: %s" % str(e))

View File

@ -2,6 +2,7 @@ import calendar
import datetime
import json
import logging
from collections import defaultdict
from typing import Optional
from uuid import uuid4
@ -514,6 +515,10 @@ class Scrobble(TimeStampedModel):
MOOD = "Mood", "Mood"
BRICKSET = "BrickSet", "Brick set"
@classmethod
def list(cls):
return list(map(lambda c: c.value, cls))
uuid = models.UUIDField(editable=False, **BNULL)
video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
@ -597,6 +602,51 @@ class Scrobble(TimeStampedModel):
long_play_seconds = models.BigIntegerField(**BNULL)
long_play_complete = models.BooleanField(**BNULL)
@classmethod
def for_year(cls, user, year):
return cls.objects.filter(timestamp__year=year, user=user).order_by(
"-timestamp"
)
@classmethod
def for_month(cls, user, year, month):
return cls.objects.filter(
timestamp__year=year, timestamp__month=month, user=user
).order_by("-timestamp")
@classmethod
def for_day(cls, user, year, month, day):
return cls.objects.filter(
timestamp__year=year,
timestamp__month=month,
timestamp__day=day,
user=user,
).order_by("-timestamp")
@classmethod
def for_week(cls, user, year, week):
return cls.objects.filter(
timestamp__year=year, timestamp__week=week, user=user
).order_by("-timestamp")
@classmethod
def as_dict_by_type(cls, scrobble_qs: models.QuerySet) -> dict:
scrobbles_by_type = defaultdict(list)
for scrobble in scrobble_qs:
scrobbles_by_type[scrobble.media_type].append(scrobble)
return scrobbles_by_type
@classmethod
def in_progress_for_user(cls, user_id: int) -> models.QuerySet:
return cls.objects.filter(
user=user_id,
in_progress=True,
played_to_completion=False,
is_paused=False,
)
@property
def last_serial_scrobble(self) -> Optional["Scrobble"]:
from scrobbles.models import Scrobble
@ -788,6 +838,15 @@ class Scrobble(TimeStampedModel):
def is_long_play(self) -> bool:
return self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA.values()
@property
def elapsed_time(self) -> int | None:
if self.played_to_completion:
if self.playback_position_seconds:
return self.playback_position_seconds
if self.media_obj.run_time_seconds:
return self.media_obj.run_time_seconds
return (timezone.now() - self.timestamp).seconds
@property
def percent_played(self) -> int:
if not self.media_obj:
@ -801,7 +860,7 @@ class Scrobble(TimeStampedModel):
playback_seconds = self.playback_position_seconds
if not playback_seconds:
playback_seconds = (timezone.now() - self.timestamp).seconds
playback_seconds = self.elapsed_time
run_time_secs = self.media_obj.run_time_seconds
percent = int((playback_seconds / run_time_secs) * 100)
@ -917,6 +976,16 @@ class Scrobble(TimeStampedModel):
)
return True
@classmethod
def by_date(cls, media_type: str = "Track"):
cls.objects.filter(media_type=media_type).values(
"timestamp__date"
).annotate(count=models.Count("id")).values(
"timestamp__date", "count"
).order_by(
"-count",
)
@property
def media_obj(self):
media_obj = None

View File

@ -36,6 +36,11 @@ urlpatterns = [
views.web_scrobbler_webhook,
name="web-scrobbler-webhook",
),
path(
"webhook/emacs/",
views.emacs_webhook,
name="emacs-webhook",
),
path(
"webhook/gps/",
views.gps_webhook,

View File

@ -2,10 +2,11 @@ import calendar
import json
import logging
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import pendulum
import pytz
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count, Q
@ -52,7 +53,6 @@ from scrobbles.tasks import (
from scrobbles.utils import (
get_long_plays_completed,
get_long_plays_in_progress,
get_recently_played_board_games,
)
logger = logging.getLogger(__name__)
@ -107,32 +107,94 @@ class RecentScrobbleList(ListView):
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
user = self.request.user
data["date"] = ""
if user.is_authenticated:
self.queryset = self.get_queryset().filter(user=user)
user_id = self.request.user.id
completed_for_user = Scrobble.objects.filter(
played_to_completion=True, user=user
)
data["long_play_in_progress"] = get_long_plays_in_progress(user)
data["play_again"] = get_recently_played_board_games(user)
data["video_scrobble_list"] = completed_for_user.filter(
video__isnull=False
).order_by("-timestamp")[:15]
data["podcast_scrobble_list"] = completed_for_user.filter(
podcast_episode__isnull=False
).order_by("-timestamp")[:15]
data["sport_scrobble_list"] = completed_for_user.filter(
sport_event__isnull=False
).order_by("-timestamp")[:15]
data["videogame_scrobble_list"] = completed_for_user.filter(
video_game__isnull=False
).order_by("-timestamp")[:15]
data["boardgame_scrobble_list"] = completed_for_user.filter(
board_game__isnull=False
).order_by("-timestamp")[:15]
today = timezone.localtime(timezone.now())
date_str = self.request.GET.get("date", "")
date = today
if date_str:
try:
date = pendulum.parse(date_str)
except:
pass
if date_str:
if date_str == "this_week" or "-W" in date_str:
next_date = date + timedelta(weeks=+2)
prev_date = date + timedelta(weeks=-1)
if date.isocalendar()[1] < today.isocalendar()[1]:
data[
"next_link"
] = f"?date={next_date.strftime('%Y-W%W')}"
data["prev_link"] = f"?date={prev_date.strftime('%Y-W%W')}"
data[
"title"
] = f"Week {date.strftime('%-W')} of {date.year}"
data = data | Scrobble.as_dict_by_type(
Scrobble.for_week(
user_id, date.year, date.isocalendar()[1]
)
)
elif date_str == "this_month" or date_str.count("-") == 1:
next_date = date + relativedelta(months=1)
prev_date = date + relativedelta(months=-1)
if date.month < today.month:
data[
"next_link"
] = f"?date={next_date.strftime('%Y-%m')}"
data["title"] = f"{date.strftime('%B %Y')}"
data["prev_link"] = f"?date={prev_date.strftime('%Y-%m')}"
data = data | Scrobble.as_dict_by_type(
Scrobble.for_month(user_id, date.year, date.month)
)
elif date_str == "today" or date_str.count("-") == 2:
next_date = date + timedelta(days=1)
prev_date = date - timedelta(days=1)
data[
"prev_link"
] = f"?date={prev_date.strftime('%Y-%m-%d')}"
if date < today:
data[
"next_link"
] = f"?date={next_date.strftime('%Y-%m-%d')}"
if date == today:
data["title"] = "Today"
else:
data["title"] = f"{date.strftime('%Y-%m-%d')}"
data[
"today_link"
] = f"?date={today.strftime('%Y-%m-%d')}"
data = data | Scrobble.as_dict_by_type(
Scrobble.for_day(
user_id, date.year, date.month, date.day
)
)
elif date_str == "this_year" or date_str.count("-") == 0:
next_date = date + relativedelta(years=+1)
prev_date = date + relativedelta(years=-1)
data["next_link"] = ""
if date.year < today.year:
data["title"] = f"{date.strftime('%Y')}"
data["next_link"] = f"?date={next_date.year}"
data["title"] = f"{date.year}"
data["prev_link"] = f"?date={prev_date.year}"
data = data | Scrobble.as_dict_by_type(
Scrobble.for_year(user_id, date.year)
)
else:
next_date = today - timedelta(days=1)
prev_date = today - timedelta(days=1)
data["title"] = "Today"
data["next_link"] = ""
data["prev_link"] = f"?date={prev_date.strftime('%Y-%m-%d')}"
data = data | Scrobble.as_dict_by_type(
Scrobble.for_day(user_id, date.year, date.month, date.day)
)
data[
"today_link"
] = "" # f"?date={today.strftime('%Y-%m-%d')}"
data["active_imports"] = AudioScrobblerTSVImport.objects.filter(
processing_started__isnull=False,
@ -149,9 +211,7 @@ class RecentScrobbleList(ListView):
return data
def get_queryset(self):
return Scrobble.objects.filter(
track__isnull=False, in_progress=False
).order_by("-timestamp")[:15]
return Scrobble.objects.all().order_by("-timestamp")
class ScrobbleLongPlaysView(TemplateView):
@ -464,6 +524,35 @@ def gps_webhook(request):
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
@permission_classes([IsAuthenticated])
@api_view(["POST"])
@ -698,45 +787,13 @@ class ChartRecordView(TemplateView):
media_type = self.request.GET.get("media", "Track")
user = self.request.user
params = {}
context_data["chart_type"] = self.request.GET.get(
"chart_type", "maloja"
)
context_data["artist_charts"] = {}
if not date:
limit = 20
artist_params = {"user": user, "media_type": "Artist"}
context_data["current_artist_charts"] = {
"today": live_charts(
**artist_params, chart_period="today", limit=limit
),
"week": live_charts(
**artist_params, chart_period="week", limit=limit
),
"month": live_charts(
**artist_params, chart_period="month", limit=limit
),
"year": live_charts(
**artist_params, chart_period="year", limit=limit
),
"all": live_charts(**artist_params, limit=limit),
}
track_params = {"user": user, "media_type": "Track"}
context_data["current_track_charts"] = {
"today": live_charts(
**track_params, chart_period="today", limit=limit
),
"week": live_charts(
**track_params, chart_period="week", limit=limit
),
"month": live_charts(
**track_params, chart_period="month", limit=limit
),
"year": live_charts(
**track_params, chart_period="year", limit=limit
),
"all": live_charts(**track_params, limit=limit),
}
limit = 14
artist = {"user": user, "media_type": "Artist", "limit": limit}
# This is weird. They don't display properly as QuerySets, so we cast to lists
context_data["chart_keys"] = {

View File

@ -163,6 +163,7 @@ SITE_ID = 1
MIDDLEWARE = [
"vrobbler.health_check.HealthCheckMiddleware",
"vrobbler.apps.scrobbles.middleware.TimezoneMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",

View File

@ -1,212 +1,83 @@
{% load humanize %}
{% load naturalduration %}
<div>
<h2>Last Scrobbles</h2>
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> | This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
</div>
<div class="row">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#latest-listened"
type="button" role="tab" aria-controls="home" aria-selected="true">Tracks</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched"
type="button" role="tab" aria-controls="profile" aria-selected="false">Videos</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-podcasted"
type="button" role="tab" aria-controls="profile" aria-selected="false">Podcasts</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-sports"
type="button" role="tab" aria-controls="profile" aria-selected="false">Sports</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-videogames"
type="button" role="tab" aria-controls="profile" aria-selected="false">Video Games</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-boardgames"
type="button" role="tab" aria-controls="profile" aria-selected="false">Board Games</button>
</li>
</ul>
<div class="tab-content" id="myTabContent2">
<div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
aria-labelledby="latest-listened-tab">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Album</th>
<th scope="col">Track</th>
<th scope="col">Artist</th>
</tr>
</thead>
<tbody>
{% for scrobble in object_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
{% if scrobble.track.album.cover_image %}
<td><a href="{{scrobble.track.album.get_absolute_url}}"><img src="{{scrobble.track.album.cover_image_small.url}}" width=25 height=25 style="border:1px solid black;" /></aa></td>
{% else %}
<td><a href="{{scrobble.track.album.get_absolute_url}}">{{scrobble.track.album.name}}</a></td>
{% endif %}
<td><a href="{{scrobble.track.get_absolute_url }}">{{scrobble.track.title}}</a></td>
<td><a href="{{scrobble.track.artist.get_absolute_url }}">{{scrobble.track.artist.name}}</aa></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade show" id="latest-watched" role="tabpanel"
aria-labelledby="latest-watched-tab">
<h2>Latest watched</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Cover</th>
<th scope="col">Title</th>
<th scope="col">Series</th>
</tr>
</thead>
<tbody>
{% for scrobble in video_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
{% if scrobble.video.cover_image %}
<td><img src="{{scrobble.media_obj.cover_image_medium.url}}" width=25 height=25 style="border:1px solid black;" /></td>
{% else %}
<td></td>
{% endif %}
<td><a href="{{scrobble.video.get_absolute_url }}">{% if scrobble.video.tv_series%}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{%endif %} {{scrobble.video.title}}</a></td>
<td><a href="{{scrobble.video.tv_series.get_absolute_url }}">{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}</a>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade show" id="latest-sports" role="tabpanel" aria-labelledby="latest-sports-tab">
<h2>Latest Sports</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
<th scope="col">Round</th>
<th scope="col">League</th>
</tr>
</thead>
<tbody>
{% for scrobble in sport_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.sport_event.title}}</td>
<td>{{scrobble.sport_event.round.name}}</td>
<td>{{scrobble.sport_event.round.season.league}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade show" id="latest-podcasted" role="tabpanel"
aria-labelledby="latest-podcasted-tab">
<h2>Latest Podcasted</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
<th scope="col">Podcast</th>
</tr>
</thead>
<tbody>
{% for scrobble in podcast_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.podcast_episode.title}}</td>
<td>{{scrobble.podcast_episode.podcast}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade show" id="latest-videogames" role="tabpanel"
aria-labelledby="latest-videogames-tab">
<h2>Latest Video Games</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Cover/Screenshot</th>
<th scope="col">Title</th>
<th scope="col">Time played (mins)</th>
<th scope="col">Percent complete</th>
</tr>
</thead>
<tbody>
{% for scrobble in videogame_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
{% if scrobble.screenshot %}
<td><img src="{{scrobble.screenshot_medium.url}}" width=25 height=25 style="border:1px solid black;" /></td>
{% else %}
{% if scrobble.media_obj.hltb_cover %}
<td><img src="{{scrobble.media_obj.hltb_cover_medium.url}}" width=25 height=25 style="border:1px solid black;" /></td>
{% endif %}
{% endif %}
<td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></td>
<td>{{scrobble.playback_position_seconds|natural_duration}}</td>
<td>{{scrobble.percent_played}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade show" id="latest-boardgames" role="tabpanel"
aria-labelledby="latest-boardgames-tab">
<h2>Latest Board Games</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Cover</th>
<th scope="col">Title</th>
<th scope="col">Time played (mins)</th>
</tr>
</thead>
<tbody>
{% for scrobble in boardgame_scrobble_list %}
<tr>
<td>{{scrobble.timestamp|naturaltime}}</td>
<td><img src="{{scrobble.media_obj.cover_medium.url}}" width=25 height=25 style="border:1px solid black;" /></td>
<td><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></td>
<td>{{scrobble.playback_position_seconds|natural_duration}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div>
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> | This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
</div>
</div>
<div class="row">
<div class="col-md">
{% if Track %}
<h2>Music</h2>
{% with scrobbles=Track %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
</div>
<div class="col-md">
{% if Task %}
<h2>Latest tasks</h2>
{% with scrobbles=Task %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if Video %}
<h2>Videos</h2>
{% with scrobbles=Video %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if WebPage %}
<h4>Web pages</h4>
{% with scrobbles=WebPage %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if SportEvent %}
<h2>Sports</h2>
{% with scrobbles=SportEvent %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if PodcastEpisode %}
<h2>Latest podcasts</h2>
{% with scrobbles=PodcastEpisode %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if VideoGame %}
<h4>Video games</h4>
{% with scrobbles=VideoGame %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if BoardGame %}
<h4>Board games</h4>
{% with scrobbles=BoardGame %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if Beer %}
<h4>Beers</h4>
{% with scrobbles=Beer %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
{% if Book %}
<h4>Books</h4>
{% with scrobbles=Book %}
{% include "scrobbles/_scrobble_table.html" %}
{% endwith %}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,7 @@
{% load humanize %}
{% load naturalduration %}
<tr>
<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>{{scrobble.elapsed_time|natural_duration}}</td>
</tr>

View File

@ -0,0 +1,22 @@
{% load humanize %}
{% load naturalduration %}
<div class="tab-pane fade show" id="latest-beers" role="tabpanel"
aria-labelledby="latest-beers-tab">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
<th scope="col">Time</th>
</tr>
</thead>
<tbody>
{% for scrobble in scrobbles %}
{% include "scrobbles/_row.html" %}
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@ -1,173 +1,176 @@
{% load static %}
<h2>Top Artist</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for key, name in chart_keys.items %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.counter == 2 %}active{% endif %}"
id="artist-{{key}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{key}}"
type="button" role="tab" aria-controls="home" aria-selected="true">{{name}}</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="artistTabContent" class="maloja-chart">
{% for key, artists in current_artist_charts.items %}
<div class="tab-pane fade {% if forloop.counter == 2 %}show active{% endif %}" id="artist-{{key}}" role="tabpanel" aria-labelledby="artist-{{key}}-tab">
<div style="display:block">
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">#1 {{artists.0.name}}</div>
{% if artists.0 %}
{% if artists.0.thumbnail %}
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{{artists.0.thumbnail_medium.url}}" width="300px"></a>
{% else %}
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{% static "images/not-found.jpg" %}" width="300px"></a>
{% endif %}
{% endif %}
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#2 {{artists.1.name}}</div>
{% if artists.1 %}
{% if artists.1.thumbnail %}
<a href="{{artists.1.get_absolute_url}}"><img lt="{{artists.1.name}}" src="{{artists.1.thumbnail_medium.url}}" width="150px"></a>
<div class="row">
<h2>Top Artist</h2>
<ul class="nav nav-tabs" id="artistTab" role="tablist">
{% for key, name in chart_keys.items %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.counter == 2 %}active{% endif %}"
id="artist-{{key}}-tab" data-bs-toggle="tab" data-bs-target="#artist-{{key}}"
type="button" role="tab" aria-controls="home" aria-selected="true">{{name}}</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="artistTabContent" class="maloja-chart">
{% for key, artists in current_artist_charts.items %}
<div class="tab-pane fade {% if forloop.counter == 2 %}show active{% endif %}" id="artist-{{key}}" role="tabpanel" aria-labelledby="artist-{{key}}-tab">
<div style="display:block">
<div style="float:left;">
<div class="image-wrapper" style="display:flex; flex-wrap: wrap; margin:0">
<div class="caption">#1 {{artists.0.name}}</div>
{% if artists.0 %}
{% if artists.0.thumbnail %}
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{{artists.0.thumbnail_medium.url}}" width="300px"></a>
{% else %}
<a href="{{artists.1.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#3 {{artists.2.name}}</div>
{% if artists.2 %}
{% if artists.2.thumbnail %}
<a href="{{artists.2.get_absolute_url}}"><img src="{{artists.2.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.2.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#4 {{artists.3.name}}</div>
{% if artists.3 %}
{% if artists.3.thumbnail %}
<a href="{{artists.3.get_absolute_url}}"><img src="{{artists.3.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.3.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#5 {{artists.4.name}}</div>
{% if artists.4 %}
{% if artists.4.thumbnail %}
<a href="{{artists.4.get_absolute_url}}"><img src="{{artists.4.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.4.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
<a href="{{artists.0.get_absolute_url}}"><img lt="{{artists.0.name}}" src="{% static "images/not-found.jpg" %}" width="300px"></a>
{% endif %}
{% endif %}
</div>
</div>
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#6 {{artists.5.name}}</div>
{% if artists.5 %}
{% if artists.5.thumbnail %}
<a href="{{artists.5.get_absolute_url}}"><img src="{{artists.5.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.5.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#2 {{artists.1.name}}</div>
{% if artists.1 %}
{% if artists.1.thumbnail %}
<a href="{{artists.1.get_absolute_url}}"><img lt="{{artists.1.name}}" src="{{artists.1.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.1.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#3 {{artists.2.name}}</div>
{% if artists.2 %}
{% if artists.2.thumbnail %}
<a href="{{artists.2.get_absolute_url}}"><img src="{{artists.2.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.2.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#4 {{artists.3.name}}</div>
{% if artists.3 %}
{% if artists.3.thumbnail %}
<a href="{{artists.3.get_absolute_url}}"><img src="{{artists.3.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.3.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:50%">
<div class="caption-medium">#5 {{artists.4.name}}</div>
{% if artists.4 %}
{% if artists.4.thumbnail %}
<a href="{{artists.4.get_absolute_url}}"><img src="{{artists.4.thumbnail_medium.url}}" width="150px"></a>
{% else %}
<a href="{{artists.4.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="150px"></a>
{% endif %}
{% endif %}
</div>
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#7 {{artists.6.name}}</div>
{% if artists.6 %}
{% if artists.6.thumbnail %}
<a href="{{artists.6.get_absolute_url}}"><img src="{{artists.6.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.6.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#8 {{artists.7.name}}</div>
{% if artists.7 %}
{% if artists.7.thumbnail %}
<a href="{{artists.7.get_absolute_url}}"><img src="{{artists.7.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.7.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#9 {{artists.8.name}}</div>
{% if artists.8 %}
{% if artists.8.thumbnail %}
<a href="{{artists.8.get_absolute_url}}"><img src="{{artists.8.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.8.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#10 {{artists.9.name}}</div>
{% if artists.9 %}
{% if artists.9.thumbnail %}
<a href="{{artists.9.get_absolute_url}}"><img src="{{artists.9.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.9.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#11 {{artists.10.name}}</div>
{% if artists.10 %}
{% if artists.10.thumbnail %}
<a href="{{artists.10.get_absolute_url}}"><img src="{{artists.10.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.10.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#12 {{artists.11.name}}</div>
{% if artists.11 %}
{% if artists.11.thumbnail %}
<a href="{{artists.11.get_absolute_url}}"><img src="{{artists.11.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.11.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#13 {{artists.12.name}}</div>
{% if artists.12 %}
{% if artists.12.thumbnail %}
<a href="{{artists.12.get_absolute_url}}"><img src="{{artists.12.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.12.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#14 {{artists.13.name}}</div>
{% if artists.13 %}
{% if artists.13.thumbnail %}
<a href="{{artists.13.get_absolute_url}}"><img src="{{artists.13.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.13.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div style="float:left; width:300px;">
<div style="display:flex; flex-wrap: wrap;">
<div class="image-wrapper" class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#6 {{artists.5.name}}</div>
{% if artists.5 %}
{% if artists.5.thumbnail %}
<a href="{{artists.5.get_absolute_url}}"><img src="{{artists.5.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.5.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#7 {{artists.6.name}}</div>
{% if artists.6 %}
{% if artists.6.thumbnail %}
<a href="{{artists.6.get_absolute_url}}"><img src="{{artists.6.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.6.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#8 {{artists.7.name}}</div>
{% if artists.7 %}
{% if artists.7.thumbnail %}
<a href="{{artists.7.get_absolute_url}}"><img src="{{artists.7.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.7.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#9 {{artists.8.name}}</div>
{% if artists.8 %}
{% if artists.8.thumbnail %}
<a href="{{artists.8.get_absolute_url}}"><img src="{{artists.8.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.8.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#10 {{artists.9.name}}</div>
{% if artists.9 %}
{% if artists.9.thumbnail %}
<a href="{{artists.9.get_absolute_url}}"><img src="{{artists.9.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.9.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#11 {{artists.10.name}}</div>
{% if artists.10 %}
{% if artists.10.thumbnail %}
<a href="{{artists.10.get_absolute_url}}"><img src="{{artists.10.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.10.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#12 {{artists.11.name}}</div>
{% if artists.11 %}
{% if artists.11.thumbnail %}
<a href="{{artists.11.get_absolute_url}}"><img src="{{artists.11.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.11.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#13 {{artists.12.name}}</div>
{% if artists.12 %}
{% if artists.12.thumbnail %}
<a href="{{artists.12.get_absolute_url}}"><img src="{{artists.12.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.12.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
<div class="image-wrapper" class="image-wrapper" style="width:33;">
<div class="caption-small">#14 {{artists.13.name}}</div>
{% if artists.13 %}
{% if artists.13.thumbnail %}
<a href="{{artists.13.get_absolute_url}}"><img src="{{artists.13.thumbnail_medium.url}}" width="100px"></a>
{% else %}
<a href="{{artists.13.get_absolute_url}}"><img src="{% static "images/not-found.jpg" %}" width="100px"></a>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<div class="row">

View File

@ -2,12 +2,54 @@
{% block title %}{{name}}{% endblock %}
{% block head_extra %}
<style>
.container { margin-bottom:100px; }
h2 { padding-top:20px; }
.image-wrapper {
contain: content;
}
.image-wrapper :hover {
background:rgba(0,0,0,0.3);
}
.caption {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 90%;
color:white;
background:rgba(0,0,0,0.4);
}
.caption-medium {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 75%;
color:white;
background:rgba(0,0,0,0.4);
}
.caption-small {
position: fixed;
top: 5px;
left: 5px;
padding: 3px;
font-size: 60%;
color:white;
background:rgba(0,0,0,0.4);
}
</style>
{% endblock %}
{% block lists %}
<div "calss="row>
{% include "scrobbles/_top_charts.html" %}
</div>
{% if chart_type == "maloja" %}
{% include "scrobbles/_top_charts.html" %}
{% else %}
<div class="row">
{% if artist_charts %}
<div class="col-md">
@ -151,5 +193,6 @@
</div>
{% endif %}
</div>
{% endif %}
{% endblock %}

View File

@ -49,9 +49,28 @@
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
<h1 class="h2">{% if date %}{{date|naturaltime}}{% else %}{{title}}{% endif %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
{% if user.is_authenticated %}
<div class="btn-group me-2">
{% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}
<form action="{% url 'scrobbles:lastfm-import' %}" method="get">
<button type="submit" class="btn btn-sm btn-outline-secondary">Last.fm Sync</button>
</form>
{% endif %}
{% if prev_link %}
<a type="button" class="btn btn-sm btn-outline-secondary" href="{{prev_link}}"
data-bs-target="#">Previous</a>
{% endif %}
{% if today_link %}
<a type="button" class="btn btn-sm btn-outline-secondary" href="{{today_link}}"
data-bs-target="#">Today</a>
{% endif %}
{% if next_link %}
<a type="button" class="btn btn-sm btn-outline-secondary" href="{{next_link}}"
data-bs-target="#">Next</a>
{% endif %}
</div>
<div class="btn-group me-2">
{% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}
@ -69,11 +88,13 @@
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<div data-feather="calendar"></div>
This week
{{title}}
</button>
<div class="dropdown-menu" data-bs-toggle="#graphDataChange" aria-labelledby="graphDateButton">
<a class="dropdown-item" href="#">This month</a>
<a class="dropdown-item" href="#">This year</a>
<a class="dropdown-item" href="?date=today">Today</a>
<a class="dropdown-item" href="?date=this_week">This week</a>
<a class="dropdown-item" href="?date=this_month">This month</a>
<a class="dropdown-item" href="?date=this_year">This year</a>
</div>
</div>
</div>

View File

@ -1,5 +1,8 @@
<html>
<style>label { display:none; } textarea {height:8em; width:100%}</style>
<head>
<style>label { display:none; } textarea {height:8em; width:100%}</style>
<title>Reading {{object.title}}</title>
</head>
<body>
<form method=post>{% csrf_token %}
{{form.as_p}}
@ -7,7 +10,7 @@
<input type="submit" value="Finish" />
</form>
<a href="{{object.url}}" target="_blank">Open in new window</a>
<iframe style="height:78%; width:100%" src="{{object.url}}" title="{{object.url}}" allowfullscreen sandbox>
<iframe style="height:84%; width:100%" src="{{object.url}}" title="{{object.url}}" allowfullscreen sandbox>
<p>
Page could not be opened, use link above to read.
</p>