Add task to sync with last.fm

This commit is contained in:
2023-02-16 02:27:39 -05:00
parent cc82504262
commit 21df4e0a77
7 changed files with 275 additions and 201 deletions

2
poetry.lock generated
View File

@ -1598,7 +1598,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "9d9a0b85a6cb22dd5adba7db19813ef01cf843f229a44d4ea0e2d4b899353d43" content-hash = "4b71b291b00a768d7d3c253a02faf70249d1f10ba85fcb88fc5c80fecb412332"
[metadata.files] [metadata.files]
amqp = [ amqp = [

View File

@ -34,6 +34,7 @@ pytz = "^2022.7.1"
django-redis = "^5.2.0" django-redis = "^5.2.0"
pylast = "^5.1.0" pylast = "^5.1.0"
django-encrypted-field = "^1.0.5" django-encrypted-field = "^1.0.5"
celery = "^5.2.7"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
Werkzeug = "2.0.3" Werkzeug = "2.0.3"

View File

@ -0,0 +1,15 @@
import logging
from celery import shared_task
from scrobbles.models import LastFmImport
logger = logging.getLogger(__name__)
@shared_task
def process_lastfm_import(import_id):
lastfm_import = LastFmImport.objects.filter(id=import_id).first()
if not lastfm_import:
logger.warn(f"LastFmImport not found with id {import_id}")
lastfm_import.process()

View File

@ -12,6 +12,7 @@ urlpatterns = [
views.AudioScrobblerImportCreateView.as_view(), views.AudioScrobblerImportCreateView.as_view(),
name='audioscrobbler-file-upload', name='audioscrobbler-file-upload',
), ),
path('lastfm-import/', views.lastfm_import, name='lastfm-import'),
path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'), path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'), path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'),
path('export/', views.export, name='export'), path('export/', views.export, name='export'),

13
vrobbler/celery.py Normal file
View File

@ -0,0 +1,13 @@
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vrobbler.settings")
app = Celery()
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print(f"Request: {self.request!r}")

View File

