Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audiobook play time #2551

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bfa64a9
Add audiobook_play_time field in edition model
joachimesque Dec 26, 2022
bee0f31
Add audiobook_play_time field in form
joachimesque Dec 26, 2022
2cfbfe2
Display format in book page
joachimesque Dec 26, 2022
0dbfff3
Let the DOM dance
joachimesque Dec 26, 2022
647b3c6
Black
joachimesque Dec 26, 2022
9f7d520
Fix lint
joachimesque Dec 26, 2022
54a7931
Fix test
joachimesque Dec 26, 2022
d10abf4
Move duration localization to a filter
joachimesque Dec 26, 2022
9925df1
Improve widget attributes readability
joachimesque Dec 26, 2022
92c07f4
Display right field on load
joachimesque Dec 26, 2022
c0eb7d4
Fix lint error
joachimesque Dec 26, 2022
4cfaba0
Add template tag test
joachimesque Dec 26, 2022
791b65c
Use DurationField with custom widget and clean function
joachimesque Dec 27, 2022
7e37086
Handle no-js option
joachimesque Dec 27, 2022
b550693
Move format logic to dedicated snippet
joachimesque Dec 27, 2022
bf32945
Add schema.org Book object duration
joachimesque Dec 27, 2022
6fa56dc
Change activitypub audiobookPlayTime type
joachimesque Dec 27, 2022
b331271
Fix (some) tests, by allowing null/0/empty values
joachimesque Dec 27, 2022
88ade64
Fix JS formating
joachimesque Dec 27, 2022
024aeb9
Merge branch 'main' into audiobook-play-time
joachimesque Jan 6, 2023
9c8f484
Merge branch 'main' into audiobook-play-time
jaschaurbach May 19, 2023
65448ce
Merge branch 'main' into audiobook-play-time
jaschaurbach May 31, 2023
6008f1f
Merge branch 'main' into audiobook-play-time
joachimesque Aug 1, 2023
bcedca5
Always store and display an absolute value of the duration
joachimesque Aug 1, 2023
6c0a329
Fix migrations conflict
joachimesque Aug 1, 2023
e2b6efd
Merge branch 'main' into audiobook-play-time
mouse-reeve Aug 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions bookwyrm/activitypub/book.py
Expand Up @@ -59,6 +59,7 @@ class Edition(Book):
isbn13: str = ""
oclcNumber: str = ""
pages: int = None
audiobookPlayTime: str = ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of encoding audiobookPlayTime as an integer (seconds or minutes), or even better a time interval and interpreting it accordingly on the frontend? As a general principle, I find it's best if the data stored in the database is as unambiguous and as close to "ground truth" as possible, with any interpretation being done clientside.

More briefly: I find it leads to less headaches if you are permissive on input, and strict on what you store.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One big question is: how will it federate? My transform (* 60 to the value at clean() time) would multiply an already correct value, coming from another server, and so on. Do you know how I can prevent that? Is it by making an activitypub-aware field with field_to_activity and field_from_activity transforms? That's a part I really don't know in the codebase.

physicalFormat: str = ""
physicalFormatDetail: str = ""
publishers: List[str] = field(default_factory=lambda: [])
Expand Down
17 changes: 16 additions & 1 deletion bookwyrm/forms/books.py
@@ -1,4 +1,6 @@
""" using django model forms """
import json

from django import forms

