Explorar el Código

Add task to sync with last.fm

Colin Powell hace 2 años
padre
commit
21df4e0a77

+ 1 - 1
poetry.lock

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

+ 1 - 0
pyproject.toml

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

+ 15 - 0
vrobbler/apps/scrobbles/tasks.py

@@ -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()

+ 1 - 0
vrobbler/apps/scrobbles/urls.py

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

+ 13 - 0
vrobbler/celery.py

@@ -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}")

+ 6 - 0
vrobbler/settings.py

@@ -72,6 +72,12 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
 
 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 = [
     "django.contrib.admin",
     "django.contrib.auth",

+ 238 - 200
vrobbler/templates/scrobbles/scrobble_list.html

@@ -4,17 +4,27 @@
 
 {% block content %}
 <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>
         <div class="btn-toolbar mb-2 mb-md-0">
             {% if user.is_authenticated %}
             <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>
-                <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#exportModal">Export</button>
+                {% if user.profile.lastfm_username %}
+
+                <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>
             {% endif %}
             <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>
                     This week
                 </button>
@@ -32,44 +42,54 @@
 
         {% if user.is_authenticated %}
         <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 class="row">
             <div class="col-md">
                 <ul class="nav nav-tabs" id="myTab" role="tablist">
                     <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 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 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 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>
                 </ul>
 
                 <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>
                         <div class="table-responsive">
                             <table class="table table-striped table-sm">
-                            <thead>
-                                <tr>
-                                    <th scope="col">#</th>
-                                    <th scope="col">Artist</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for artist in top_weekly_artists %}
-                                <tr>
-                                    <td>{{artist.num_scrobbles}}</td>
-                                    <td>{{artist.name}}</td>
-                                </tr>
-                                {% endfor %}
-                            </tbody>
+                                <thead>
+                                    <tr>
+                                        <th scope="col">#</th>
+                                        <th scope="col">Artist</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for artist in top_weekly_artists %}
+                                    <tr>
+                                        <td>{{artist.num_scrobbles}}</td>
+                                        <td>{{artist.name}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
                             </table>
                         </div>
                     </div>
@@ -78,216 +98,233 @@
                         <h2>Top tracks this week</h2>
                         <div class="table-responsive">
                             <table class="table table-striped table-sm">
-                            <thead>
-                                <tr>
-                                    <th scope="col">#</th>
-                                    <th scope="col">Track</th>
-                                    <th scope="col">Artist</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for track in top_weekly_tracks %}
-                                <tr>
-                                    <td>{{track.num_scrobbles}}</td>
-                                    <td>{{track.title}}</td>
-                                    <td>{{track.artist.name}}</td>
-                                </tr>
-                                {% endfor %}
-                            </tbody>
+                                <thead>
+                                    <tr>
+                                        <th scope="col">#</th>
+                                        <th scope="col">Track</th>
+                                        <th scope="col">Artist</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for track in top_weekly_tracks %}
+                                    <tr>
+                                        <td>{{track.num_scrobbles}}</td>
+                                        <td>{{track.title}}</td>
+                                        <td>{{track.artist.name}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
                             </table>
                         </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>
                         <div class="table-responsive">
                             <table class="table table-striped table-sm">
-                            <thead>
-                                <tr>
-                                    <th scope="col">#</th>
-                                    <th scope="col">Track</th>
-                                    <th scope="col">Artist</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for track in top_monthly_tracks %}
-                                <tr>
-                                    <td>{{track.num_scrobbles}}</td>
-                                    <td>{{track.title}}</td>
-                                    <td>{{track.artist.name}}</td>
-                                </tr>
-                                {% endfor %}
-                            </tbody>
+                                <thead>
+                                    <tr>
+                                        <th scope="col">#</th>
+                                        <th scope="col">Track</th>
+                                        <th scope="col">Artist</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for track in top_monthly_tracks %}
+                                    <tr>
+                                        <td>{{track.num_scrobbles}}</td>
+                                        <td>{{track.title}}</td>
+                                        <td>{{track.artist.name}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
                             </table>
                         </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>
                         <div class="table-responsive">
                             <table class="table table-striped table-sm">
-                            <thead>
-                                <tr>
-                                    <th scope="col">#</th>
-                                    <th scope="col">Artist</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for artist in top_monthly_artists %}
-                                <tr>
-                                    <td>{{artist.num_scrobbles}}</td>
-                                    <td>{{artist.name}}</td>
-                                </tr>
-                                {% endfor %}
-                            </tbody>
+                                <thead>
+                                    <tr>
+                                        <th scope="col">#</th>
+                                        <th scope="col">Artist</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for artist in top_monthly_artists %}
+                                    <tr>
+                                        <td>{{artist.num_scrobbles}}</td>
+                                        <td>{{artist.name}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
                             </table>
                         </div>
                     </div>
 
                 </div>
             </div>
-        <div class="col-md">
-            <ul class="nav nav-tabs" id="myTab" role="tablist">
-                <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>
-                </li>
-                <li class="nav-item" role="presentation">
-                    <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 class="nav-item" role="presentation">
-                    <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>
-                </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="col-md">
+                <ul class="nav nav-tabs" id="myTab" role="tablist">
+                    <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>
+                    </li>
+                    <li class="nav-item" role="presentation">
+                        <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 class="nav-item" role="presentation">
+                        <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>
+                    </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-pane fade show active" id="latest-listened" role="tabpanel" aria-labelledby="latest-listened-tab">
-                    <h2>Latest listened</h2>
-                    <div class="table-responsive">
-                        <table class="table table-striped table-sm">
-                        <thead>
-                            <tr>
-                            <th scope="col">Time</th>
-                            <th scope="col">Album</th>
-                            <th scope="col">Track</th>
-                            <th scope="col">Artist</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {% for scrobble in object_list %}
-                            <tr>
-                                <td>{{scrobble.timestamp|naturaltime}}</td>
-                                {% if scrobble.track.album.cover_image %}
-                                <td><img src="{{scrobble.track.album.cover_image.url}}" width=50 height=50 style="border:1px solid black;" /></td>
-                                {% else %}
-                                <td>{{scrobble.track.album.name}}</td>
-                                {% endif %}
-                                <td>{{scrobble.track.title}}</td>
-                                <td>{{scrobble.track.artist.name}}</td>
-                            </tr>
-                            {% endfor %}
-                        </tbody>
-                        </table>
+                <div class="tab-content" id="myTabContent2">
+                    <div class="tab-pane fade show active" id="latest-listened" role="tabpanel"
+                        aria-labelledby="latest-listened-tab">
+                        <h2>Latest listened</h2>
+                        <div class="table-responsive">
+                            <table class="table table-striped table-sm">
+                                <thead>
+                                    <tr>
+                                        <th scope="col">Time</th>
+                                        <th scope="col">Album</th>
+                                        <th scope="col">Track</th>
+                                        <th scope="col">Artist</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for scrobble in object_list %}
+                                    <tr>
+                                        <td>{{scrobble.timestamp|naturaltime}}</td>
+                                        {% if scrobble.track.album.cover_image %}
+                                        <td><img src="{{scrobble.track.album.cover_image.url}}" width=50 height=50
+                                                style="border:1px solid black;" /></td>
+                                        {% else %}
+                                        <td>{{scrobble.track.album.name}}</td>
+                                        {% endif %}
+                                        <td>{{scrobble.track.title}}</td>
+                                        <td>{{scrobble.track.artist.name}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
                     </div>
-                </div>
 
-                <div class="tab-pane fade show" id="latest-watched" role="tabpanel" aria-labelledby="latest-watched-tab">
-                    <h2>Latest watched</h2>
-                    <div class="table-responsive">
-                        <table class="table table-striped table-sm">
-                        <thead>
-                            <tr>
-                            <th scope="col">Time</th>
-                            <th scope="col">Title</th>
-                            <th scope="col">Series</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {% for scrobble in video_scrobble_list %}
-                            <tr>
-                                <td>{{scrobble.timestamp|naturaltime}}</td>
-                                <td>{% if scrobble.video.tv_series %}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{% endif %} {{scrobble.video.title}}</td>
-                                <td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}</td>
-                            </tr>
-                            {% endfor %}
-                        </tbody>
-                        </table>
+                    <div class="tab-pane fade show" id="latest-watched" role="tabpanel"
+                        aria-labelledby="latest-watched-tab">
+                        <h2>Latest watched</h2>
+                        <div class="table-responsive">
+                            <table class="table table-striped table-sm">
+                                <thead>
+                                    <tr>
+                                        <th scope="col">Time</th>
+                                        <th scope="col">Title</th>
+                                        <th scope="col">Series</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for scrobble in video_scrobble_list %}
+                                    <tr>
+                                        <td>{{scrobble.timestamp|naturaltime}}</td>
+                                        <td>{% if scrobble.video.tv_series
+                                            %}S{{scrobble.video.season_number}}E{{scrobble.video.episode_number}} -{%
+                                            endif %} {{scrobble.video.title}}</td>
+                                        <td>{% if scrobble.video.tv_series %}{{scrobble.video.tv_series}}{% endif %}
+                                        </td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
                     </div>