@ -72,6 +72,12 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None) REDIS_URL = os.getenv("VROBBLER_REDIS_URL", None)
CELERY_TASK_ALWAYS_EAGER = os.getenv("VROBBLER_SKIP_CELERY", False)
CELERY_BROKER_URL = REDIS_URL if REDIS_URL else "memory://localhost/"
CELERY_RESULT_BACKEND = "django-db"
CELERY_TIMEZONE = os.getenv("VROBBLER_TIME_ZONE", "US/Eastern")
CELERY_TASK_TRACK_STARTED = True
INSTALLED_APPS = [ INSTALLED_APPS = [
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",

View File

@ -4,17 +4,27 @@
{% block content %} {% block content %}
<main class="col-md-4 ms-sm-auto col-lg-10 px-md-4"> <main class="col-md-4 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1> <h1 class="h2">Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-toolbar mb-2 mb-md-0">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="btn-group me-2"> <div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#importModal">Import</button> {% if user.profile.lastfm_username %}
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#exportModal">Export</button>
<form action="{% url 'scrobbles:lastfm-import' %}" method="get">
<button type="submit" class="btn btn-sm btn-outline-secondary">Last.fm Sync</button>
</form>
{% endif %}
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#importModal">Import</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#exportModal">Export</button>
</div> </div>
{% endif %} {% endif %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span data-feather="calendar"></span> <span data-feather="calendar"></span>
This week This week
</button> </button>
@ -32,44 +42,54 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="row"> <div class="row">
<p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> | This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p> <p>Today <b>{{counts.today}}</b> | This Week <b>{{counts.week}}</b> | This Month <b>{{counts.month}}</b> |
This Year <b>{{counts.year}}</b> | All Time <b>{{counts.alltime}}</b></p>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#artists-week" type="button" role="tab" aria-controls="home" aria-selected="true">Weekly Artists</button> <button class="nav-link active" id="home-tab" data-bs-toggle="tab"
data-bs-target="#artists-week" type="button" role="tab" aria-controls="home"
aria-selected="true">Weekly Artists</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="artist-month-tab" data-bs-toggle="tab" data-bs-target="#artists-month" type="button" role="tab" aria-controls="home" aria-selected="true">Monthly Artists</button> <button class="nav-link" id="artist-month-tab" data-bs-toggle="tab"
data-bs-target="#artists-month" type="button" role="tab" aria-controls="home"
aria-selected="true">Monthly Artists</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-week" type="button" role="tab" aria-controls="profile" aria-selected="false">Weekly Tracks</button> <button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-week"
type="button" role="tab" aria-controls="profile" aria-selected="false">Weekly
Tracks</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-month" type="button" role="tab" aria-controls="profile" aria-selected="false">Monthly Tracks</button> <button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tracks-month"
type="button" role="tab" aria-controls="profile" aria-selected="false">Monthly
Tracks</button>
</li> </li>
</ul> </ul>
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="artists-week" role="tabpanel" aria-labelledby="artists-week-tab"> <div class="tab-pane fade show active" id="artists-week" role="tabpanel"
aria-labelledby="artists-week-tab">
<h2>Top artists this week</h2> <h2>Top artists this week</h2>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">#</th> <th scope="col">#</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for artist in top_weekly_artists %} {% for artist in top_weekly_artists %}
<tr> <tr>
<td>{{artist.num_scrobbles}}</td> <td>{{artist.num_scrobbles}}</td>
<td>{{artist.name}}</td> <td>{{artist.name}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
@ -78,216 +98,233 @@
<h2>Top tracks this week</h2> <h2>Top tracks this week</h2>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">#</th> <th scope="col">#</th>
<th scope="col">Track</th> <th scope="col">Track</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for track in top_weekly_tracks %} {% for track in top_weekly_tracks %}
<tr> <tr>
<td>{{track.num_scrobbles}}</td> <td>{{track.num_scrobbles}}</td>
<td>{{track.title}}</td> <td>{{track.title}}</td>
<td>{{track.artist.name}}</td> <td>{{track.artist.name}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="tracks-month" role="tabpanel" aria-labelledby="tracks-month-tab"> <div class="tab-pane fade show" id="tracks-month" role="tabpanel"
aria-labelledby="tracks-month-tab">
<h2>Top tracks this month</h2> <h2>Top tracks this month</h2>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">#</th> <th scope="col">#</th>
<th scope="col">Track</th> <th scope="col">Track</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for track in top_monthly_tracks %} {% for track in top_monthly_tracks %}
<tr> <tr>
<td>{{track.num_scrobbles}}</td> <td>{{track.num_scrobbles}}</td>
<td>{{track.title}}</td> <td>{{track.title}}</td>
<td>{{track.artist.name}}</td> <td>{{track.artist.name}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="tab-pane fade show " id="artists-month" role="tabpanel" aria-labelledby="artists-month-tab"> <div class="tab-pane fade show " id="artists-month" role="tabpanel"
aria-labelledby="artists-month-tab">
<h2>Top artists this month</h2> <h2>Top artists this month</h2>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">#</th> <th scope="col">#</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for artist in top_monthly_artists %} {% for artist in top_monthly_artists %}
<tr> <tr>
<td>{{artist.num_scrobbles}}</td> <td>{{artist.num_scrobbles}}</td>
<td>{{artist.name}}</td> <td>{{artist.name}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md"> <div class="col-md">
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#latest-listened" type="button" role="tab" aria-controls="home" aria-selected="true">Tracks</button> <button class="nav-link active" id="home-tab" data-bs-toggle="tab"
</li> data-bs-target="#latest-listened" type="button" role="tab" aria-controls="home"
<li class="nav-item" role="presentation"> aria-selected="true">Tracks</button>
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched" type="button" role="tab" aria-controls="profile" aria-selected="false">Videos</button> </li>
</li> <li class="nav-item" role="presentation">
<li class="nav-item" role="presentation"> <button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-watched"
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-podcasted" type="button" role="tab" aria-controls="profile" aria-selected="false">Podcasts</button> type="button" role="tab" aria-controls="profile" aria-selected="false">Videos</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-sports" type="button" role="tab" aria-controls="profile" aria-selected="false">Sports</button> <button class="nav-link" id="profile-tab" data-bs-toggle="tab"
</li> data-bs-target="#latest-podcasted" type="button" role="tab" aria-controls="profile"
</ul> aria-selected="false">Podcasts</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#latest-sports"
type="button" role="tab" aria-controls="profile" aria-selected="false">Sports</button>
</li>
</ul>
<div class="tab-content" id="myTabContent2"> <div class="tab-content" id="myTabContent2">
<div class="tab-pane fade show active" id="latest-listened" role="tabpanel" aria-labelledby="latest-listened-tab"> <div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
<h2>Latest listened</h2> aria-labelledby="latest-listened-tab">
<div class="table-responsive"> <h2>Latest listened</h2>
<table class="table table-striped table-sm"> <div class="table-responsive">
<thead> <table class="table table-striped table-sm">
<tr> <thead>
<th scope="col">Time</th> <tr>
<th scope="col">Album</th> <th scope="col">Time</th>
<th scope="col">Track</th> <th scope="col">Album</th>
<th scope="col">Artist</th> <th scope="col">Track</th>
</tr> <th scope="col">Artist</th>
</thead> </tr>
<tbody> </thead>
{% for scrobble in object_list %} <tbody>
<tr> {% for scrobble in object_list %}
<td>{{scrobble.timestamp|naturaltime}}</td> <tr>
{% if scrobble.track.album.cover_image %} <td>{{scrobble.timestamp|naturaltime}}</td>
<td><img src="{{scrobble.track.album.cover_image.url}}" width=50 height=50 style="border:1px solid black;" /></td> {% if scrobble.track.album.cover_image %}
{% else %} <td><img src="{{scrobble.track.album.cover_image.url}}" width=50 height=50
<td>{{scrobble.track.album.name}}</td> style="border:1px solid black;" /></td>
{% endif %} {% else %}
<td>{{scrobble.track.title}}</td> <td>{{scrobble.track.album.name}}</td>
<td>{{scrobble.track.artist.name}}</td> {% endif %}
</tr> <td>{{scrobble.track.title}}</td>
{% endfor %} <td>{{scrobble.track.artist.name}}</td>
</tbody> </tr>
</table> {% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div>
<div class="tab-pane fade show" id="latest-watched" role="tabpanel" aria-labelledby="latest-watched-tab"> <div class="tab-pane fade show" id="latest-watched" role="tabpanel"
<h2>Latest watched</h2> aria-labelledby="latest-watched-tab">
<div class="table-responsive"> <h2>Latest watched</h2>
<table class="table table-striped table-sm"> <div class="table-responsive">
<thead> <table class="table table-striped table-sm">
<tr> <thead>
<th scope="col">Time</th> <tr>
<th scope="col">Title</th> <th scope="col">Time</th>
<th scope="col">Series</th> <th scope="col">Title</th>
</tr> <th scope="col">Series</th>
</thead> </tr>
<tbody> </thead>
{% for scrobble in video_scrobble_list %} <tbody>
<tr> {% for scrobble in video_scrobble_list %}
<td>{{scrobble.timestamp|naturaltime}}</td> <tr>
<td>{% if scrobble.video.tv_series %}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{% endif %} {{scrobble.video.title}}</td> <td>{{scrobble.timestamp|naturaltime}}</td>
<td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}</td> <td>{% if scrobble.video.tv_series
</tr> %}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{%
{% endfor %} endif %} {{scrobble.video.title}}</td>
</tbody> <td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}
</table> </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div>
<div class="tab-pane fade show" id="latest-sports" role="tabpanel" aria-labelledby="latest-sports-tab"> <div class="tab-pane fade show" id="latest-sports" role="tabpanel"
<h2>Latest Sports</h2> aria-labelledby="latest-sports-tab">
<div class="table-responsive"> <h2>Latest Sports</h2>
<table class="table table-striped table-sm"> <div class="table-responsive">
<thead> <table class="table table-striped table-sm">
<tr> <thead>
<th scope="col">Date</th> <tr>
<th scope="col">Title</th> <th scope="col">Date</th>
<th scope="col">League</th> <th scope="col">Title</th>
</tr> <th scope="col">League</th>
</thead> </tr>
<tbody> </thead>
{% for scrobble in sport_scrobble_list %} <tbody>
<tr> {% for scrobble in sport_scrobble_list %}
<td>{{scrobble.timestamp|naturaltime}}</td> <tr>
<td>{{scrobble.sport_event.title}}</td> <td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.sport_event.league.abbreviation}}</td> <td>{{scrobble.sport_event.title}}</td>
</tr> <td>{{scrobble.sport_event.league.abbreviation}}</td>
{% endfor %} </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
</div>
</div> </div>
</div>
<div class="tab-pane fade show" id="latest-podcasted" role="tabpanel" aria-labelledby="latest-podcasted-tab"> <div class="tab-pane fade show" id="latest-podcasted" role="tabpanel"
<h2>Latest Podcasted</h2> aria-labelledby="latest-podcasted-tab">
<div class="table-responsive"> <h2>Latest Podcasted</h2>
<table class="table table-striped table-sm"> <div class="table-responsive">
<thead> <table class="table table-striped table-sm">
<tr> <thead>
<th scope="col">Date</th> <tr>
<th scope="col">Title</th> <th scope="col">Date</th>
<th scope="col">Podcast</th> <th scope="col">Title</th>
</tr> <th scope="col">Podcast</th>
</thead> </tr>
<tbody> </thead>
{% for scrobble in podcast_scrobble_list %} <tbody>
<tr> {% for scrobble in podcast_scrobble_list %}
<td>{{scrobble.timestamp|naturaltime}}</td> <tr>
<td>{{scrobble.podcast_episode.title}}</td> <td>{{scrobble.timestamp|naturaltime}}</td>
<td>{{scrobble.podcast_episode.podcast}}</td> <td>{{scrobble.podcast_episode.title}}</td>
</tr> <td>{{scrobble.podcast_episode.podcast}}</td>
{% endfor %} </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
</div>
</div> </div>
</div>
</div>
</div> </div>
{% endif %}
</div> </div>
{% endif %}
</div>
</main> </main>
<div class="modal fade" id="importModal" tabindex="-1" role="dialog" aria-labelledby="importModalLabel" aria-hidden="true"> <div class="modal fade" id="importModal" tabindex="-1" role="dialog" aria-labelledby="importModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="importModalLabel">Import scrobbles</h5> <h5 class="modal-title" id="importModalLabel">Import scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<form action="{% url 'audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data"> <form action="{% url 'audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
<div class="modal-body"> <div class="modal-body">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="tsv_file" class="col-form-label">Audioscrobbler TSV file:</label> <label for="tsv_file" class="col-form-label">Audioscrobbler TSV file:</label>
<input type="file" name="tsv_file" class="form-control" id="id_tsv_file"> <input type="file" name="tsv_file" class="form-control" id="id_tsv_file">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary">Import</button> <button type="submit" class="btn btn-primary">Import</button>
@ -298,21 +335,22 @@
</div> </div>
</div> </div>
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true"> <div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5> <h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5>
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<form action="{% url 'scrobbles:export' %}" method="get"> <form action="{% url 'scrobbles:export' %}" method="get">
<div class="modal-body"> <div class="modal-body">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
{{export_form.as_div}} {{export_form.as_div}}
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary">Export</button> <button type="submit" class="btn btn-primary">Export</button>
@ -326,7 +364,7 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
$('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') }); $('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') });
$('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') }); $('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') });
</script> </script>
{% endblock %} {% endblock %}