from bookwyrm import models
Expand Down Expand Up @@ -33,6 +35,7 @@ class Meta:
"physical_format",
"physical_format_detail",
"pages",
"audiobook_play_time",
"isbn_13",
"isbn_10",
"openlibrary_key",
Expand Down Expand Up @@ -70,12 +73,24 @@ class Meta:
attrs={"aria-describedby": "desc_cover"}
),
"physical_format": Select(
attrs={"aria-describedby": "desc_physical_format"}
attrs={
"aria-describedby": "desc_physical_format",
"data-toggle-on-select": "true",
"data-toggle-strategy": json.dumps(
{
"default": "toggle-target-pages",
"AudiobookFormat": "toggle-target-audiobook-play-time",
}
),
}
),
"physical_format_detail": forms.TextInput(
attrs={"aria-describedby": "desc_physical_format_detail"}
),
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
"audiobook_play_time": forms.TextInput(
attrs={"aria-describedby": "desc_audiobook_play_time"},
),
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
"openlibrary_key": forms.TextInput(
Expand Down
21 changes: 21 additions & 0 deletions bookwyrm/migrations/0173_edition_audiobook_play_time.py
@@ -0,0 +1,21 @@
# Generated by Django 3.2.16 on 2022-12-26 18:13

import bookwyrm.models.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("bookwyrm", "0172_alter_user_preferred_language"),
]

operations = [
migrations.AddField(
model_name="edition",
name="audiobook_play_time",
field=bookwyrm.models.fields.PermissivePlayTime(
blank=True, max_length=255, null=True
),
),
]
5 changes: 5 additions & 0 deletions bookwyrm/models/book.py
Expand Up @@ -295,6 +295,11 @@ class Edition(Book):
max_length=255, choices=FormatChoices, null=True, blank=True
)
physical_format_detail = fields.CharField(max_length=255, blank=True, null=True)
audiobook_play_time = fields.PermissivePlayTime(
max_length=255,
blank=True,
null=True,
)
mouse-reeve marked this conversation as resolved.
Show resolved Hide resolved
publishers = fields.ArrayField(
models.CharField(max_length=255), blank=True, default=list
)
Expand Down
36 changes: 36 additions & 0 deletions bookwyrm/models/fields.py
@@ -1,6 +1,7 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
import re
import math
from uuid import uuid4
from urllib.parse import urljoin

Expand Down Expand Up @@ -538,3 +539,38 @@ def field_to_activity(self, value):
if not value:
return None
return float(value)


# pylint: disable=bare-except
class PermissivePlayTime(CharField):
"""The Play Time field can accept total runtime in `minutes`
or in `hours:minutes`, but stores it as `hours:minutes`"""

def to_python(self, value):
"""Normalizes both kinds of inputs to formatted string"""
original_value = value
if value in (None, ""):
return value

try:
# check if value is in minutes only
value = int(value)
value = f"{str(math.floor(value / 60)).zfill(2)}:{str(value % 60).zfill(2)}"
except:
try:
# Check that the values can be converted to int
value = [int(i) for i in value.split(":")]
assert value[0] >= 0
assert 0 <= value[1] <= 60
# Then transform the values to str for storage
value = ":".join([str(i).zfill(2) for i in value])
except:
raise ValidationError(
_(
"%(value)s is not a valid duration in"
" minutes or in the format hour:minutes"
),
params={"value": original_value},
)

return value
joachimesque marked this conversation as resolved.
Show resolved Hide resolved
34 changes: 33 additions & 1 deletion bookwyrm/static/js/forms.js
Expand Up @@ -2,7 +2,7 @@
"use strict";

/**
* Remoev input field
* Remove input field
*
* @param {event} the button click event
*/
Expand Down Expand Up @@ -58,4 +58,36 @@
}
}
});

/**
* Toggle between two fields depending on select value
*
* @param {event} the change event on the associated select
*/
function toggleOnSelect(event) {
const value = event.target.value;

const toggleStrategy = JSON.parse(event.target.dataset.toggleStrategy);
let selectedTarget = "";

if (value in toggleStrategy) {
selectedTarget = toggleStrategy[value];
} else {
selectedTarget = toggleStrategy.default;
}

Object.values(toggleStrategy).forEach((toggleItem) => {
const toggleElement = document.getElementById(toggleItem);

if (toggleItem === selectedTarget) {
toggleElement.classList.remove("is-hidden");
} else {
toggleElement.classList.add("is-hidden");
}
});
}

