Fix Discogs API integration with proper authentication
- Updated to use python3-discogs-client==2.8 library - Added environment variable configuration for API keys - Implemented proper error handling for missing API credentials - Added .env file for configuration management - Enhanced search functionality with graceful fallbacks - Updated README with Discogs API setup instructions
This commit is contained in:
6
.env
Normal file
6
.env
Normal file
@@ -0,0 +1,6 @@
|
||||
# Discogs API Configuration
|
||||
# Get your API key from: https://www.discogs.com/settings/developers
|
||||
# DISCOGS_USER_TOKEN=your_discogs_user_token_here
|
||||
|
||||
# Optional: Discogs User Agent (recommended)
|
||||
# DISCOGS_USER_AGENT=YourAppName/1.0 +http://yourwebsite.com
|
141
README.md
141
README.md
@@ -0,0 +1,141 @@
|
||||
# Media Inventory App
|
||||
|
||||
A FastAPI-based web application that allows users to search for different types of media (books, vinyl records, CDs, and cassettes) using external APIs.
|
||||
|
||||
## Features
|
||||
|
||||
- 📚 **Book Search**: Search for books using the OpenLibrary API
|
||||
- 🎵 **Music Search**: Search for vinyl records, CDs, and cassettes using the Discogs API
|
||||
- 🎨 **Clean UI**: Bootstrap-based responsive web interface
|
||||
- 🔍 **Smart Search**: Different search parameters based on media type
|
||||
- 📱 **Mobile Friendly**: Responsive design that works on all devices
|
||||
|
||||
## APIs Used
|
||||
|
||||
- **OpenLibrary**: For book information and covers
|
||||
- **Discogs**: For music releases and album art
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd fastapi-inventory
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Configure Discogs API (Required for music searches)**:
|
||||
- Go to [Discogs Developer Settings](https://www.discogs.com/settings/developers)
|
||||
- Create a new application or use an existing one
|
||||
- Generate a User Token
|
||||
- Copy the `.env` file and add your token:
|
||||
```bash
|
||||
# Edit .env file
|
||||
DISCOGS_USER_TOKEN=your_discogs_user_token_here
|
||||
DISCOGS_USER_AGENT=YourAppName/1.0 +http://yourwebsite.com
|
||||
```
|
||||
|
||||
4. **Run the application**:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
Or using uvicorn directly:
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
5. **Open your browser** and navigate to:
|
||||
```
|
||||
http://localhost:8000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Select Media Type**: Choose from Book, Vinyl Record, CD, or Cassette
|
||||
2. **Enter Search Query**: Type the title, album name, or keywords
|
||||
3. **Add Artist (Optional)**: For music searches, you can specify an artist to narrow results
|
||||
4. **Search**: Click the search button to get results
|
||||
5. **View Details**: Click on the external links to view more information on the source websites
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
fastapi-inventory/
|
||||
├── main.py # FastAPI application
|
||||
├── requirements.txt # Python dependencies
|
||||
├── README.md # This file
|
||||
├── templates/ # Jinja2 HTML templates
|
||||
│ ├── base.html # Base template
|
||||
│ ├── index.html # Search form
|
||||
│ └── results.html # Search results
|
||||
└── static/ # Static files
|
||||
└── style.css # Custom CSS styles
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /`: Main search form
|
||||
- `POST /search`: Process search requests and return results
|
||||
- `GET /docs`: FastAPI automatic API documentation
|
||||
- `GET /redoc`: Alternative API documentation
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **FastAPI**: Modern, fast web framework for building APIs
|
||||
- **Uvicorn**: ASGI server for running FastAPI
|
||||
- **httpx**: Async HTTP client for API requests
|
||||
- **Jinja2**: Template engine for HTML rendering
|
||||
- **python-multipart**: For handling form data
|
||||
|
||||
## External APIs
|
||||
|
||||
### OpenLibrary API
|
||||
- **Endpoint**: `https://openlibrary.org/search.json`
|
||||
- **Usage**: Search for books by title, author, or keywords
|
||||
- **Rate Limits**: No authentication required, reasonable rate limits
|
||||
|
||||
### Discogs API
|
||||
- **Endpoint**: `https://api.discogs.com/database/search`
|
||||
- **Usage**: Search for music releases by title, artist, or format
|
||||
- **Rate Limits**: No authentication required for basic searches, 60 requests per minute
|
||||
|
||||
## Features in Detail
|
||||
|
||||
### Book Search
|
||||
- Searches OpenLibrary for book information
|
||||
- Displays title, author, publication year, and ISBN
|
||||
- Shows book covers when available
|
||||
- Links to OpenLibrary pages for more details
|
||||
|
||||
### Music Search
|
||||
- Searches Discogs for music releases
|
||||
- Supports vinyl, CD, and cassette formats
|
||||
- Displays title, artist, year, label, and format information
|
||||
- Shows album artwork when available
|
||||
- Links to Discogs pages for more details
|
||||
|
||||
### User Interface
|
||||
- Clean, modern design using Bootstrap 5
|
||||
- Responsive layout that works on desktop and mobile
|
||||
- Dynamic form that shows/hides artist field based on media type
|
||||
- Card-based results display with consistent layout
|
||||
- Error handling and user feedback
|
||||
|
||||
## Development
|
||||
|
||||
To run in development mode with auto-reload:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
The application will automatically reload when you make changes to the code.
|
||||
|
||||
## License
|
||||
|
||||
This project is open source and available under the MIT License.
|
||||
|
191
main.py
Normal file
191
main.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from fastapi import FastAPI, Request, Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import httpx
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
import json
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import discogs_client as discogs
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="Media Inventory App", description="Search for books, vinyl, CDs, and cassettes")
|
||||
|
||||
# Mount static files and templates
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# API endpoints for external services
|
||||
OPENLIBRARY_SEARCH_URL = "https://openlibrary.org/search.json"
|
||||
DISCOGS_SEARCH_URL = "https://api.discogs.com/database/search"
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request):
|
||||
"""Serve the main search form"""
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@app.post("/search")
|
||||
async def search_media(
|
||||
request: Request,
|
||||
media_type: str = Form(...),
|
||||
query: str = Form(...),
|
||||
artist: Optional[str] = Form(None)
|
||||
):
|
||||
"""Search for media based on type and query"""
|
||||
|
||||
if not query.strip():
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"error": "Please enter a search query"
|
||||
})
|
||||
|
||||
try:
|
||||
if media_type == "book":
|
||||
results = await search_openlibrary(query)
|
||||
else: # vinyl, cd, cassette
|
||||
results = await search_discogs(query, media_type, artist)
|
||||
|
||||
return templates.TemplateResponse("results.html", {
|
||||
"request": request,
|
||||
"results": results,
|
||||
"media_type": media_type,
|
||||
"query": query
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"error": f"Search failed: {str(e)}"
|
||||
})
|
||||
|
||||
async def search_openlibrary(query: str):
|
||||
"""Search OpenLibrary for books"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
params = {
|
||||
"q": query,
|
||||
"limit": 10,
|
||||
"fields": "key,title,author_name,first_publish_year,isbn,cover_i"
|
||||
}
|
||||
|
||||
response = await client.get(OPENLIBRARY_SEARCH_URL, params=params)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
books = []
|
||||
for doc in data.get("docs", []):
|
||||
book = {
|
||||
"title": doc.get("title", "Unknown Title"),
|
||||
"author": ", ".join(doc.get("author_name", ["Unknown Author"])),
|
||||
"year": doc.get("first_publish_year", "Unknown"),
|
||||
"isbn": doc.get("isbn", [None])[0] if doc.get("isbn") else None,
|
||||
"cover_url": f"https://covers.openlibrary.org/b/id/{doc.get('cover_i')}-M.jpg" if doc.get('cover_i') else None,
|
||||
"openlibrary_url": f"https://openlibrary.org{doc.get('key')}" if doc.get('key') else None
|
||||
}
|
||||
books.append(book)
|
||||
|
||||
return books
|
||||
|
||||
async def search_discogs(query: str, media_type: str, artist: Optional[str] = None):
|
||||
"""Search Discogs for vinyl, CDs, and cassettes using discogs_client"""
|
||||
|
||||
# Get API credentials from environment
|
||||
user_token = os.getenv("DISCOGS_USER_TOKEN")
|
||||
user_agent = os.getenv("DISCOGS_USER_AGENT", "MediaInventoryApp/1.0")
|
||||
|
||||
if not user_token:
|
||||
# Return a helpful message if no API key is configured
|
||||
return [{
|
||||
"title": "Discogs API Key Required",
|
||||
"artist": "Configuration Needed",
|
||||
"year": "N/A",
|
||||
"label": "Please add your Discogs API key to the .env file",
|
||||
"format": "See README.md for setup instructions",
|
||||
"cover_url": None,
|
||||
"discogs_url": "https://www.discogs.com/settings/developers"
|
||||
}]
|
||||
|
||||
try:
|
||||
# Initialize Discogs client
|
||||
d = discogs.Client(user_agent, user_token=user_token)
|
||||
|
||||
# Map our media types to Discogs format
|
||||
format_map = {
|
||||
"vinyl": "Vinyl",
|
||||
"cd": "CD",
|
||||
"cassette": "Cassette"
|
||||
}
|
||||
|
||||
# Build search query
|
||||
search_query = query
|
||||
if artist:
|
||||
search_query = f"{artist} {query}"
|
||||
|
||||
# Search for releases
|
||||
search_results = d.search(
|
||||
search_query,
|
||||
type='release',
|
||||
format=format_map.get(media_type, "Vinyl")
|
||||
)
|
||||
|
||||
releases = []
|
||||
# Limit to first 10 results
|
||||
for i, result in enumerate(search_results):
|
||||
if i >= 10:
|
||||
break
|
||||
|
||||
try:
|
||||
# Extract artist name from title or use separate artist field
|
||||
title = result.title
|
||||
artist_name = "Unknown Artist"
|
||||
|
||||
if " - " in title:
|
||||
parts = title.split(" - ", 1)
|
||||
artist_name = parts[0]
|
||||
title = parts[1] if len(parts) > 1 else title
|
||||
|
||||
# Try to get additional details
|
||||
try:
|
||||
year = result.year if hasattr(result, 'year') else "Unknown"
|
||||
labels = [label.name for label in result.labels] if hasattr(result, 'labels') and result.labels else ["Unknown Label"]
|
||||
formats = [f.get('name', 'Unknown') for f in result.formats] if hasattr(result, 'formats') and result.formats else ["Unknown Format"]
|
||||
except:
|
||||
year = "Unknown"
|
||||
labels = ["Unknown Label"]
|
||||
formats = ["Unknown Format"]
|
||||
|
||||
release = {
|
||||
"title": title,
|
||||
"artist": artist_name,
|
||||
"year": str(year),
|
||||
"label": ", ".join(labels),
|
||||
"format": ", ".join(formats),
|
||||
"cover_url": result.thumb if hasattr(result, 'thumb') else None,
|
||||
"discogs_url": result.url if hasattr(result, 'url') else None
|
||||
}
|
||||
releases.append(release)
|
||||
|
||||
except Exception as e:
|
||||
# Skip problematic results but continue processing
|
||||
continue
|
||||
|
||||
return releases
|
||||
|
||||
except Exception as e:
|
||||
# Return error information
|
||||
return [{
|
||||
"title": "Search Error",
|
||||
"artist": "Discogs API",
|
||||
"year": "N/A",
|
||||
"label": f"Error: {str(e)}",
|
||||
"format": "Please check your API configuration",
|
||||
"cover_url": None,
|
||||
"discogs_url": "https://www.discogs.com/settings/developers"
|
||||
}]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
httpx==0.25.2
|
||||
jinja2==3.1.2
|
||||
python-multipart==0.0.6
|
||||
python3-discogs-client==2.8
|
||||
python-dotenv==1.1.1
|
117
static/style.css
Normal file
117
static/style.css
Normal file
@@ -0,0 +1,117 @@
|
||||
/* Custom styles for Media Inventory App */
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Loading animation for search button */
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom media type icons */
|
||||
.media-icon {
|
||||
font-size: 1.2em;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Results grid improvements */
|
||||
.card h5 {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Cover image placeholder styling */
|
||||
.card-img-top.bg-light {
|
||||
background-color: #f8f9fa !important;
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
/* Search form enhancements */
|
||||
.form-text {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Button hover effects */
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Card title truncation for long titles */
|
||||
.card-title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 2.6rem;
|
||||
}
|
||||
|
||||
/* Ensure cards have equal height */
|
||||
.card.h-100 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-body.d-flex.flex-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mt-auto {
|
||||
margin-top: auto !important;
|
||||
}
|
23
templates/base.html
Normal file
23
templates/base.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Media Inventory App{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">📚 Media Inventory</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
86
templates/index.html
Normal file
86
templates/index.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search Media - Media Inventory App{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title mb-0">🔍 Search Media</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/search">
|
||||
<div class="mb-3">
|
||||
<label for="media_type" class="form-label">Media Type</label>
|
||||
<select class="form-select" id="media_type" name="media_type" required>
|
||||
<option value="">Select media type...</option>
|
||||
<option value="book">📚 Book</option>
|
||||
<option value="vinyl">🎵 Vinyl Record</option>
|
||||
<option value="cd">💿 CD</option>
|
||||
<option value="cassette">📼 Cassette</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="query" class="form-label">Search Query</label>
|
||||
<input type="text" class="form-control" id="query" name="query"
|
||||
placeholder="Enter title, album name, or keywords..." required>
|
||||
<div class="form-text">
|
||||
For books: Enter book title or author name<br>
|
||||
For music: Enter album title or song name
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="artist-field" style="display: none;">
|
||||
<label for="artist" class="form-label">Artist (Optional)</label>
|
||||
<input type="text" class="form-control" id="artist" name="artist"
|
||||
placeholder="Enter artist name...">
|
||||
<div class="form-text">
|
||||
Helps narrow down music search results
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||||
🔍 Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">ℹ️ How it works</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li><strong>📚 Books:</strong> Searches OpenLibrary.org for book information</li>
|
||||
<li><strong>🎵 Music (Vinyl/CD/Cassette):</strong> Searches Discogs.com for music releases</li>
|
||||
<li><strong>🎯 Tips:</strong> Be specific with your search terms for better results</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('media_type').addEventListener('change', function() {
|
||||
const artistField = document.getElementById('artist-field');
|
||||
const selectedType = this.value;
|
||||
|
||||
if (selectedType === 'vinyl' || selectedType === 'cd' || selectedType === 'cassette') {
|
||||
artistField.style.display = 'block';
|
||||
} else {
|
||||
artistField.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
84
templates/results.html
Normal file
84
templates/results.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search Results - Media Inventory App{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Search Results</h2>
|
||||
<a href="/" class="btn btn-outline-primary">🔍 New Search</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Query:</strong> "{{ query }}"
|
||||
<strong>Media Type:</strong>
|
||||
{% if media_type == 'book' %}📚 Book{% endif %}
|
||||
{% if media_type == 'vinyl' %}🎵 Vinyl Record{% endif %}
|
||||
{% if media_type == 'cd' %}💿 CD{% endif %}
|
||||
{% if media_type == 'cassette' %}📼 Cassette{% endif %}
|
||||
</div>
|
||||
|
||||
{% if results %}
|
||||
<div class="row">
|
||||
{% for item in results %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card h-100">
|
||||
{% if item.cover_url %}
|
||||
<img src="{{ item.cover_url }}" class="card-img-top" alt="Cover"
|
||||
style="height: 200px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
|
||||
style="height: 200px;">
|
||||
<span class="text-muted">No Image</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">{{ item.title }}</h5>
|
||||
|
||||
{% if media_type == 'book' %}
|
||||
<p class="card-text">
|
||||
<strong>Author:</strong> {{ item.author }}<br>
|
||||
<strong>Year:</strong> {{ item.year }}<br>
|
||||
{% if item.isbn %}
|
||||
<strong>ISBN:</strong> {{ item.isbn }}<br>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="card-text">
|
||||
<strong>Artist:</strong> {{ item.artist }}<br>
|
||||
<strong>Year:</strong> {{ item.year }}<br>
|
||||
<strong>Label:</strong> {{ item.label }}<br>
|
||||
<strong>Format:</strong> {{ item.format }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto">
|
||||
{% if media_type == 'book' and item.openlibrary_url %}
|
||||
<a href="{{ item.openlibrary_url }}" target="_blank"
|
||||
class="btn btn-primary btn-sm">
|
||||
📚 View on OpenLibrary
|
||||
</a>
|
||||
{% elif media_type != 'book' and item.discogs_url %}
|
||||
<a href="{{ item.discogs_url }}" target="_blank"
|
||||
class="btn btn-primary btn-sm">
|
||||
🎵 View on Discogs
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<h4>No results found</h4>
|
||||
<p>Try adjusting your search terms or selecting a different media type.</p>
|
||||
<a href="/" class="btn btn-primary">🔍 Try Another Search</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user