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:
2025-08-11 16:15:15 +07:00
parent 28f927e973
commit d9f184d084
8 changed files with 655 additions and 0 deletions

6
.env Normal file
View 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
View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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 %}