Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,27 @@ def _fetch_musicbrainz_data(mbid: str) -> dict | None:

@login_required
def media_import(request):
"""Display TMDB search page for importing media."""
# Optional: if editing existing media, pass media_id to template
"""Display search page for importing media metadata."""
media_id = request.GET.get("media_id")
context = {"media_id": media_id}
media_type = request.GET.get("media_type", "")
title = request.GET.get("title", "")

# Map media_type to the appropriate import source
source_mapping = {
"FILM": "tmdb",
"TV": "tmdb",
"GAME": "igdb",
"BOOK": "openlibrary",
"COMIC": "openlibrary",
"MUSIC": "musicbrainz",
}
default_source = source_mapping.get(media_type, "tmdb")

context = {
"media_id": media_id,
"default_source": default_source,
"default_query": title,
}
return render(request, "base/media_import.html", context)


Expand Down
2 changes: 1 addition & 1 deletion src/templates/base/media_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h1 class="text-4xl font-bold">
</h1>
</div>
<div class="flex gap-2">
<a href="{% url 'media_import' %}{% if media %}?media_id={{ media.pk }}{% endif %}"
<a href="{% url 'media_import' %}{% if media %}?media_id={{ media.pk }}&media_type={{ media.media_type }}&title={{ media.title|urlencode }}{% endif %}"
class="btn btn-outline btn-sm"
title="{% translate 'Import metadata' %}">
{% lucide "download" class="w-4 h-4" %}
Expand Down
25 changes: 16 additions & 9 deletions src/templates/base/media_import.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,36 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
class="tab"
aria-label="{% translate 'Movies & TV' %}"
id="tab-tmdb"
checked
{% if default_source == "tmdb" or not default_source %}checked{% endif %}
autocomplete="off"
hx-on:click="document.getElementById('source-tmdb').classList.remove('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
<input type="radio"
name="import_source"
class="tab"
aria-label="{% translate 'Video games' %}"
id="tab-igdb"
{% if default_source == "igdb" %}checked{% endif %}
autocomplete="off"
hx-on:click="document.getElementById('source-igdb').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
<input type="radio"
name="import_source"
class="tab"
aria-label="{% translate 'Books' %}"
id="tab-openlibrary"
{% if default_source == "openlibrary" %}checked{% endif %}
autocomplete="off"
hx-on:click="document.getElementById('source-openlibrary').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-musicbrainz').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
<input type="radio"
name="import_source"
class="tab"
aria-label="{% translate 'Music' %}"
id="tab-musicbrainz"
{% if default_source == "musicbrainz" %}checked{% endif %}
autocomplete="off"
hx-on:click="document.getElementById('source-musicbrainz').classList.remove('hidden'); document.getElementById('source-tmdb').classList.add('hidden'); document.getElementById('source-igdb').classList.add('hidden'); document.getElementById('source-openlibrary').classList.add('hidden'); document.getElementById('import-results').innerHTML='';">
</div>
{# TMDB Search (Movies & TV) #}
<div id="source-tmdb" class="card bg-base-200 shadow-md mb-6">
<div id="source-tmdb" class="card bg-base-200 shadow-md mb-6{% if default_source and default_source != 'tmdb' %} hidden{% endif %}">
<div class="card-body p-4 space-y-3">
<div class="flex items-center gap-2 mb-2">
{% lucide "film" class="w-5 h-5" %}
Expand All @@ -56,9 +59,10 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
name="q"
class="input join-item w-full"
placeholder="{% translate 'Search...' %}"
value="{{ default_query }}"
autocomplete="off"
hx-get="{% url 'tmdb_search_htmx' %}{% if media_id %}?media_id={{ media_id }}{% endif %}"
hx-trigger="keyup changed delay:400ms, search"
hx-trigger="keyup changed delay:400ms, search{% if default_query and default_source == 'tmdb' %}, load{% endif %}"
hx-target="#import-results"
hx-include="#tmdb-lang"
hx-indicator="#search-spinner" />
Expand All @@ -84,7 +88,7 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
</div>
</div>
{# IGDB Search (Video games) #}
<div id="source-igdb" class="card bg-base-200 shadow-md mb-6 hidden">
<div id="source-igdb" class="card bg-base-200 shadow-md mb-6{% if default_source != 'igdb' %} hidden{% endif %}">
<div class="card-body p-4 space-y-3">
<div class="flex items-center gap-2 mb-2">
{% lucide "gamepad-2" class="w-5 h-5" %}
Expand All @@ -97,9 +101,10 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
name="q"
class="input join-item w-full"
placeholder="{% translate 'Search...' %}"
value="{{ default_query }}"
autocomplete="off"
hx-get="{% url 'igdb_search_htmx' %}{% if media_id %}?media_id={{ media_id }}{% endif %}"
hx-trigger="keyup changed delay:400ms, search"
hx-trigger="keyup changed delay:400ms, search{% if default_query and default_source == 'igdb' %}, load{% endif %}"
hx-target="#import-results"
hx-indicator="#search-spinner" />
<button type="button"
Expand All @@ -113,7 +118,7 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
</div>
{# OpenLibrary Search (Books) #}
<div id="source-openlibrary"
class="card bg-base-200 shadow-md mb-6 hidden">
class="card bg-base-200 shadow-md mb-6{% if default_source != 'openlibrary' %} hidden{% endif %}">
<div class="card-body p-4 space-y-3">
<div class="flex items-center gap-2 mb-2">
{% lucide "book-open" class="w-5 h-5" %}
Expand All @@ -126,9 +131,10 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
name="q"
class="input join-item w-full"
placeholder="{% translate 'Search...' %}"
value="{{ default_query }}"
autocomplete="off"
hx-get="{% url 'openlibrary_search_htmx' %}{% if media_id %}?media_id={{ media_id }}{% endif %}"
hx-trigger="keyup changed delay:400ms, search"
hx-trigger="keyup changed delay:400ms, search{% if default_query and default_source == 'openlibrary' %}, load{% endif %}"
hx-target="#import-results"
hx-indicator="#search-spinner" />
<button type="button"
Expand All @@ -142,7 +148,7 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
</div>
{# MusicBrainz Search (Music) #}
<div id="source-musicbrainz"
class="card bg-base-200 shadow-md mb-6 hidden">
class="card bg-base-200 shadow-md mb-6{% if default_source != 'musicbrainz' %} hidden{% endif %}">
<div class="card-body p-4 space-y-3">
<div class="flex items-center gap-2 mb-2">
{% lucide "disc-3" class="w-5 h-5" %}
Expand All @@ -155,9 +161,10 @@ <h1 class="text-4xl font-bold">{% translate "Import metadata" %}</h1>
name="q"
class="input join-item w-full"
placeholder="{% translate 'Search...' %}"
value="{{ default_query }}"
autocomplete="off"
hx-get="{% url 'musicbrainz_search_htmx' %}{% if media_id %}?media_id={{ media_id }}{% endif %}"
hx-trigger="keyup changed delay:400ms, search"
hx-trigger="keyup changed delay:400ms, search{% if default_query and default_source == 'musicbrainz' %}, load{% endif %}"
hx-target="#import-results"
hx-indicator="#search-spinner" />
<button type="button"
Expand Down
105 changes: 105 additions & 0 deletions src/tests/core/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,3 +1169,108 @@ def test_saved_view_accepts_descending_sort(self, logged_in_client, user, db):
assert SavedView.objects.filter(user=user, name="Descending Sort View").exists()
saved_view = SavedView.objects.get(user=user, name="Descending Sort View")
assert saved_view.sort == "-created_at"


class TestMediaImportView:
"""Tests for the media_import view."""

def test_media_import_accessible_when_logged_in(self, logged_in_client):
"""The import view is accessible when logged in."""
response = logged_in_client.get(reverse("media_import"))

assert response.status_code == 200
assert "base/media_import.html" in [t.name for t in response.templates]

def test_media_import_default_source_is_tmdb(self, logged_in_client):
"""Without parameters, default source is TMDB."""
response = logged_in_client.get(reverse("media_import"))

assert response.context["default_source"] == "tmdb"
assert response.context["default_query"] == ""

def test_media_import_film_maps_to_tmdb(self, logged_in_client):
"""FILM media type maps to TMDB source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "FILM", "title": "The Matrix"},
)

assert response.context["default_source"] == "tmdb"
assert response.context["default_query"] == "The Matrix"

def test_media_import_tv_maps_to_tmdb(self, logged_in_client):
"""TV media type maps to TMDB source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "TV", "title": "Breaking Bad"},
)

assert response.context["default_source"] == "tmdb"
assert response.context["default_query"] == "Breaking Bad"

def test_media_import_game_maps_to_igdb(self, logged_in_client):
"""GAME media type maps to IGDB source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "GAME", "title": "The Witcher 3"},
)

assert response.context["default_source"] == "igdb"
assert response.context["default_query"] == "The Witcher 3"

def test_media_import_book_maps_to_openlibrary(self, logged_in_client):
"""BOOK media type maps to OpenLibrary source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "BOOK", "title": "1984"},
)