document
.querySelectorAll("[data-toggle-on-select]")
.forEach((node) => node.addEventListener("change", toggleOnSelect));
})();
14 changes: 13 additions & 1 deletion bookwyrm/templates/book/edit/edit_book_form.html
Expand Up @@ -275,14 +275,26 @@ <h2 class="title is-4">
</div>
</div>

<div class="field">
<div class="field{% if form.physical_format.value == 'AudiobookFormat' %} is-hidden{% endif %}" id="toggle-target-pages">
<label class="label" for="id_pages">
{% trans "Pages:" %}
</label>
{{ form.pages }}

{% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}
</div>

<div class="field{% if form.physical_format.value != 'AudiobookFormat' %} is-hidden{% endif %}" id="toggle-target-audiobook-play-time">
<label class="label" for="id_audiobook_play_time">
{% trans "Audiobook play time:" %}
</label>
{{ form.audiobook_play_time }}
<p class="help">
{% trans "Can be filled as hours:minutes or as the total run in minutes" %}
</p>

{% include 'snippets/form_errors.html' with errors_list=form.audiobook_play_time.errors id="desc_audiobook_play_time" %}
</div>
joachimesque marked this conversation as resolved.
Show resolved Hide resolved
</div>
</section>

Expand Down
9 changes: 8 additions & 1 deletion bookwyrm/templates/book/publisher_info.html
Expand Up @@ -2,6 +2,7 @@

{% load i18n %}
{% load humanize %}
{% load book_display_tags %}

{% firstof book.physical_format_detail book.get_physical_format_display as format %}
{% firstof book.physical_format book.physical_format_detail as format_property %}
Expand All @@ -18,7 +19,13 @@

<p>
{% if format and not pages %}
{{ format }}
{% if format_property and format_property == "AudiobookFormat" and book.audiobook_play_time %}
{{ format }}<br />
{% trans "Play time:" %}
{{ book.audiobook_play_time|localized_duration }}
{% else %}
{{ format }}
{% endif %}
joachimesque marked this conversation as resolved.
Show resolved Hide resolved
{% elif format and pages %}
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
{% elif pages %}
Expand Down
22 changes: 22 additions & 0 deletions bookwyrm/templatetags/book_display_tags.py
@@ -1,5 +1,9 @@
""" template filters """
import datetime

from django import template
import humanize

from bookwyrm import models


Expand Down Expand Up @@ -33,3 +37,21 @@ def get_book_file_links(book):
def get_author_edition(book, author):
"""default edition for a book on the author page"""
return book.author_edition(author)


# pylint: disable=bare-except
@register.filter(name="localized_duration")
def get_localized_duration(duration):
"""Returns a localized version of the play time"""

try:
values = [int(i) for i in duration.split(":")]
assert len(values) == 2
delta = (values[0] * 60) + values[1]
except:
return duration

duration = datetime.timedelta(minutes=delta)
duration = humanize.precisedelta(duration)
joachimesque marked this conversation as resolved.
Show resolved Hide resolved

return duration
7 changes: 7 additions & 0 deletions bookwyrm/tests/templatetags/test_book_display_tags.py
Expand Up @@ -60,3 +60,10 @@ def test_get_book_file_links(self, *_):
links = book_display_tags.get_book_file_links(self.book)
self.assertTrue(links.exists())
self.assertEqual(links[0], link)

def test_get_localized_duration(self, *_):
"""just a little test of humanization"""

result = book_display_tags.get_localized_duration("12:34")

self.assertEqual(result, "12 hours and 34 minutes")
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -10,6 +10,7 @@ django-model-utils==4.2.0
django-sass-processor==1.2.2
environs==9.5.0
flower==1.2.0
humanize==4.4.0
libsass==0.22.0
Markdown==3.3.3
Pillow>=9.3.0
Expand Down