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 all 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: Optional[int] = None
audiobookPlayTime: Optional[int] = None
physicalFormat: str = ""
physicalFormatDetail: str = ""
publishers: list[str] = field(default_factory=list)
Expand Down
30 changes: 28 additions & 2 deletions bookwyrm/forms/books.py
@@ -1,10 +1,12 @@
""" using django model forms """
import json

from django import forms

from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
from .widgets import ArrayWidget, SelectDateWidget, Select
from .widgets import ArrayWidget, SelectDateWidget, Select, MinutesDurationWidget


# pylint: disable=missing-class-docstring
Expand Down Expand Up @@ -34,6 +36,7 @@ class Meta:
"physical_format",
"physical_format_detail",
"pages",
"audiobook_play_time",
"isbn_13",
"isbn_10",
"openlibrary_key",
Expand Down Expand Up @@ -74,12 +77,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": MinutesDurationWidget(
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 All @@ -99,6 +114,17 @@ class Meta:
"isfdb": forms.TextInput(attrs={"aria-describedby": "desc_isfdb"}),
}

def clean_audiobook_play_time(self):
"""Converts a raw input in seconds to minutes"""
data = self.cleaned_data["audiobook_play_time"]

if data in [0, None, ""]:
return data

data = abs(data)

return data * 60


class EditionFromWorkForm(CustomForm):
def __init__(self, *args, **kwargs):
Expand Down
15 changes: 15 additions & 0 deletions bookwyrm/forms/widgets.py
Expand Up @@ -12,6 +12,21 @@ def value_from_datadict(self, data, files, name):
return [i for i in data.getlist(name) if i]


class MinutesDurationWidget(forms.TextInput):
"""Custom widget displaying only hh:mm"""

def format_value(self, value):
"""Removes seconds from a hh:mm:ss string"""

if value in [0, None, ""]:
return value

if len(value.split(":")) == 3 and value.endswith(":00"):
value = value[:-3]

return value


class Select(forms.Select):
"""custom template for select widget"""

Expand Down
18 changes: 18 additions & 0 deletions bookwyrm/migrations/0180_edition_audiobook_play_time.py
@@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-08-01 13:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("bookwyrm", "0179_populate_sort_title"),
]

operations = [
migrations.AddField(
model_name="edition",
name="audiobook_play_time",
field=models.DurationField(blank=True, null=True),
),
]
4 changes: 4 additions & 0 deletions bookwyrm/models/book.py
Expand Up @@ -299,6 +299,10 @@ 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 = models.DurationField(
Copy link
Member

Choose a reason for hiding this comment

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

Because this is calling a standard Django model field, rather than a custom BookWyrm activitypub model field, it's not going to be serialized in ActivityPub. To fix this, there needs to be a subclass of DurationField in https://github.com/bookwyrm-social/bookwyrm/blob/main/bookwyrm/models/fields.py#L528 -- that code will handle parsing the string version of the play time into a DurationField.

I know this is kind of murky depths of how the activitypub implementation works --is that something you want to do, and if so does that give you enough of an idea of how to proceed? If you'd rather not, I can go ahead and make that change.

Copy link
Member

Choose a reason for hiding this comment

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

@joachimesque Have you seen this?

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
33 changes: 33 additions & 0 deletions bookwyrm/static/js/forms.js
Expand Up @@ -58,4 +58,37 @@
}
}
});

/**
* Toggle between two fields depending on select value
*
* @param {target} the select that will direct the toggle
*/
function handleToggleOnSelect(target) {
mouse-reeve marked this conversation as resolved.
Show resolved Hide resolved
const value = target.value;
const toggleStrategy = JSON.parse(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) => {
handleToggleOnSelect(node);

node.addEventListener("change", () => handleToggleOnSelect(node));
});
})();
4 changes: 4 additions & 0 deletions bookwyrm/templates/book/book.html
Expand Up @@ -129,6 +129,10 @@ <h1 class="title" itemprop="name" dir="auto">
<section class="is-clipped">
{% with book=book %}
<div class="content">
{% include 'book/format_info.html' %}
</div>