-                </div>
 
-                <div class="tab-pane fade show" id="latest-sports" role="tabpanel" aria-labelledby="latest-sports-tab">
-                    <h2>Latest Sports</h2>
-                    <div class="table-responsive">
-                        <table class="table table-striped table-sm">
-                        <thead>
-                            <tr>
-                            <th scope="col">Date</th>
-                            <th scope="col">Title</th>
-                            <th scope="col">League</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {% for scrobble in sport_scrobble_list %}
-                            <tr>
-                                <td>{{scrobble.timestamp|naturaltime}}</td>
-                                <td>{{scrobble.sport_event.title}}</td>
-                                <td>{{scrobble.sport_event.league.abbreviation}}</td>
-                            </tr>
-                            {% endfor %}
-                        </tbody>
-                        </table>
+                    <div class="tab-pane fade show" id="latest-sports" role="tabpanel"
+                        aria-labelledby="latest-sports-tab">
+                        <h2>Latest Sports</h2>
+                        <div class="table-responsive">
+                            <table class="table table-striped table-sm">
+                                <thead>
+                                    <tr>
+                                        <th scope="col">Date</th>
+                                        <th scope="col">Title</th>
+                                        <th scope="col">League</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for scrobble in sport_scrobble_list %}
+                                    <tr>
+                                        <td>{{scrobble.timestamp|naturaltime}}</td>
+                                        <td>{{scrobble.sport_event.title}}</td>
+                                        <td>{{scrobble.sport_event.league.abbreviation}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
                     </div>
-                </div>
 
-                <div class="tab-pane fade show" id="latest-podcasted" role="tabpanel" aria-labelledby="latest-podcasted-tab">
-                    <h2>Latest Podcasted</h2>
-                    <div class="table-responsive">
-                        <table class="table table-striped table-sm">
-                        <thead>
-                            <tr>
-                            <th scope="col">Date</th>
-                            <th scope="col">Title</th>
-                            <th scope="col">Podcast</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            {% for scrobble in podcast_scrobble_list %}
-                            <tr>
-                                <td>{{scrobble.timestamp|naturaltime}}</td>
-                                <td>{{scrobble.podcast_episode.title}}</td>
-                                <td>{{scrobble.podcast_episode.podcast}}</td>
-                            </tr>
-                            {% endfor %}
-                        </tbody>
-                        </table>
+                    <div class="tab-pane fade show" id="latest-podcasted" role="tabpanel"
+                        aria-labelledby="latest-podcasted-tab">
+                        <h2>Latest Podcasted</h2>
+                        <div class="table-responsive">
+                            <table class="table table-striped table-sm">
+                                <thead>
+                                    <tr>
+                                        <th scope="col">Date</th>
+                                        <th scope="col">Title</th>
+                                        <th scope="col">Podcast</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {% for scrobble in podcast_scrobble_list %}
+                                    <tr>
+                                        <td>{{scrobble.timestamp|naturaltime}}</td>
+                                        <td>{{scrobble.podcast_episode.title}}</td>
+                                        <td>{{scrobble.podcast_episode.podcast}}</td>
+                                    </tr>
+                                    {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
                     </div>
-                </div>
 
+                </div>
             </div>
+            {% endif %}
         </div>
-        {% endif %}
-    </div>
 </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-content">
             <div class="modal-header">
                 <h5 class="modal-title" id="importModalLabel">Import scrobbles</h5>
                 <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>
             </div>
             <form action="{% url 'audioscrobbler-file-upload' %}" method="post" enctype="multipart/form-data">
                 <div class="modal-body">
-                        {% csrf_token %}
-                        <div class="form-group">
-                            <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">
-                        </div>
+                    {% csrf_token %}
+                    <div class="form-group">
+                        <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">
+                    </div>
                 </div>
                 <div class="modal-footer">
                     <button type="submit" class="btn btn-primary">Import</button>
@@ -298,21 +335,22 @@
     </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-content">
             <div class="modal-header">
                 <h5 class="modal-title" id="exportModalLabel">Export scrobbles</h5>
                 <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>
             </div>
             <form action="{% url 'scrobbles:export' %}" method="get">
                 <div class="modal-body">
-                        {% csrf_token %}
-                        <div class="form-group">
-                            {{export_form.as_div}}
-                        </div>
+                    {% csrf_token %}
+                    <div class="form-group">
+                        {{export_form.as_div}}
+                    </div>
                 </div>
                 <div class="modal-footer">
                     <button type="submit" class="btn btn-primary">Export</button>
@@ -326,7 +364,7 @@
 
 {% block extra_js %}
 <script>
-$('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') });
-$('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') });
+    $('#importModal').on('shown.bs.modal', function () { $('#importInput').trigger('focus') });
+    $('#exportModal').on('shown.bs.modal', function () { $('#exportInput').trigger('focus') });
 </script>
-{% endblock %}
+{% endblock %}