Compare commits

...

15 Commits

12 changed files with 177 additions and 703 deletions

View File

@ -4,7 +4,7 @@ expected_desc = (
"NPR's Up First is the news you need to start your day. " "NPR's Up First is the news you need to start your day. "
"The three biggest stories of the day, with reporting and analysis " "The three biggest stories of the day, with reporting and analysis "
"from NPR News — in 10 minutes. Available weekdays by 6 a.m. ET, " "from NPR News — in 10 minutes. Available weekdays by 6 a.m. ET, "
"with hosts Leila Fadel, Steve Inskeep, Rachel Martin and A Martinez. " "with hosts Leila Fadel, Steve Inskeep, Michel Martin and A Martinez. "
"Also available on Saturdays by 8 a.m. ET, with Ayesha Rascoe and " "Also available on Saturdays by 8 a.m. ET, with Ayesha Rascoe and "
"Scott Simon. On Sundays, hear a longer exploration behind the " "Scott Simon. On Sundays, hear a longer exploration behind the "
"headlines with Rachel Martin, available by 8 a.m. ET. Subscribe " "headlines with Rachel Martin, available by 8 a.m. ET. Subscribe "

View File

@ -3,7 +3,8 @@
A fun way to keep track of things in the project to fix or improve. A fun way to keep track of things in the project to fix or improve.
* Version 1.0.0 * Version 1.0.0
** TODO Add a "finished_timestamp" so we don't rely on content length :improvement:scrobbling: ** DONE Add a "stop_timestamp" so we don't rely on content length :improvement:scrobbling:
CLOSED: [2023-04-02 Sun 23:58]
Essentially, we currently have the timestamp as when the content began Essentially, we currently have the timestamp as when the content began
scrobbling and then calculate the finish time from the length of the content. scrobbling and then calculate the finish time from the length of the content.

View File

@ -1,7 +1,7 @@
import re
import codecs import codecs
import logging import logging
import os import os
import re
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
@ -10,10 +10,11 @@ from typing import Iterable, List
import pytz import pytz
import requests import requests
from books.models import Author, Book, Page from books.models import Author, Book, Page
from books.openlibrary import get_author_openlibrary_id
from django.db.models import Sum
from pylast import httpx, tempfile from pylast import httpx, tempfile
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from stream_sqlite import stream_sqlite from stream_sqlite import stream_sqlite
from vrobbler.apps.books.openlibrary import get_author_openlibrary_id
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -105,22 +106,15 @@ def get_book_map_from_sqlite(rows: Iterable) -> dict:
# This will try to fix metadata by looking it up on OL # This will try to fix metadata by looking it up on OL
book.fix_metadata() book.fix_metadata()
playback_position_seconds = 0
if book_row[KoReaderBookColumn.TOTAL_READ_TIME.value]:
playback_position_seconds = book_row[
KoReaderBookColumn.TOTAL_READ_TIME.value
]
pages_read = 0
if book_row[KoReaderBookColumn.TOTAL_READ_PAGES.value]:
pages_read = int(
book_row[KoReaderBookColumn.TOTAL_READ_PAGES.value]
)
timestamp = datetime.utcfromtimestamp(
book_row[KoReaderBookColumn.LAST_OPEN.value]
).replace(tzinfo=pytz.utc)
book.refresh_from_db() book.refresh_from_db()
book_id_map[book_row[KoReaderBookColumn.ID.value]] = book.id total_seconds = 0
if book_row[KoReaderBookColumn.TOTAL_READ_TIME.value]:
total_seconds = book_row[KoReaderBookColumn.TOTAL_READ_TIME.value]
book_id_map[book_row[KoReaderBookColumn.ID.value]] = (
book.id,
total_seconds,
)
return book_id_map return book_id_map
@ -131,12 +125,14 @@ def build_scrobbles_from_pages(
new_scrobbles = [] new_scrobbles = []
new_scrobbles = [] new_scrobbles = []
pages_created = [] pages_found = []
book_read_time_map = {}
for page_row in rows: for page_row in rows:
koreader_id = page_row[KoReaderPageStatColumn.ID_BOOK.value] koreader_id = page_row[KoReaderPageStatColumn.ID_BOOK.value]
page_number = page_row[KoReaderPageStatColumn.PAGE.value] page_number = page_row[KoReaderPageStatColumn.PAGE.value]
ts = page_row[KoReaderPageStatColumn.START_TIME.value] ts = page_row[KoReaderPageStatColumn.START_TIME.value]
book_id = book_id_map[koreader_id] book_id = book_id_map[koreader_id][0]
book_read_time_map[book_id] = book_id_map[koreader_id][1]
page, page_created = Page.objects.get_or_create( page, page_created = Page.objects.get_or_create(
book_id=book_id, number=page_number, user_id=user_id book_id=book_id, number=page_number, user_id=user_id
@ -149,25 +145,39 @@ def build_scrobbles_from_pages(
KoReaderPageStatColumn.DURATION.value KoReaderPageStatColumn.DURATION.value
] ]
page.save(update_fields=["start_time", "duration_seconds"]) page.save(update_fields=["start_time", "duration_seconds"])
pages_created.append(page) pages_found.append(page)
for page in pages_created: playback_position_seconds = 0
for page in set(pages_found):
# Add up page seconds to set the aggregate time of all pages to reading time
playback_position_seconds = (
playback_position_seconds + page.duration_seconds
)
if page.is_scrobblable: if page.is_scrobblable:
# Page number is a placeholder, we'll re-preocess this after creation # Check to see if a scrobble with this timestamp, book and user already exists
logger.debug( scrobble = Scrobble.objects.filter(
f"Queueing scrobble for {page.book}, page {page.number}" timestamp=page.start_time,
)
new_scrobble = Scrobble(
book_id=page.book_id, book_id=page.book_id,
user_id=user_id, user_id=user_id,
source="KOReader", ).first()
timestamp=page.start_time, if not scrobble:
played_to_completion=True, logger.debug(
in_progress=False, f"Queueing scrobble for {page.book}, page {page.number}"
book_pages_read=page.number, )
long_play_complete=False, new_scrobble = Scrobble(
) book_id=page.book_id,
new_scrobbles.append(new_scrobble) user_id=user_id,
source="KOReader",
timestamp=page.start_time,
played_to_completion=True,
playback_position_seconds=playback_position_seconds,
in_progress=False,
book_pages_read=page.number,
long_play_complete=False,
)
new_scrobbles.append(new_scrobble)
# After setting a scrobblable page, reset our accumulator
playback_position_seconds = 0
return new_scrobbles return new_scrobbles
@ -176,15 +186,16 @@ def enrich_koreader_scrobbles(scrobbles: list) -> None:
for media completion""" for media completion"""
for scrobble in scrobbles: for scrobble in scrobbles:
scrobble.book_pages_read = scrobble.book.page_set.last().number
# But if there's a next scrobble, set pages read to their starting page
#
if scrobble.next: if scrobble.next:
# Set pages read to the starting page of the next scrobble minus one, if it exists
scrobble.book_pages_read = scrobble.next.book_pages_read - 1 scrobble.book_pages_read = scrobble.next.book_pages_read - 1
scrobble.save(update_fields=["book_pages_read"]) scrobble.long_play_seconds = scrobble.book.page_set.filter(
else: number__lte=scrobble.book_pages_read
# Set pages read to the last page we have ).aggregate(Sum("duration_seconds"))["duration_seconds__sum"]
scrobble.book_pages_read = scrobble.book.page_set.last().number
scrobble.save(update_fields=["book_pages_read", "long_play_complete"]) scrobble.save(update_fields=["book_pages_read", "long_play_seconds"])
def process_koreader_sqlite_url(file_url, user_id) -> list: def process_koreader_sqlite_url(file_url, user_id) -> list:
@ -194,6 +205,7 @@ def process_koreader_sqlite_url(file_url, user_id) -> list:
for table_name, pragma_table_info, rows in stream_sqlite( for table_name, pragma_table_info, rows in stream_sqlite(
_sqlite_bytes(file_url), max_buffer_size=1_048_576 _sqlite_bytes(file_url), max_buffer_size=1_048_576
): ):
logger.debug(f"Found table {table_name} - processing")
if table_name == "book": if table_name == "book":
book_id_map = get_book_map_from_sqlite(rows) book_id_map = get_book_map_from_sqlite(rows)
@ -201,6 +213,7 @@ def process_koreader_sqlite_url(file_url, user_id) -> list:
new_scrobbles = build_scrobbles_from_pages( new_scrobbles = build_scrobbles_from_pages(
rows, book_id_map, user_id rows, book_id_map, user_id
) )
logger.debug(f"Creating {len(new_scrobbles)} new scrobbles")
created = [] created = []
if new_scrobbles: if new_scrobbles:

View File

@ -112,7 +112,7 @@ class Book(LongPlayScrobblableMixin):
if book_dict.get("pages") == None: if book_dict.get("pages") == None:
book_dict.pop("pages") book_dict.pop("pages")
ol_title = book_dict.get("title") ol_title = book_dict.get("title", "")
if ol_title.lower() != self.title.lower(): if ol_title.lower() != self.title.lower():
logger.warn( logger.warn(
@ -122,21 +122,6 @@ class Book(LongPlayScrobblableMixin):
Book.objects.filter(pk=self.id).update(**book_dict) Book.objects.filter(pk=self.id).update(**book_dict)
self.refresh_from_db() self.refresh_from_db()
# Process authors
author = None
author_created = False
if ol_author_id:
author, author_created = Author.objects.get_or_create(
openlibrary_id=ol_author_id
)
if author_created or force_update:
author.fix_metadata()
if not author and ol_author_name:
author, author_created = Author.objects.get_or_create(
name=ol_author_name
)
self.authors.add(author)
if cover_url: if cover_url:
r = requests.get(cover_url) r = requests.get(cover_url)
if r.status_code == 200: if r.status_code == 200:

View File

@ -17,7 +17,6 @@ from music.models import Album, Artist, Track
def get_or_create_artist(name: str, mbid: str = None) -> Artist: def get_or_create_artist(name: str, mbid: str = None) -> Artist:
artist = None artist = None
logger.debug(f"Got artist {name} and mbid: {mbid}")
if "feat." in name.lower(): if "feat." in name.lower():
name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip() name = re.split("feat.", name, flags=re.IGNORECASE)[0].strip()
@ -29,13 +28,9 @@ def get_or_create_artist(name: str, mbid: str = None) -> Artist:
artist_dict = lookup_artist_from_mb(name) artist_dict = lookup_artist_from_mb(name)
mbid = mbid or artist_dict["id"] mbid = mbid or artist_dict["id"]
logger.debug(f"Looking up artist {name} and mbid: {mbid}")
artist = Artist.objects.filter(musicbrainz_id=mbid).first() artist = Artist.objects.filter(musicbrainz_id=mbid).first()
if not artist: if not artist:
artist = Artist.objects.create(name=name, musicbrainz_id=mbid) artist = Artist.objects.create(name=name, musicbrainz_id=mbid)
logger.debug(
f"Created artist {artist.name} ({artist.musicbrainz_id}) "
)
artist.fix_metadata() artist.fix_metadata()
return artist return artist
@ -48,9 +43,6 @@ def get_or_create_album(
album_dict = lookup_album_dict_from_mb(name, artist_name=artist.name) album_dict = lookup_album_dict_from_mb(name, artist_name=artist.name)
name = name or album_dict["title"] name = name or album_dict["title"]
logger.debug(f"Looking up album {name} and mbid: {mbid}")
album = Album.objects.filter(artists__in=[artist], name=name).first() album = Album.objects.filter(artists__in=[artist], name=name).first()
if not album and name: if not album and name:

View File

@ -1,6 +1,7 @@
# Generated by Django 4.1.5 on 2023-03-22 22:26 # Generated by Django 4.1.5 on 2023-03-22 22:26
from django.db import migrations, models from django.db import migrations, models
from profiles.constants import PRETTY_TIMEZONE_CHOICES
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -15,586 +16,7 @@ class Migration(migrations.Migration):
name="timezone", name="timezone",
field=models.CharField( field=models.CharField(
blank=True, blank=True,
choices=[ choices=PRETTY_TIMEZONE_CHOICES,
("Pacific/Midway", "(GMT-1100) Pacific/Midway"),
("Pacific/Niue", "(GMT-1100) Pacific/Niue"),
("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"),
("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"),
("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"),
("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"),
("US/Hawaii", "(GMT-1000) US/Hawaii"),
("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"),
("America/Adak", "(GMT-0900) America/Adak"),
("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"),
("America/Anchorage", "(GMT-0800) America/Anchorage"),
("America/Juneau", "(GMT-0800) America/Juneau"),
("America/Metlakatla", "(GMT-0800) America/Metlakatla"),
("America/Nome", "(GMT-0800) America/Nome"),
("America/Sitka", "(GMT-0800) America/Sitka"),
("America/Yakutat", "(GMT-0800) America/Yakutat"),
("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"),
("US/Alaska", "(GMT-0800) US/Alaska"),
("America/Creston", "(GMT-0700) America/Creston"),
("America/Dawson", "(GMT-0700) America/Dawson"),
(
"America/Dawson_Creek",
"(GMT-0700) America/Dawson_Creek",
),
("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"),
("America/Hermosillo", "(GMT-0700) America/Hermosillo"),
("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"),
("America/Mazatlan", "(GMT-0700) America/Mazatlan"),
("America/Phoenix", "(GMT-0700) America/Phoenix"),
("America/Tijuana", "(GMT-0700) America/Tijuana"),
("America/Vancouver", "(GMT-0700) America/Vancouver"),
("America/Whitehorse", "(GMT-0700) America/Whitehorse"),
("Canada/Pacific", "(GMT-0700) Canada/Pacific"),
("US/Arizona", "(GMT-0700) US/Arizona"),
("US/Pacific", "(GMT-0700) US/Pacific"),
(
"America/Bahia_Banderas",
"(GMT-0600) America/Bahia_Banderas",
),
("America/Belize", "(GMT-0600) America/Belize"),
("America/Boise", "(GMT-0600) America/Boise"),
(
"America/Cambridge_Bay",
"(GMT-0600) America/Cambridge_Bay",
),
("America/Chihuahua", "(GMT-0600) America/Chihuahua"),
(
"America/Ciudad_Juarez",
"(GMT-0600) America/Ciudad_Juarez",
),
("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"),
("America/Denver", "(GMT-0600) America/Denver"),
("America/Edmonton", "(GMT-0600) America/Edmonton"),
("America/El_Salvador", "(GMT-0600) America/El_Salvador"),
("America/Guatemala", "(GMT-0600) America/Guatemala"),
("America/Inuvik", "(GMT-0600) America/Inuvik"),
("America/Managua", "(GMT-0600) America/Managua"),
("America/Merida", "(GMT-0600) America/Merida"),
("America/Mexico_City", "(GMT-0600) America/Mexico_City"),
("America/Monterrey", "(GMT-0600) America/Monterrey"),
("America/Regina", "(GMT-0600) America/Regina"),
(
"America/Swift_Current",
"(GMT-0600) America/Swift_Current",
),
("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"),
("America/Yellowknife", "(GMT-0600) America/Yellowknife"),
("Canada/Mountain", "(GMT-0600) Canada/Mountain"),
("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"),
("US/Mountain", "(GMT-0600) US/Mountain"),
("America/Atikokan", "(GMT-0500) America/Atikokan"),
("America/Bogota", "(GMT-0500) America/Bogota"),
("America/Cancun", "(GMT-0500) America/Cancun"),
("America/Cayman", "(GMT-0500) America/Cayman"),
("America/Chicago", "(GMT-0500) America/Chicago"),
("America/Eirunepe", "(GMT-0500) America/Eirunepe"),
("America/Guayaquil", "(GMT-0500) America/Guayaquil"),
(
"America/Indiana/Knox",
"(GMT-0500) America/Indiana/Knox",
),
(
"America/Indiana/Tell_City",
"(GMT-0500) America/Indiana/Tell_City",
),
("America/Jamaica", "(GMT-0500) America/Jamaica"),
("America/Lima", "(GMT-0500) America/Lima"),
("America/Matamoros", "(GMT-0500) America/Matamoros"),
("America/Menominee", "(GMT-0500) America/Menominee"),
(
"America/North_Dakota/Beulah",
"(GMT-0500) America/North_Dakota/Beulah",
),
(
"America/North_Dakota/Center",
"(GMT-0500) America/North_Dakota/Center",
),
(
"America/North_Dakota/New_Salem",
"(GMT-0500) America/North_Dakota/New_Salem",
),
("America/Ojinaga", "(GMT-0500) America/Ojinaga"),
("America/Panama", "(GMT-0500) America/Panama"),
(
"America/Rankin_Inlet",
"(GMT-0500) America/Rankin_Inlet",
),
("America/Resolute", "(GMT-0500) America/Resolute"),
("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"),
("America/Winnipeg", "(GMT-0500) America/Winnipeg"),
("Canada/Central", "(GMT-0500) Canada/Central"),
("Pacific/Easter", "(GMT-0500) Pacific/Easter"),
("US/Central", "(GMT-0500) US/Central"),
("America/Anguilla", "(GMT-0400) America/Anguilla"),
("America/Antigua", "(GMT-0400) America/Antigua"),
("America/Aruba", "(GMT-0400) America/Aruba"),
("America/Barbados", "(GMT-0400) America/Barbados"),
(
"America/Blanc-Sablon",
"(GMT-0400) America/Blanc-Sablon",
),
("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"),
(
"America/Campo_Grande",
"(GMT-0400) America/Campo_Grande",
),
("America/Caracas", "(GMT-0400) America/Caracas"),
("America/Cuiaba", "(GMT-0400) America/Cuiaba"),
("America/Curacao", "(GMT-0400) America/Curacao"),
("America/Detroit", "(GMT-0400) America/Detroit"),
("America/Dominica", "(GMT-0400) America/Dominica"),
("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"),
("America/Grenada", "(GMT-0400) America/Grenada"),
("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"),
("America/Guyana", "(GMT-0400) America/Guyana"),
("America/Havana", "(GMT-0400) America/Havana"),
(
"America/Indiana/Indianapolis",
"(GMT-0400) America/Indiana/Indianapolis",
),
(
"America/Indiana/Marengo",
"(GMT-0400) America/Indiana/Marengo",
),
(
"America/Indiana/Petersburg",
"(GMT-0400) America/Indiana/Petersburg",
),
(
"America/Indiana/Vevay",
"(GMT-0400) America/Indiana/Vevay",
),
(
"America/Indiana/Vincennes",
"(GMT-0400) America/Indiana/Vincennes",
),
(
"America/Indiana/Winamac",
"(GMT-0400) America/Indiana/Winamac",
),
("America/Iqaluit", "(GMT-0400) America/Iqaluit"),
(
"America/Kentucky/Louisville",
"(GMT-0400) America/Kentucky/Louisville",
),
(
"America/Kentucky/Monticello",
"(GMT-0400) America/Kentucky/Monticello",
),
("America/Kralendijk", "(GMT-0400) America/Kralendijk"),
("America/La_Paz", "(GMT-0400) America/La_Paz"),
(
"America/Lower_Princes",
"(GMT-0400) America/Lower_Princes",
),
("America/Manaus", "(GMT-0400) America/Manaus"),
("America/Marigot", "(GMT-0400) America/Marigot"),
("America/Martinique", "(GMT-0400) America/Martinique"),
("America/Montserrat", "(GMT-0400) America/Montserrat"),
("America/Nassau", "(GMT-0400) America/Nassau"),
("America/New_York", "(GMT-0400) America/New_York"),
(
"America/Port-au-Prince",
"(GMT-0400) America/Port-au-Prince",
),
(
"America/Port_of_Spain",
"(GMT-0400) America/Port_of_Spain",
),
("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"),
("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"),
(
"America/Santo_Domingo",
"(GMT-0400) America/Santo_Domingo",
),
(
"America/St_Barthelemy",
"(GMT-0400) America/St_Barthelemy",
),
("America/St_Kitts", "(GMT-0400) America/St_Kitts"),
("America/St_Lucia", "(GMT-0400) America/St_Lucia"),
("America/St_Thomas", "(GMT-0400) America/St_Thomas"),
("America/St_Vincent", "(GMT-0400) America/St_Vincent"),
("America/Toronto", "(GMT-0400) America/Toronto"),
("America/Tortola", "(GMT-0400) America/Tortola"),
("Canada/Eastern", "(GMT-0400) Canada/Eastern"),
("US/Eastern", "(GMT-0400) US/Eastern"),
("America/Araguaina", "(GMT-0300) America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"(GMT-0300) America/Argentina/Buenos_Aires",
),
(
"America/Argentina/Catamarca",
"(GMT-0300) America/Argentina/Catamarca",
),
(
"America/Argentina/Cordoba",
"(GMT-0300) America/Argentina/Cordoba",
),
(
"America/Argentina/Jujuy",
"(GMT-0300) America/Argentina/Jujuy",
),
(
"America/Argentina/La_Rioja",
"(GMT-0300) America/Argentina/La_Rioja",
),
(
"America/Argentina/Mendoza",
"(GMT-0300) America/Argentina/Mendoza",
),
(
"America/Argentina/Rio_Gallegos",
"(GMT-0300) America/Argentina/Rio_Gallegos",
),
(
"America/Argentina/Salta",
"(GMT-0300) America/Argentina/Salta",
),
(
"America/Argentina/San_Juan",
"(GMT-0300) America/Argentina/San_Juan",
),
(
"America/Argentina/San_Luis",
"(GMT-0300) America/Argentina/San_Luis",
),
(
"America/Argentina/Tucuman",
"(GMT-0300) America/Argentina/Tucuman",
),
(
"America/Argentina/Ushuaia",
"(GMT-0300) America/Argentina/Ushuaia",
),
("America/Asuncion", "(GMT-0300) America/Asuncion"),
("America/Bahia", "(GMT-0300) America/Bahia"),
("America/Belem", "(GMT-0300) America/Belem"),
("America/Cayenne", "(GMT-0300) America/Cayenne"),
("America/Fortaleza", "(GMT-0300) America/Fortaleza"),
("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"),
("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"),
("America/Halifax", "(GMT-0300) America/Halifax"),
("America/Maceio", "(GMT-0300) America/Maceio"),
("America/Moncton", "(GMT-0300) America/Moncton"),
("America/Montevideo", "(GMT-0300) America/Montevideo"),
("America/Nuuk", "(GMT-0300) America/Nuuk"),
("America/Paramaribo", "(GMT-0300) America/Paramaribo"),
(
"America/Punta_Arenas",
"(GMT-0300) America/Punta_Arenas",
),
("America/Recife", "(GMT-0300) America/Recife"),
("America/Santarem", "(GMT-0300) America/Santarem"),
("America/Santiago", "(GMT-0300) America/Santiago"),
("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"),
("America/Thule", "(GMT-0300) America/Thule"),
("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"),
("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"),
("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"),
("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"),
("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"),
("America/St_Johns", "(GMT-0230) America/St_Johns"),
("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"),
("America/Miquelon", "(GMT-0200) America/Miquelon"),
("America/Noronha", "(GMT-0200) America/Noronha"),
(
"Atlantic/South_Georgia",
"(GMT-0200) Atlantic/South_Georgia",
),
(
"America/Scoresbysund",
"(GMT-0100) America/Scoresbysund",
),
("Atlantic/Azores", "(GMT-0100) Atlantic/Azores"),
("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"),
("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"),
("Africa/Accra", "(GMT+0000) Africa/Accra"),
("Africa/Bamako", "(GMT+0000) Africa/Bamako"),
("Africa/Banjul", "(GMT+0000) Africa/Banjul"),
("Africa/Bissau", "(GMT+0000) Africa/Bissau"),
("Africa/Casablanca", "(GMT+0000) Africa/Casablanca"),
("Africa/Conakry", "(GMT+0000) Africa/Conakry"),
("Africa/Dakar", "(GMT+0000) Africa/Dakar"),
("Africa/El_Aaiun", "(GMT+0000) Africa/El_Aaiun"),
("Africa/Freetown", "(GMT+0000) Africa/Freetown"),
("Africa/Lome", "(GMT+0000) Africa/Lome"),
("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"),
("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"),
("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"),
("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"),
(
"America/Danmarkshavn",
"(GMT+0000) America/Danmarkshavn",
),
("Antarctica/Troll", "(GMT+0000) Antarctica/Troll"),
("Atlantic/Canary", "(GMT+0000) Atlantic/Canary"),
("Atlantic/Faroe", "(GMT+0000) Atlantic/Faroe"),
("Atlantic/Madeira", "(GMT+0000) Atlantic/Madeira"),
("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"),
("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"),
("Europe/Dublin", "(GMT+0000) Europe/Dublin"),
("Europe/Guernsey", "(GMT+0000) Europe/Guernsey"),
("Europe/Isle_of_Man", "(GMT+0000) Europe/Isle_of_Man"),
("Europe/Jersey", "(GMT+0000) Europe/Jersey"),
("Europe/Lisbon", "(GMT+0000) Europe/Lisbon"),
("Europe/London", "(GMT+0000) Europe/London"),
("GMT", "(GMT+0000) GMT"),
("UTC", "(GMT+0000) UTC"),
("Africa/Algiers", "(GMT+0100) Africa/Algiers"),
("Africa/Bangui", "(GMT+0100) Africa/Bangui"),
("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"),
("Africa/Ceuta", "(GMT+0100) Africa/Ceuta"),
("Africa/Douala", "(GMT+0100) Africa/Douala"),
("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"),
("Africa/Lagos", "(GMT+0100) Africa/Lagos"),
("Africa/Libreville", "(GMT+0100) Africa/Libreville"),
("Africa/Luanda", "(GMT+0100) Africa/Luanda"),
("Africa/Malabo", "(GMT+0100) Africa/Malabo"),
("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"),
("Africa/Niamey", "(GMT+0100) Africa/Niamey"),
("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"),
("Africa/Tunis", "(GMT+0100) Africa/Tunis"),
("Arctic/Longyearbyen", "(GMT+0100) Arctic/Longyearbyen"),
("Europe/Amsterdam", "(GMT+0100) Europe/Amsterdam"),
("Europe/Andorra", "(GMT+0100) Europe/Andorra"),
("Europe/Belgrade", "(GMT+0100) Europe/Belgrade"),
("Europe/Berlin", "(GMT+0100) Europe/Berlin"),
("Europe/Bratislava", "(GMT+0100) Europe/Bratislava"),
("Europe/Brussels", "(GMT+0100) Europe/Brussels"),
("Europe/Budapest", "(GMT+0100) Europe/Budapest"),
("Europe/Busingen", "(GMT+0100) Europe/Busingen"),
("Europe/Copenhagen", "(GMT+0100) Europe/Copenhagen"),
("Europe/Gibraltar", "(GMT+0100) Europe/Gibraltar"),
("Europe/Ljubljana", "(GMT+0100) Europe/Ljubljana"),
("Europe/Luxembourg", "(GMT+0100) Europe/Luxembourg"),
("Europe/Madrid", "(GMT+0100) Europe/Madrid"),
("Europe/Malta", "(GMT+0100) Europe/Malta"),
("Europe/Monaco", "(GMT+0100) Europe/Monaco"),
("Europe/Oslo", "(GMT+0100) Europe/Oslo"),
("Europe/Paris", "(GMT+0100) Europe/Paris"),
("Europe/Podgorica", "(GMT+0100) Europe/Podgorica"),
("Europe/Prague", "(GMT+0100) Europe/Prague"),
("Europe/Rome", "(GMT+0100) Europe/Rome"),
("Europe/San_Marino", "(GMT+0100) Europe/San_Marino"),
("Europe/Sarajevo", "(GMT+0100) Europe/Sarajevo"),
("Europe/Skopje", "(GMT+0100) Europe/Skopje"),
("Europe/Stockholm", "(GMT+0100) Europe/Stockholm"),
("Europe/Tirane", "(GMT+0100) Europe/Tirane"),
("Europe/Vaduz", "(GMT+0100) Europe/Vaduz"),
("Europe/Vatican", "(GMT+0100) Europe/Vatican"),
("Europe/Vienna", "(GMT+0100) Europe/Vienna"),
("Europe/Warsaw", "(GMT+0100) Europe/Warsaw"),
("Europe/Zagreb", "(GMT+0100) Europe/Zagreb"),
("Europe/Zurich", "(GMT+0100) Europe/Zurich"),
("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"),
("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"),
("Africa/Cairo", "(GMT+0200) Africa/Cairo"),
("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"),
("Africa/Harare", "(GMT+0200) Africa/Harare"),
("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"),
("Africa/Juba", "(GMT+0200) Africa/Juba"),
("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"),
("Africa/Kigali", "(GMT+0200) Africa/Kigali"),
("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"),
("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"),
("Africa/Maputo", "(GMT+0200) Africa/Maputo"),
("Africa/Maseru", "(GMT+0200) Africa/Maseru"),
("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"),
("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"),
("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"),
("Asia/Beirut", "(GMT+0200) Asia/Beirut"),
("Asia/Famagusta", "(GMT+0200) Asia/Famagusta"),
("Asia/Gaza", "(GMT+0200) Asia/Gaza"),
("Asia/Hebron", "(GMT+0200) Asia/Hebron"),
("Asia/Jerusalem", "(GMT+0200) Asia/Jerusalem"),
("Asia/Nicosia", "(GMT+0200) Asia/Nicosia"),
("Europe/Athens", "(GMT+0200) Europe/Athens"),
("Europe/Bucharest", "(GMT+0200) Europe/Bucharest"),
("Europe/Chisinau", "(GMT+0200) Europe/Chisinau"),
("Europe/Helsinki", "(GMT+0200) Europe/Helsinki"),
("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"),
("Europe/Kyiv", "(GMT+0200) Europe/Kyiv"),
("Europe/Mariehamn", "(GMT+0200) Europe/Mariehamn"),
("Europe/Riga", "(GMT+0200) Europe/Riga"),
("Europe/Sofia", "(GMT+0200) Europe/Sofia"),
("Europe/Tallinn", "(GMT+0200) Europe/Tallinn"),
("Europe/Vilnius", "(GMT+0200) Europe/Vilnius"),
("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"),
("Africa/Asmara", "(GMT+0300) Africa/Asmara"),
(
"Africa/Dar_es_Salaam",
"(GMT+0300) Africa/Dar_es_Salaam",
),
("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"),
("Africa/Kampala", "(GMT+0300) Africa/Kampala"),
("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"),
("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"),
("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"),
("Asia/Aden", "(GMT+0300) Asia/Aden"),
("Asia/Amman", "(GMT+0300) Asia/Amman"),
("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"),
("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"),
("Asia/Damascus", "(GMT+0300) Asia/Damascus"),
("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"),
("Asia/Qatar", "(GMT+0300) Asia/Qatar"),
("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"),
("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"),
("Europe/Kirov", "(GMT+0300) Europe/Kirov"),
("Europe/Minsk", "(GMT+0300) Europe/Minsk"),
("Europe/Moscow", "(GMT+0300) Europe/Moscow"),
("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"),
("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"),
("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"),
("Indian/Comoro", "(GMT+0300) Indian/Comoro"),
("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"),
("Asia/Tehran", "(GMT+0330) Asia/Tehran"),
("Asia/Baku", "(GMT+0400) Asia/Baku"),
("Asia/Dubai", "(GMT+0400) Asia/Dubai"),
("Asia/Muscat", "(GMT+0400) Asia/Muscat"),
("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"),
("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"),
("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"),
("Europe/Samara", "(GMT+0400) Europe/Samara"),
("Europe/Saratov", "(GMT+0400) Europe/Saratov"),
("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"),
("Indian/Mahe", "(GMT+0400) Indian/Mahe"),
("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"),
("Indian/Reunion", "(GMT+0400) Indian/Reunion"),
("Asia/Kabul", "(GMT+0430) Asia/Kabul"),
("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"),
("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"),
("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"),
("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"),
("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"),
("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"),
("Asia/Karachi", "(GMT+0500) Asia/Karachi"),
("Asia/Oral", "(GMT+0500) Asia/Oral"),
("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"),
("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"),
("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"),
("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"),
("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"),
("Indian/Maldives", "(GMT+0500) Indian/Maldives"),
("Asia/Colombo", "(GMT+0530) Asia/Colombo"),
("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"),
("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"),
("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"),
("Asia/Almaty", "(GMT+0600) Asia/Almaty"),
("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"),
("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"),
("Asia/Omsk", "(GMT+0600) Asia/Omsk"),
("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"),
("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"),
("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"),
("Indian/Chagos", "(GMT+0600) Indian/Chagos"),
("Asia/Yangon", "(GMT+0630) Asia/Yangon"),
("Indian/Cocos", "(GMT+0630) Indian/Cocos"),
("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"),
("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"),
("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"),
("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"),
("Asia/Hovd", "(GMT+0700) Asia/Hovd"),
("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"),
("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"),
("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"),
("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"),
("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"),
("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"),
("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"),
("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"),
("Indian/Christmas", "(GMT+0700) Indian/Christmas"),
("Asia/Brunei", "(GMT+0800) Asia/Brunei"),
("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"),
("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"),
("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"),
("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"),
("Asia/Kuching", "(GMT+0800) Asia/Kuching"),
("Asia/Macau", "(GMT+0800) Asia/Macau"),
("Asia/Makassar", "(GMT+0800) Asia/Makassar"),
("Asia/Manila", "(GMT+0800) Asia/Manila"),
("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"),
("Asia/Singapore", "(GMT+0800) Asia/Singapore"),
("Asia/Taipei", "(GMT+0800) Asia/Taipei"),
("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"),
("Australia/Perth", "(GMT+0800) Australia/Perth"),
("Australia/Eucla", "(GMT+0845) Australia/Eucla"),
("Asia/Chita", "(GMT+0900) Asia/Chita"),
("Asia/Dili", "(GMT+0900) Asia/Dili"),
("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"),
("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"),
("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"),
("Asia/Seoul", "(GMT+0900) Asia/Seoul"),
("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"),
("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"),
("Pacific/Palau", "(GMT+0900) Pacific/Palau"),
("Australia/Darwin", "(GMT+0930) Australia/Darwin"),
(
"Antarctica/DumontDUrville",
"(GMT+1000) Antarctica/DumontDUrville",
),
("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"),
("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"),
("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"),
("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"),
("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"),
("Pacific/Guam", "(GMT+1000) Pacific/Guam"),
(
"Pacific/Port_Moresby",
"(GMT+1000) Pacific/Port_Moresby",
),
("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"),
("Australia/Adelaide", "(GMT+1030) Australia/Adelaide"),
(
"Australia/Broken_Hill",
"(GMT+1030) Australia/Broken_Hill",
),
("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"),
(
"Antarctica/Macquarie",
"(GMT+1100) Antarctica/Macquarie",
),
("Asia/Magadan", "(GMT+1100) Asia/Magadan"),
("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"),
("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"),
("Australia/Hobart", "(GMT+1100) Australia/Hobart"),
("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"),
("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"),
("Australia/Sydney", "(GMT+1100) Australia/Sydney"),
(
"Pacific/Bougainville",
"(GMT+1100) Pacific/Bougainville",
),
("Pacific/Efate", "(GMT+1100) Pacific/Efate"),
("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"),
("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"),
("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"),
("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"),
("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"),
("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"),
("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"),
("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"),
("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"),
("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"),
("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"),
("Pacific/Norfolk", "(GMT+1200) Pacific/Norfolk"),
("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"),
("Pacific/Wake", "(GMT+1200) Pacific/Wake"),
("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"),
("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"),
("Pacific/Apia", "(GMT+1300) Pacific/Apia"),
("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"),
("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"),
("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"),
("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"),
("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"),
("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"),
],
max_length=255, max_length=255,
null=True, null=True,
), ),

View File

@ -0,0 +1,22 @@
# Generated by Django 4.1.7 on 2023-04-03 02:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("scrobbles", "0034_alter_chartrecord_rank"),
]
operations = [
migrations.AddField(
model_name="scrobble",
name="videogame_save_data",
field=models.FileField(
blank=True,
null=True,
upload_to="scrobbles/videogame_save_data/",
),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-04-03 03:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("scrobbles", "0035_scrobble_videogame_save_data"),
]
operations = [
migrations.AddField(
model_name="scrobble",
name="stop_timestamp",
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -24,7 +24,10 @@ from profiles.utils import (
) )
from scrobbles.constants import LONG_PLAY_MEDIA from scrobbles.constants import LONG_PLAY_MEDIA
from scrobbles.stats import build_charts from scrobbles.stats import build_charts
from scrobbles.utils import check_scrobble_for_finish from scrobbles.utils import (
check_long_play_for_finish,
check_scrobble_for_finish,
)
from sports.models import SportEvent from sports.models import SportEvent
from videogames.models import VideoGame from videogames.models import VideoGame
from videos.models import Series, Video from videos.models import Series, Video
@ -427,6 +430,7 @@ class Scrobble(TimeStampedModel):
# Time keeping # Time keeping
timestamp = models.DateTimeField(**BNULL) timestamp = models.DateTimeField(**BNULL)
stop_timestamp = models.DateTimeField(**BNULL)
playback_position_ticks = models.PositiveBigIntegerField(**BNULL) playback_position_ticks = models.PositiveBigIntegerField(**BNULL)
playback_position_seconds = models.IntegerField(**BNULL) playback_position_seconds = models.IntegerField(**BNULL)
@ -443,6 +447,9 @@ class Scrobble(TimeStampedModel):
# Fields for keeping track long content like books and games # Fields for keeping track long content like books and games
book_pages_read = models.IntegerField(**BNULL) book_pages_read = models.IntegerField(**BNULL)
videogame_save_data = models.FileField(
upload_to="scrobbles/videogame_save_data/", **BNULL
)
long_play_seconds = models.BigIntegerField(**BNULL) long_play_seconds = models.BigIntegerField(**BNULL)
long_play_complete = models.BooleanField(**BNULL) long_play_complete = models.BooleanField(**BNULL)
@ -538,7 +545,7 @@ class Scrobble(TimeStampedModel):
if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA.values(): if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA.values():
logger.info(f"No - Long play media") logger.info(f"No - Long play media")
updatable = False updatable = False
if self.percent_played > 100: if self.percent_played >= 100:
logger.info(f"No - 100% played - {self.id} - {self.source}") logger.info(f"No - 100% played - {self.id} - {self.source}")
updatable = False updatable = False
if self.is_stale: if self.is_stale:
@ -574,8 +581,8 @@ class Scrobble(TimeStampedModel):
media_class = media.__class__.__name__ media_class = media.__class__.__name__
media_query = models.Q(track=media)
if media_class == "Track": if media_class == "Track":
media_query = models.Q(track=media)
scrobble_data["track_id"] = media.id scrobble_data["track_id"] = media.id
if media_class == "Video": if media_class == "Video":
media_query = models.Q(video=media) media_query = models.Q(video=media)
@ -601,21 +608,26 @@ class Scrobble(TimeStampedModel):
.order_by("-modified") .order_by("-modified")
.first() .first()
) )
if scrobble and scrobble.can_be_updated: # We want to finish a video after it's reeached 90%
# but we also don't want to create new videos, so in
# this special case, we allow jellyfin resumed status
# to allow a scrobble to be updated.
jellyfin_in_progress = scrobble_data.get("jellyfin_status", None)
if scrobble and (scrobble.can_be_updated or jellyfin_in_progress):
logger.info( logger.info(
f"Updating {scrobble.id}", f"Updating {scrobble.id}",
{"scrobble_data": scrobble_data, "media": media}, {"scrobble_data": scrobble_data, "media": media},
) )
return scrobble.update(scrobble_data) return scrobble.update(scrobble_data)
# Discard status before creating
scrobble_data.pop("mopidy_status", None)
scrobble_data.pop("jellyfin_status", None)
source = scrobble_data["source"] source = scrobble_data["source"]
logger.info( logger.info(
f"Creating for {media.id} - {source}", f"Creating for {media.id} - {source}",
{"scrobble_data": scrobble_data, "media": media}, {"scrobble_data": scrobble_data, "media": media},
) )
# If creating a new scrobble, we don't need status
scrobble_data.pop("mopidy_status", None)
scrobble_data.pop("jellyfin_status", None)
return cls.create(scrobble_data) return cls.create(scrobble_data)
def update(self, scrobble_data: dict) -> "Scrobble": def update(self, scrobble_data: dict) -> "Scrobble":
@ -637,9 +649,19 @@ class Scrobble(TimeStampedModel):
if scrobble_status == "resumed": if scrobble_status == "resumed":
self.resume() self.resume()
check_scrobble_for_finish(self)
if scrobble_status != "resumed":
scrobble_data["stop_timestamp"] = (
scrobble_data.pop("timestamp", None) or timezone.now()
)
update_fields = []
for key, value in scrobble_data.items(): for key, value in scrobble_data.items():
setattr(self, key, value) setattr(self, key, value)
self.save() update_fields.append(key)
self.save(update_fields=update_fields)
return self return self
@classmethod @classmethod
@ -654,54 +676,22 @@ class Scrobble(TimeStampedModel):
return scrobble return scrobble
def stop(self, force_finish=False) -> None: def stop(self, force_finish=False) -> None:
if not self.in_progress:
return
self.in_progress = False self.in_progress = False
self.save(update_fields=["in_progress"]) self.save(update_fields=["in_progress"])
logger.info(f"{self.id} - {self.source}") logger.info(f"stopping {self.id} from {self.source}")
class_name = self.media_obj.__class__.__name__ class_name = self.media_obj.__class__.__name__
if class_name in LONG_PLAY_MEDIA.values(): if class_name in LONG_PLAY_MEDIA.values():
now = timezone.now() check_long_play_for_finish(self)
self.played_to_completion = True
self.playback_position_seconds = (now - self.timestamp).seconds
media_filter = models.Q(video_game=self.video_game)
if class_name == "Book":
media_filter = models.Q(book=self.book)
# Check for last scrobble, and if present, update long play time
last_scrobble = Scrobble.objects.filter(
media_filter,
user_id=self.user,
played_to_completion=True,
long_play_complete=False,
).last()
self.long_play_seconds = self.playback_position_seconds
if last_scrobble:
self.long_play_seconds = (
last_scrobble.long_play_seconds
+ self.playback_position_seconds
)
self.save(
update_fields=[
"playback_position_seconds",
"played_to_completion",
"long_play_seconds",
]
)
return return
check_scrobble_for_finish(self, force_finish=force_finish)
def pause(self) -> None: def pause(self) -> None:
if self.is_paused: if self.is_paused:
logger.warning(f"{self.id} - already paused - {self.source}") logger.warning(f"{self.id} - already paused - {self.source}")
return return
logger.info(f"{self.id} - pausing - {self.source}")
self.is_paused = True self.is_paused = True
self.save(update_fields=["is_paused"]) self.save(update_fields=["is_paused"])
logger.info(f"{self.id} - pausing - {self.source}")
check_scrobble_for_finish(self)
def resume(self) -> None: def resume(self) -> None:
if self.is_paused or not self.in_progress: if self.is_paused or not self.in_progress:

View File

@ -1,6 +1,7 @@
import logging import logging
from urllib.parse import unquote from urllib.parse import unquote
from django.utils import timezone
from dateutil.parser import ParserError, parse from dateutil.parser import ParserError, parse
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
@ -78,19 +79,11 @@ def check_scrobble_for_finish(
scrobble: "Scrobble", force_to_100=False, force_finish=False scrobble: "Scrobble", force_to_100=False, force_finish=False
) -> None: ) -> None:
completion_percent = scrobble.media_obj.COMPLETION_PERCENT completion_percent = scrobble.media_obj.COMPLETION_PERCENT
if scrobble.percent_played >= completion_percent or force_finish: if scrobble.percent_played >= completion_percent or force_finish:
if ( logger.info(f"{scrobble.id} finished at {scrobble.percent_played}")
scrobble.playback_position_seconds scrobble.playback_position_seconds = (
and scrobble.media_obj.run_time_seconds scrobble.media_obj.run_time_seconds
): )
logger.info(f"{scrobble.id} {completion_percent} met, finishing")
scrobble.playback_position_seconds = (
scrobble.media_obj.run_time_seconds
)
logger.info(
f"{scrobble.playback_position_seconds} set to {scrobble.media_obj.run_time_seconds}"
)
scrobble.in_progress = False scrobble.in_progress = False
scrobble.is_paused = False scrobble.is_paused = False
@ -104,6 +97,44 @@ def check_scrobble_for_finish(
"playback_position_seconds", "playback_position_seconds",
] ]
) )
else:
logger.info(
f"{scrobble.id} not complete at {scrobble.percent_played}%"
)
def check_long_play_for_finish(scrobble):
Scrobble = apps.get_model("scrobbles", "Scrobble")
class_name = scrobble.media_obj.__class__.__name__
now = timezone.now()
scrobble.played_to_completion = True
scrobble.playback_position_seconds = (now - scrobble.timestamp).seconds
media_filter = models.Q(video_game=scrobble.video_game)
if class_name == "Book":
media_filter = models.Q(book=scrobble.book)
# Check for last scrobble, and if present, update long play time
last_scrobble = Scrobble.objects.filter(
media_filter,
user_id=scrobble.user,
played_to_completion=True,
long_play_complete=False,
).last()
scrobble.long_play_seconds = scrobble.playback_position_seconds
if last_scrobble:
scrobble.long_play_seconds = (
last_scrobble.long_play_seconds
+ scrobble.playback_position_seconds
)
scrobble.save(
update_fields=[
"playback_position_seconds",
"played_to_completion",
"long_play_seconds",
]
)
def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet: def get_scrobbles_for_media(media_obj, user: User) -> models.QuerySet:

View File

@ -235,7 +235,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "EST") TIME_ZONE = os.getenv("VROBBLER_TIME_ZONE", "America/New_York")
USE_I18N = True USE_I18N = True

View File

@ -24,7 +24,7 @@
<dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt> <dt><a href="{{media.get_absolute_url}}">{{media.title}}</a></dt>
{% if media.hltb_cover %} {% if media.hltb_cover %}
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.hltb_cover.url}}" width=200 height=200 /></a></dd> <dd><a href="{{media.get_absolute_url}}"><img src="{{media.hltb_cover.url}}" width=200 height=200 /></a></dd>
{% else %} {% elif media.cover %}
<dd><a href="{{media.get_absolute_url}}"><img src="{{media.cover.url}}" style="width: 200px; height: 200px; object-fit:cover; " /></a></dd> <dd><a href="{{media.get_absolute_url}}"><img src="{{media.cover.url}}" style="width: 200px; height: 200px; object-fit:cover; " /></a></dd>
{% endif %} {% endif %}
<dd> <dd>