assert response.context["default_source"] == "openlibrary"
assert response.context["default_query"] == "1984"

def test_media_import_comic_maps_to_openlibrary(self, logged_in_client):
"""COMIC media type maps to OpenLibrary source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "COMIC", "title": "Watchmen"},
)

assert response.context["default_source"] == "openlibrary"
assert response.context["default_query"] == "Watchmen"

def test_media_import_music_maps_to_musicbrainz(self, logged_in_client):
"""MUSIC media type maps to MusicBrainz source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "MUSIC", "title": "Abbey Road"},
)

assert response.context["default_source"] == "musicbrainz"
assert response.context["default_query"] == "Abbey Road"

def test_media_import_unknown_type_defaults_to_tmdb(self, logged_in_client):
"""Unknown media type defaults to TMDB source."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "UNKNOWN", "title": "Something"},
)

assert response.context["default_source"] == "tmdb"

def test_media_import_passes_media_id(self, logged_in_client, media):
"""Media ID is passed to the template context."""
response = logged_in_client.get(
reverse("media_import"),
{"media_id": media.pk},
)

assert response.context["media_id"] == str(media.pk)

def test_media_import_handles_special_characters_in_title(self, logged_in_client):
"""Title with special characters is handled correctly."""
response = logged_in_client.get(
reverse("media_import"),
{"media_type": "FILM", "title": "Amélie & Co: L'histoire"},
)

assert response.context["default_query"] == "Amélie & Co: L'histoire"