Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a880a2f2f | |||
| 248d3f2d3e | |||
| e243fec679 | |||
| de9b4ee9c1 | |||
| bf9a6a9679 |
63
PROJECT.org
63
PROJECT.org
@ -544,6 +544,69 @@ log a warning and move on.
|
|||||||
We should have a global view `/favorites/` that shows the logged in users's
|
We should have a global view `/favorites/` that shows the logged in users's
|
||||||
favorited media objects.
|
favorited media objects.
|
||||||
|
|
||||||
|
* Version 50.2 [2/2]
|
||||||
|
** DONE [#B] Koreader imports only import single-page scrobbles the next day :bug:books:importers:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: b50141fd-cda6-4a3a-afd3-cd8499e7523e
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
When you read a single page in a book in Koreader and try to import it, the scrobble is only
|
||||||
|
created the day after, not on the day of the reading.
|
||||||
|
|
||||||
|
** DONE [#A] Fix bugs in celery tasks causing imports to fail :bug:celery:tasks:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: d1171cb0-6413-44b8-a68a-019a4d2fb285
|
||||||
|
:END:
|
||||||
|
|
||||||
|
*** Description
|
||||||
|
|
||||||
|
Seems like all celery tasks are failing for different reasons except the chart
|
||||||
|
updates.
|
||||||
|
|
||||||
|
*** Errors
|
||||||
|
**** scrobbles.tasks.send_notification_for_in_progress
|
||||||
|
#+begin_src bash
|
||||||
|
KeyError: 'track'
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/usr/local/lib/python3.11/site-packages/django/db/backends/utils.py", line 89, in _execute
|
||||||
|
return self.cursor.execute(sql, params)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
psycopg2.errors.UndefinedColumn: column music_track.artist_id does not exist
|
||||||
|
LINE 1: ..."."title", "music_track"."base_run_time_seconds", "music_tra...
|
||||||
|
^
|
||||||
|
HINT: Perhaps you meant to reference the column "music_track.artist_fk_id".
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
**** scrobbles.tasks.import_from_webdav_all_users
|
||||||
|
#+begin_src bash
|
||||||
|
File "/usr/local/lib/python3.11/site-packages/vrobbler/apps/scrobbles/importers/webdav.py", line 166, in scan_webdav_for_koreader
|
||||||
|
if last_import and last_import.webdav_etag and remote_etag:
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
AttributeError: 'KoReaderImport' object has no attribute 'webdav_etag'
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
**** scrobbles.tasks.process_bgstats_import
|
||||||
|
#+begin_src bash
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/usr/local/lib/python3.11/site-packages/django/db/backends/utils.py", line 89, in _execute
|
||||||
|
return self.cursor.execute(sql, params)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
psycopg2.errors.NotNullViolation: null value in column "visibility" of relation "scrobbles_scrobble" violates not-null constraint
|
||||||
|
DETAIL: Failing row contains (374463, 2026-06-11 13:27:06.528319+00, 2026-06-11 13:27:06.52834+00, 2026-06-11 13:17:34+00, 180, f, f, BG Stats, 1, null, t, {"players": [{"new": false, "win": false, "rank": 0, "role": "",..., null, null, null, 8e73ceec-b731-4623-9637-712bbf9f76ce, null, null, null, null, , null, BoardGame, 324, null, null, America/New_York, null, null, , null, null, , null, null, null, null, null, null, null, null, null, null, null).
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* Version 50.1 [1/1]
|
||||||
|
** DONE [#B] Fix bug in charts where only #1 is displayed :charts:templates:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 7136dffb-e6b7-184b-48ac-bb09bae0b0f0
|
||||||
|
:END:
|
||||||
|
|
||||||
* Version 50.0 [2/2]
|
* Version 50.0 [2/2]
|
||||||
** DONE [#A] Allow updating all a user's scrobble visibility at once :scrobbles:sharing:feature:
|
** DONE [#A] Allow updating all a user's scrobble visibility at once :scrobbles:sharing:feature:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "50.0"
|
version = "50.2"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -187,18 +187,20 @@ def build_page_data(page_rows: list, book_map: dict, user_tz=None) -> dict:
|
|||||||
book_ids_not_found.append(koreader_book_id)
|
book_ids_not_found.append(koreader_book_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if "pages" not in book_map[koreader_book_id].keys():
|
book_map[koreader_book_id].setdefault("pages", [])
|
||||||
book_map[koreader_book_id]["pages"] = {}
|
|
||||||
|
|
||||||
page_number = page_row[KoReaderPageStatColumn.PAGE.value]
|
page_number = page_row[KoReaderPageStatColumn.PAGE.value]
|
||||||
duration = page_row[KoReaderPageStatColumn.DURATION.value]
|
duration = page_row[KoReaderPageStatColumn.DURATION.value]
|
||||||
start_ts = page_row[KoReaderPageStatColumn.START_TIME.value]
|
start_ts = page_row[KoReaderPageStatColumn.START_TIME.value]
|
||||||
|
|
||||||
book_map[koreader_book_id]["pages"][page_number] = {
|
book_map[koreader_book_id]["pages"].append(
|
||||||
"duration": duration,
|
{
|
||||||
"start_ts": start_ts,
|
"page_number": page_number,
|
||||||
"end_ts": start_ts + duration,
|
"duration": duration,
|
||||||
}
|
"start_ts": start_ts,
|
||||||
|
"end_ts": start_ts + duration,
|
||||||
|
}
|
||||||
|
)
|
||||||
if book_ids_not_found:
|
if book_ids_not_found:
|
||||||
logger.info(f"Found pages for books not in file: {set(book_ids_not_found)}")
|
logger.info(f"Found pages for books not in file: {set(book_ids_not_found)}")
|
||||||
return book_map
|
return book_map
|
||||||
@ -225,11 +227,12 @@ def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobbl
|
|||||||
pages_processed = 0
|
pages_processed = 0
|
||||||
total_pages_read = len(book_map[koreader_book_id]["pages"])
|
total_pages_read = len(book_map[koreader_book_id]["pages"])
|
||||||
ordered_pages = sorted(
|
ordered_pages = sorted(
|
||||||
book_map[koreader_book_id]["pages"].items(),
|
book_map[koreader_book_id]["pages"],
|
||||||
key=lambda x: x[1]["start_ts"],
|
key=lambda x: x["start_ts"],
|
||||||
)
|
)
|
||||||
|
|
||||||
for cur_page_number, stats in ordered_pages:
|
for stats in ordered_pages:
|
||||||
|
page_number = stats["page_number"]
|
||||||
pages_processed += 1
|
pages_processed += 1
|
||||||
|
|
||||||
seconds_from_last_page = 0
|
seconds_from_last_page = 0
|
||||||
@ -243,12 +246,14 @@ def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobbl
|
|||||||
)
|
)
|
||||||
|
|
||||||
end_of_reading = pages_processed == total_pages_read
|
end_of_reading = pages_processed == total_pages_read
|
||||||
big_jump_to_this_page = (cur_page_number - last_page_number) > 10
|
big_jump_to_this_page = (page_number - last_page_number) > 10
|
||||||
is_session_gap = seconds_from_last_page > SESSION_GAP_SECONDS
|
is_session_gap = seconds_from_last_page > SESSION_GAP_SECONDS
|
||||||
if (is_session_gap and not big_jump_to_this_page) or end_of_reading:
|
if (is_session_gap and not big_jump_to_this_page) or end_of_reading:
|
||||||
should_create_scrobble = True
|
should_create_scrobble = True
|
||||||
|
|
||||||
if should_create_scrobble:
|
if should_create_scrobble:
|
||||||
|
if not scrobble_page_data:
|
||||||
|
scrobble_page_data[page_number] = stats
|
||||||
scrobble_page_data = dict(
|
scrobble_page_data = dict(
|
||||||
sorted(
|
sorted(
|
||||||
scrobble_page_data.items(),
|
scrobble_page_data.items(),
|
||||||
@ -276,13 +281,6 @@ def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobbl
|
|||||||
datetime.fromtimestamp(int(last_page.get("end_ts")))
|
datetime.fromtimestamp(int(last_page.get("end_ts")))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Adjust for Daylight Saving Time
|
|
||||||
# if timestamp.dst() == timedelta(
|
|
||||||
# 0
|
|
||||||
# ) or stop_timestamp.dst() == timedelta(0):
|
|
||||||
# timestamp = timestamp - timedelta(hours=1)
|
|
||||||
# stop_timestamp = stop_timestamp - timedelta(hours=1)
|
|
||||||
|
|
||||||
scrobble = Scrobble.objects.filter(
|
scrobble = Scrobble.objects.filter(
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
book_id=book_id,
|
book_id=book_id,
|
||||||
@ -291,7 +289,7 @@ def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobbl
|
|||||||
|
|
||||||
if not scrobble:
|
if not scrobble:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Queueing scrobble for {book_id}, page {cur_page_number}"
|
f"Queueing scrobble for {book_id}, page {page_number}"
|
||||||
)
|
)
|
||||||
log_data = {
|
log_data = {
|
||||||
"koreader_hash": book_dict.get("hash"),
|
"koreader_hash": book_dict.get("hash"),
|
||||||
@ -324,9 +322,9 @@ def build_scrobbles_from_book_map(book_map: dict, user: "User") -> list["Scrobbl
|
|||||||
scrobble_page_data = {}
|
scrobble_page_data = {}
|
||||||
|
|
||||||
# We accumulate pages for the scrobble until we should create a new one
|
# We accumulate pages for the scrobble until we should create a new one
|
||||||
scrobble_page_data[cur_page_number] = stats
|
scrobble_page_data[page_number] = stats
|
||||||
|
|
||||||
last_page_number = cur_page_number
|
last_page_number = page_number
|
||||||
prev_page_stats = stats
|
prev_page_stats = stats
|
||||||
if pages_not_found:
|
if pages_not_found:
|
||||||
logger.info(f"Pages not found for books: {set(pages_not_found)}")
|
logger.info(f"Pages not found for books: {set(pages_not_found)}")
|
||||||
|
|||||||
@ -32,8 +32,6 @@ class KoReaderBookRows:
|
|||||||
DEFAULT_STR = "N/A"
|
DEFAULT_STR = "N/A"
|
||||||
DEFAULT_INT = 0
|
DEFAULT_INT = 0
|
||||||
DEFAULT_TIME = 1703800469
|
DEFAULT_TIME = 1703800469
|
||||||
BOOK_ROWS = []
|
|
||||||
PAGE_STATS_ROWS = []
|
|
||||||
|
|
||||||
def _gen_random_row(self, i):
|
def _gen_random_row(self, i):
|
||||||
wiggle = random.randrange(15)
|
wiggle = random.randrange(15)
|
||||||
@ -110,6 +108,8 @@ class KoReaderBookRows:
|
|||||||
end_session = True
|
end_session = True
|
||||||
|
|
||||||
def __init__(self, book_count=0, **kwargs):
|
def __init__(self, book_count=0, **kwargs):
|
||||||
|
self.BOOK_ROWS = []
|
||||||
|
self.PAGE_STATS_ROWS = []
|
||||||
self._generate_random_book_rows(book_count)
|
self._generate_random_book_rows(book_count)
|
||||||
self._generate_custom_book_row(**kwargs)
|
self._generate_custom_book_row(**kwargs)
|
||||||
self._generate_random_page_stats_rows()
|
self._generate_random_page_stats_rows()
|
||||||
|
|||||||
@ -7,7 +7,10 @@ register = template.Library()
|
|||||||
def get_item(dictionary, key):
|
def get_item(dictionary, key):
|
||||||
if isinstance(dictionary, dict):
|
if isinstance(dictionary, dict):
|
||||||
return dictionary.get(key)
|
return dictionary.get(key)
|
||||||
return None
|
try:
|
||||||
|
return dictionary[int(key)]
|
||||||
|
except (IndexError, KeyError, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
|
|||||||
@ -145,7 +145,7 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
},
|
},
|
||||||
"import-from-webdav": {
|
"import-from-webdav": {
|
||||||
"task": "scrobbles.tasks.import_from_webdav_all_users",
|
"task": "scrobbles.tasks.import_from_webdav_all_users",
|
||||||
"schedule": crontab(minute="*/3"),
|
"schedule": crontab(minute="*/2"),
|
||||||
},
|
},
|
||||||
# Deprecated: BG Stats files now picked up from WebDAV var/bgstats/
|
# Deprecated: BG Stats files now picked up from WebDAV var/bgstats/
|
||||||
# "import-from-imap": {
|
# "import-from-imap": {
|
||||||
|
|||||||
Reference in New Issue
Block a user