<div class="content my-3">
{% include 'book/publisher_info.html' %}
</div>

Expand Down
14 changes: 13 additions & 1 deletion bookwyrm/templates/book/edit/edit_book_form.html
Expand Up @@ -286,14 +286,26 @@ <h2 class="title is-4">
</div>
</div>

<div class="field">
<div class="field" 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" 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
39 changes: 39 additions & 0 deletions bookwyrm/templates/book/format_info.html
@@ -0,0 +1,39 @@
{% spaceless %}

{% load i18n %}
{% 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 %}
{% with pages=book.pages %}
{% if format or pages %}

{% if format_property %}
<meta itemprop="bookFormat" content="{{ format_property }}">
{% if book.physical_format == "AudiobookFormat" and book.audiobook_play_time %}
<meta itemprop="timeRequired" content="{{ book.audiobook_play_time|iso_duration }}">
Copy link
Member

Choose a reason for hiding this comment

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

When I tested this, it looks like this line was passing in a string-ified version of the duration (for the arbitrary numbers I tried, 85 days, 17:36:00), which was throwing an error in the filter. I think that this is because I entered a big enough number that it went over hours and into days

{% endif %}
{% endif %}

{% if pages %}
<meta itemprop="numberOfPages" content="{{ pages }}">
{% endif %}

<p>
{% if format and not pages %}
mouse-reeve marked this conversation as resolved.
Show resolved Hide resolved
{{ format }}
{% if book.physical_format == "AudiobookFormat" and book.audiobook_play_time %}
<br />
{% trans "Play time:" %}
{{ book.audiobook_play_time|localized_duration }}
{% endif %}
{% elif format and pages %}
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
{% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %}
</p>
{% endif %}
{% endwith %}

{% endspaceless %}
26 changes: 1 addition & 25 deletions bookwyrm/templates/book/publisher_info.html
Expand Up @@ -2,31 +2,7 @@

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

{% firstof book.physical_format_detail book.get_physical_format_display as format %}
{% firstof book.physical_format book.physical_format_detail as format_property %}
{% with pages=book.pages %}
{% if format or pages %}

{% if format_property %}
<meta itemprop="bookFormat" content="{{ format_property }}">
{% endif %}

{% if pages %}
<meta itemprop="numberOfPages" content="{{ pages }}">
{% endif %}

<p>
{% if format and not pages %}
{{ format }}
{% elif format and pages %}
{% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %}
{% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %}
</p>
{% endif %}
{% endwith %}
{% load book_display_tags %}

{% if book.languages %}
{% for language in book.languages %}
Expand Down
24 changes: 24 additions & 0 deletions bookwyrm/templatetags/book_display_tags.py
@@ -1,5 +1,7 @@
""" template filters """
from django import template
import humanize

from bookwyrm import models


Expand Down Expand Up @@ -33,3 +35,25 @@ 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)


@register.filter(name="localized_duration")
def get_localized_duration(duration):
"""Returns a localized version of the play time"""

return humanize.precisedelta(duration)


@register.filter(name="iso_duration")
def get_iso_duration(duration):
"""Returns an ISO8601 version of the play time"""
duration = abs(duration)
duration = str(duration).split(":")

iso_string = ["PT"]
if int(duration[0]) > 0:
iso_string.append(f"{str(duration[0]).zfill(2)}H")

iso_string.append(f"{str(duration[1]).zfill(2)}M")

return "".join(iso_string)
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -11,6 +11,7 @@ django-sass-processor==1.2.2
django-csp==3.7
environs==9.5.0
flower==1.2.0
humanize==4.4.0
libsass==0.22.0
Markdown==3.4.1
Pillow==9.4.0
Expand Down