-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Time tracking models and model tests * Time tracking route with api models added The api takes bulk playtime entries to insert into the DB * Playtime summation script and tests The cron job is slated to run every 12 hours, shifted by 8 to avoid clutter * Playtime reporting script added with a configurable email recipient To run once a month and send a quarterly report * Changed API route * Added time tracking links to feed entries * Mypy fixes * Python 3.8 syntax fix * Alembic ordering fix * Added collection and library information to the playtime entries and summaries * Reporting summation groups on collection and library as well now * Playtimes API validations * Modularized playtime entries * Fixed UTC date issue * PR updates * Added the 401 gone status for very old entries * Playtime entries reaping cut off time * Time tracking rels only for specific collections * Mypyp fixes * Fixed 401 Gone to 410 Gone * Only a loans feed with active loans for a work will have time tracking rels * 410 is now counted as a failure * Fixed time tracking links * Mypy fix * API spec for the route * Switched from collection.name to colletion.id for the playtime route * Add missing API fields. --------- Co-authored-by: Tim DiLauro <tdilauro@users.noreply.github.com>
- Loading branch information
1 parent
9b434fb
commit b88847e
Showing
22 changed files
with
1,627 additions
and
3 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
alembic/versions/20230728_892c8e0c89f8_audiobook_playtime_tracking.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
"""Audiobook playtime tracking | ||
Revision ID: 892c8e0c89f8 | ||
Revises: b3749bac3e55 | ||
Create Date: 2023-07-28 07:20:24.625484+00:00 | ||
""" | ||
import sqlalchemy as sa | ||
|
||
from alembic import op | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "892c8e0c89f8" | ||
down_revision = "b3749bac3e55" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.create_table( | ||
"playtime_entries", | ||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), | ||
sa.Column("identifier_id", sa.Integer(), nullable=False), | ||
sa.Column("collection_id", sa.Integer(), nullable=False), | ||
sa.Column("library_id", sa.Integer(), nullable=False), | ||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), | ||
sa.Column("total_seconds_played", sa.Integer(), nullable=False), | ||
sa.Column("tracking_id", sa.String(length=64), nullable=False), | ||
sa.Column("processed", sa.Boolean(), nullable=True), | ||
sa.ForeignKeyConstraint( | ||
["collection_id"], | ||
["collections.id"], | ||
onupdate="CASCADE", | ||
ondelete="CASCADE", | ||
), | ||
sa.ForeignKeyConstraint( | ||
["identifier_id"], | ||
["identifiers.id"], | ||
onupdate="CASCADE", | ||
ondelete="CASCADE", | ||
), | ||
sa.ForeignKeyConstraint( | ||
["library_id"], ["libraries.id"], onupdate="CASCADE", ondelete="CASCADE" | ||
), | ||
sa.PrimaryKeyConstraint("id"), | ||
sa.UniqueConstraint( | ||
"identifier_id", "collection_id", "library_id", "tracking_id" | ||
), | ||
) | ||
op.create_table( | ||
"playtime_summaries", | ||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), | ||
sa.Column("identifier_id", sa.Integer(), nullable=True), | ||
sa.Column("collection_id", sa.Integer(), nullable=False), | ||
sa.Column("library_id", sa.Integer(), nullable=False), | ||
sa.Column("identifier_str", sa.String(), nullable=False), | ||
sa.Column("collection_name", sa.String(), nullable=False), | ||
sa.Column("library_name", sa.String(), nullable=False), | ||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), | ||
sa.Column("total_seconds_played", sa.Integer(), nullable=True), | ||
sa.ForeignKeyConstraint( | ||
["collection_id"], | ||
["collections.id"], | ||
onupdate="CASCADE", | ||
ondelete="SET NULL", | ||
), | ||
sa.ForeignKeyConstraint( | ||
["identifier_id"], | ||
["identifiers.id"], | ||
onupdate="CASCADE", | ||
ondelete="SET NULL", | ||
), | ||
sa.ForeignKeyConstraint( | ||
["library_id"], ["libraries.id"], onupdate="CASCADE", ondelete="SET NULL" | ||
), | ||
sa.PrimaryKeyConstraint("id"), | ||
sa.UniqueConstraint( | ||
"identifier_str", "collection_name", "library_name", "timestamp" | ||
), | ||
) | ||
op.create_index( | ||
op.f("ix_playtime_summaries_collection_id"), | ||
"playtime_summaries", | ||
["collection_id"], | ||
unique=False, | ||
) | ||
op.create_index( | ||
op.f("ix_playtime_summaries_identifier_id"), | ||
"playtime_summaries", | ||
["identifier_id"], | ||
unique=False, | ||
) | ||
op.create_index( | ||
op.f("ix_playtime_summaries_library_id"), | ||
"playtime_summaries", | ||
["library_id"], | ||
unique=False, | ||
) | ||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.drop_index( | ||
op.f("ix_playtime_summaries_library_id"), table_name="playtime_summaries" | ||
) | ||
op.drop_index( | ||
op.f("ix_playtime_summaries_identifier_id"), table_name="playtime_summaries" | ||
) | ||
op.drop_index( | ||
op.f("ix_playtime_summaries_collection_id"), table_name="playtime_summaries" | ||
) | ||
op.drop_table("playtime_summaries") | ||
op.drop_table("playtime_entries") | ||
# ### end Alembic commands ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import datetime | ||
import logging | ||
from typing import Any, Dict, List, Optional | ||
|
||
from pydantic import Field, validator | ||
|
||
from core.util.flask_util import CustomBaseModel | ||
|
||
|
||
class PlaytimeTimeEntry(CustomBaseModel): | ||
id: str = Field(description="An id to ensure uniqueness of the time entry") | ||
during_minute: datetime.datetime = Field( | ||
description="A minute boundary datetime of the format yyyy-mm-ddThh:mmZ" | ||
) | ||
seconds_played: int = Field( | ||
description="How many seconds were played within this minute" | ||
) | ||
|
||
@validator("during_minute") | ||
def validate_minute_datetime(cls, value: datetime.datetime): | ||
"""Coerce the datetime to a minute boundary""" | ||
if value.tzname() != "UTC": | ||
logging.getLogger("TimeTracking").error( | ||
f"An incorrect timezone was received for a playtime ({value.tzname()})." | ||
) | ||
raise ValueError("Timezone MUST be UTC always") | ||
value = value.replace(second=0, microsecond=0) | ||
return value | ||
|
||
@validator("seconds_played") | ||
def validate_seconds_played(cls, value: int): | ||
"""Coerce the seconds played to a max of 60 seconds""" | ||
if value > 60: | ||
logging.getLogger("TimeTracking").warning( | ||
"Greater than 60 seconds was received for a minute playtime." | ||
) | ||
value = 60 | ||
elif value < 0: | ||
logging.getLogger("TimeTracking").warning( | ||
"Less than 0 seconds was received for a minute playtime." | ||
) | ||
value = 0 | ||
return value | ||
|
||
|
||
class PlaytimeEntriesPost(CustomBaseModel): | ||
book_id: Optional[str] = Field( | ||
description="An identifier of a book (currently ignored)." | ||
) | ||
library_id: Optional[str] = Field( | ||
description="And identifier for the library (currently ignored)." | ||
) | ||
time_entries: List[PlaytimeTimeEntry] = Field(description="A List of time entries") | ||
|
||
|
||
class PlaytimeEntriesPostSummary(CustomBaseModel): | ||
total: int = 0 | ||
successes: int = 0 | ||
failures: int = 0 | ||
|
||
|
||
class PlaytimeEntriesPostResponse(CustomBaseModel): | ||
responses: List[Dict[str, Any]] = Field( | ||
description="Responses as part of the multi-reponse" | ||
) | ||
summary: PlaytimeEntriesPostSummary = Field( | ||
description="Summary of failures and successes" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#!/usr/bin/env python | ||
"""Sum the playtimes for audiobooks.""" | ||
import os | ||
import sys | ||
|
||
bin_dir = os.path.split(__file__)[0] | ||
package_dir = os.path.join(bin_dir, "..") | ||
sys.path.append(os.path.abspath(package_dir)) | ||
|
||
from core.jobs.playtime_entries import PlaytimeEntriesEmailReportsScript | ||
from core.model import production_session | ||
|
||
PlaytimeEntriesEmailReportsScript(production_session(initialize_data=False)).run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#!/usr/bin/env python | ||
"""Sum the playtimes for audiobooks.""" | ||
import os | ||
import sys | ||
|
||
bin_dir = os.path.split(__file__)[0] | ||
package_dir = os.path.join(bin_dir, "..") | ||
sys.path.append(os.path.abspath(package_dir)) | ||
|
||
from core.jobs.playtime_entries import PlaytimeEntriesSummationScript | ||
from core.model import production_session | ||
|
||
PlaytimeEntriesSummationScript(production_session(initialize_data=False)).run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.